@n8n/scan-community-package 0.25.0 → 0.26.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/package.json +5 -3
- package/scanner/scanner.mjs +76 -17
- package/scanner/scanner.test.mjs +91 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/scan-community-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Static code analyser for n8n community packages",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"bin": "scanner/cli.mjs",
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
"fast-glob": "3.2.12",
|
|
15
15
|
"axios": "1.18.0",
|
|
16
16
|
"@typescript-eslint/parser": "^8.35.0",
|
|
17
|
+
"eslint-plugin-n8n-nodes-base": "^1.16.7",
|
|
17
18
|
"semver": "7.7.3",
|
|
19
|
+
"typescript": "6.0.2",
|
|
18
20
|
"tmp": "0.2.4",
|
|
19
|
-
"@n8n/eslint-plugin-community-nodes": "0.
|
|
21
|
+
"@n8n/eslint-plugin-community-nodes": "0.24.0"
|
|
20
22
|
},
|
|
21
23
|
"devDependencies": {
|
|
22
24
|
"vitest": "^4.1.9",
|
|
23
|
-
"@n8n/vitest-config": "1.
|
|
25
|
+
"@n8n/vitest-config": "1.17.0"
|
|
24
26
|
},
|
|
25
27
|
"homepage": "https://n8n.io",
|
|
26
28
|
"author": {
|
package/scanner/scanner.mjs
CHANGED
|
@@ -117,29 +117,88 @@ const downloadAndExtractPackage = async (packageName, version) => {
|
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Builds the flat ESLint config the scanner lints packages with. Exported so
|
|
122
|
+
* tests can assert the external `eslint-plugin-n8n-nodes-base` plugin and its
|
|
123
|
+
* rulesets are wired in, independent of ESLint execution.
|
|
124
|
+
*/
|
|
125
|
+
export const buildScanConfig = async () => {
|
|
121
126
|
const { n8nCommunityNodesPlugin } = await import('@n8n/eslint-plugin-community-nodes');
|
|
122
127
|
const tsParser = await import('@typescript-eslint/parser');
|
|
128
|
+
const n8nNodesPlugin = (await import('eslint-plugin-n8n-nodes-base')).default;
|
|
129
|
+
|
|
130
|
+
const parser = tsParser.default ?? tsParser;
|
|
131
|
+
|
|
132
|
+
return defineConfig(
|
|
133
|
+
n8nCommunityNodesPlugin.configs.recommended,
|
|
134
|
+
{
|
|
135
|
+
rules: { 'no-console': 'error' },
|
|
136
|
+
},
|
|
137
|
+
// Register the full `eslint-plugin-n8n-nodes-base` plugin and apply its
|
|
138
|
+
// three rulesets so the scan gate enforces the same rules as
|
|
139
|
+
// `n8n-node lint` (see node-cli/src/configs/eslint.ts). The off-overrides
|
|
140
|
+
// below are kept identical. Scoping differs on purpose: `n8n-node lint`
|
|
141
|
+
// runs at dev-time on `nodes/**` / `credentials/**` `.ts` sources, but
|
|
142
|
+
// published tarballs ship compiled output under `dist/` (e.g.
|
|
143
|
+
// `dist/nodes/Foo/Foo.node.js` + `.d.ts`). We match `nodes`/`credentials`
|
|
144
|
+
// dirs at any depth and target `.ts`/`.d.ts` only — the AST-walking rules
|
|
145
|
+
// resolve against the type-preserving `.d.ts`. Compiled `.js` is
|
|
146
|
+
// deliberately excluded: the description AST is buried in a constructor
|
|
147
|
+
// there so the rules no-op, and file-shape rules like
|
|
148
|
+
// node-filename-against-convention would false-positive on the `.js`
|
|
149
|
+
// extension (that check is meaningful only against `.ts` sources at
|
|
150
|
+
// dev-time). Without the `dist/`-aware glob these rules never run at the
|
|
151
|
+
// gate at all.
|
|
152
|
+
{ plugins: { 'n8n-nodes-base': n8nNodesPlugin } },
|
|
153
|
+
{
|
|
154
|
+
files: ['package.json'],
|
|
155
|
+
rules: { ...n8nNodesPlugin.configs.community.rules },
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
files: ['**/credentials/**/*.ts'],
|
|
159
|
+
rules: {
|
|
160
|
+
...n8nNodesPlugin.configs.credentials.rules,
|
|
161
|
+
// Not valid for community nodes
|
|
162
|
+
'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off',
|
|
163
|
+
// @n8n/eslint-plugin-community-nodes credential-password-field rule is more accurate
|
|
164
|
+
'n8n-nodes-base/cred-class-field-type-options-password-missing': 'off',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
files: ['**/nodes/**/*.ts'],
|
|
169
|
+
rules: {
|
|
170
|
+
...n8nNodesPlugin.configs.nodes.rules,
|
|
171
|
+
// Inputs and outputs can be enum instead of string "main"
|
|
172
|
+
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
|
|
173
|
+
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
|
|
174
|
+
// Sometimes the 3rd party API does have a maximum limit, so maxValue is valid
|
|
175
|
+
'n8n-nodes-base/node-param-type-options-max-value-present': 'off',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
// JSON files (notably `package.json`) are not parseable by ESLint's
|
|
179
|
+
// default JS parser, so register the TypeScript parser for them. The
|
|
180
|
+
// community-nodes rules that gate on `package.json` walk a TSESTree
|
|
181
|
+
// `ObjectExpression` AST, which `@typescript-eslint/parser` produces
|
|
182
|
+
// when given a top-level JSON object literal.
|
|
183
|
+
{
|
|
184
|
+
files: ['**/*.json'],
|
|
185
|
+
languageOptions: { parser },
|
|
186
|
+
},
|
|
187
|
+
// The external `nodes`/`credentials` rulesets walk a TSESTree AST, so
|
|
188
|
+
// TS sources (when present in the tarball) need the TS parser too.
|
|
189
|
+
{
|
|
190
|
+
files: ['**/*.ts'],
|
|
191
|
+
languageOptions: { parser },
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
};
|
|
123
195
|
|
|
196
|
+
export const analyzePackage = async (packageDir) => {
|
|
124
197
|
const eslint = new ESLint({
|
|
125
198
|
cwd: packageDir,
|
|
126
199
|
allowInlineConfig: false,
|
|
127
200
|
overrideConfigFile: true,
|
|
128
|
-
overrideConfig:
|
|
129
|
-
n8nCommunityNodesPlugin.configs.recommended,
|
|
130
|
-
{
|
|
131
|
-
rules: { 'no-console': 'error' },
|
|
132
|
-
},
|
|
133
|
-
// JSON files (notably `package.json`) are not parseable by ESLint's
|
|
134
|
-
// default JS parser, so register the TypeScript parser for them. The
|
|
135
|
-
// community-nodes rules that gate on `package.json` walk a TSESTree
|
|
136
|
-
// `ObjectExpression` AST, which `@typescript-eslint/parser` produces
|
|
137
|
-
// when given a top-level JSON object literal.
|
|
138
|
-
{
|
|
139
|
-
files: ['**/*.json'],
|
|
140
|
-
languageOptions: { parser: tsParser.default ?? tsParser },
|
|
141
|
-
},
|
|
142
|
-
),
|
|
201
|
+
overrideConfig: await buildScanConfig(),
|
|
143
202
|
});
|
|
144
203
|
|
|
145
204
|
try {
|
|
@@ -147,7 +206,7 @@ export const analyzePackage = async (packageDir) => {
|
|
|
147
206
|
// such as `no-overrides-field`, `valid-peer-dependencies`, and
|
|
148
207
|
// `package-name-convention` only run against `package.json`. Without
|
|
149
208
|
// it the scanner silently skips every package.json-based rule.
|
|
150
|
-
const filesToLint = glob.sync(['**/*.js', '**/*.json'], {
|
|
209
|
+
const filesToLint = glob.sync(['**/*.js', '**/*.ts', '**/*.json'], {
|
|
151
210
|
cwd: packageDir,
|
|
152
211
|
absolute: true,
|
|
153
212
|
ignore: ['node_modules/**', '**/package-lock.json'],
|
package/scanner/scanner.test.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
5
|
|
|
6
|
-
import { analyzePackage } from './scanner.mjs';
|
|
6
|
+
import { analyzePackage, buildScanConfig } from './scanner.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Build a temporary package directory on disk so we can hand it to
|
|
@@ -23,6 +23,56 @@ function makeFixturePackage(files) {
|
|
|
23
23
|
return dir;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
describe('buildScanConfig', () => {
|
|
27
|
+
// CE-1606: the scan gate must register the external eslint-plugin-n8n-nodes-base
|
|
28
|
+
// and apply all three rulesets, mirroring `n8n-node lint`. Assert on the
|
|
29
|
+
// resolved config rather than on rule execution: under vitest the external
|
|
30
|
+
// plugin resolves a different @typescript-eslint parser instance than the one
|
|
31
|
+
// it was compiled against, so its AST-walking rules silently no-op in-process
|
|
32
|
+
// (they run correctly when the scanner is invoked via `node`, as the CLI does).
|
|
33
|
+
it('registers the external n8n-nodes-base plugin', async () => {
|
|
34
|
+
const config = await buildScanConfig();
|
|
35
|
+
const pluginBlock = config.find((block) => block.plugins?.['n8n-nodes-base']);
|
|
36
|
+
|
|
37
|
+
expect(pluginBlock).toBeDefined();
|
|
38
|
+
expect(pluginBlock.plugins['n8n-nodes-base'].configs).toHaveProperty('community');
|
|
39
|
+
expect(pluginBlock.plugins['n8n-nodes-base'].configs).toHaveProperty('credentials');
|
|
40
|
+
expect(pluginBlock.plugins['n8n-nodes-base'].configs).toHaveProperty('nodes');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('applies each external ruleset with the correct file scoping', async () => {
|
|
44
|
+
const config = await buildScanConfig();
|
|
45
|
+
const ruleBlockFor = (glob) =>
|
|
46
|
+
config.find(
|
|
47
|
+
(block) =>
|
|
48
|
+
block.files?.includes(glob) &&
|
|
49
|
+
Object.keys(block.rules ?? {}).some((r) => r.startsWith('n8n-nodes-base/')),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// community ruleset → package.json
|
|
53
|
+
expect(ruleBlockFor('package.json')).toBeDefined();
|
|
54
|
+
// credentials ruleset → credentials dir at any depth (incl. dist/)
|
|
55
|
+
expect(ruleBlockFor('**/credentials/**/*.ts')).toBeDefined();
|
|
56
|
+
// nodes ruleset → nodes dir at any depth (incl. dist/)
|
|
57
|
+
expect(ruleBlockFor('**/nodes/**/*.ts')).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('carries over the same off-overrides node-cli applies', async () => {
|
|
61
|
+
const config = await buildScanConfig();
|
|
62
|
+
const rules = Object.assign({}, ...config.map((block) => block.rules ?? {}));
|
|
63
|
+
|
|
64
|
+
for (const rule of [
|
|
65
|
+
'n8n-nodes-base/cred-class-field-documentation-url-miscased',
|
|
66
|
+
'n8n-nodes-base/cred-class-field-type-options-password-missing',
|
|
67
|
+
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node',
|
|
68
|
+
'n8n-nodes-base/node-class-description-outputs-wrong',
|
|
69
|
+
'n8n-nodes-base/node-param-type-options-max-value-present',
|
|
70
|
+
]) {
|
|
71
|
+
expect(rules[rule]).toBe('off');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
26
76
|
describe('analyzePackage', () => {
|
|
27
77
|
let fixtureDir;
|
|
28
78
|
|
|
@@ -56,8 +106,12 @@ describe('analyzePackage', () => {
|
|
|
56
106
|
'package.json': {
|
|
57
107
|
name: 'n8n-nodes-fixture',
|
|
58
108
|
version: '1.0.0',
|
|
109
|
+
description: 'A fixture community node package',
|
|
110
|
+
license: 'MIT',
|
|
111
|
+
author: { name: 'Test Author', email: 'test@example.com' },
|
|
59
112
|
keywords: ['n8n-community-node-package'],
|
|
60
113
|
peerDependencies: { 'n8n-workflow': '*' },
|
|
114
|
+
n8n: { n8nNodesApiVersion: 1, nodes: ['dist/nodes/Foo/Foo.node.js'] },
|
|
61
115
|
},
|
|
62
116
|
'index.js': "module.exports = {};\n",
|
|
63
117
|
});
|
|
@@ -85,6 +139,42 @@ describe('analyzePackage', () => {
|
|
|
85
139
|
expect(result.details).toContain('no-forbidden-lifecycle-scripts');
|
|
86
140
|
});
|
|
87
141
|
|
|
142
|
+
// A well-formed node package's compiled sources must not trip the external
|
|
143
|
+
// node rules — those rules can't see enough in `.d.ts`/`.js` output to fire,
|
|
144
|
+
// so scanning must not produce false positives on legitimate packages.
|
|
145
|
+
it('does not false-positive on a well-formed node package layout (CE-1606)', async () => {
|
|
146
|
+
fixtureDir = makeFixturePackage({
|
|
147
|
+
'package.json': {
|
|
148
|
+
name: 'n8n-nodes-fixture',
|
|
149
|
+
version: '1.0.0',
|
|
150
|
+
description: 'A fixture community node package',
|
|
151
|
+
license: 'MIT',
|
|
152
|
+
author: { name: 'Test Author', email: 'test@example.com' },
|
|
153
|
+
keywords: ['n8n-community-node-package'],
|
|
154
|
+
peerDependencies: { 'n8n-workflow': '*' },
|
|
155
|
+
n8n: { n8nNodesApiVersion: 1, nodes: ['dist/nodes/Foo/Foo.node.js'] },
|
|
156
|
+
},
|
|
157
|
+
'dist/nodes/Foo/Foo.node.d.ts': `export declare class Foo {
|
|
158
|
+
description: {
|
|
159
|
+
displayName: string;
|
|
160
|
+
name: string;
|
|
161
|
+
properties: {
|
|
162
|
+
displayName: string;
|
|
163
|
+
name: string;
|
|
164
|
+
type: string;
|
|
165
|
+
default: string;
|
|
166
|
+
}[];
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
`,
|
|
170
|
+
'dist/nodes/Foo/Foo.node.js': "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.Foo = void 0;\nclass Foo {}\nexports.Foo = Foo;\n",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await analyzePackage(fixtureDir);
|
|
174
|
+
|
|
175
|
+
expect(result.passed).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
88
178
|
it('returns passed when the package contains no lintable files', async () => {
|
|
89
179
|
fixtureDir = makeFixturePackage({
|
|
90
180
|
'README.md': '# empty package\n',
|