@n8n/scan-community-package 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.
- package/package.json +5 -5
- package/scanner/provenance.mjs +1 -1
- package/scanner/scanner.mjs +169 -16
- package/scanner/scanner.test.mjs +172 -5
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/scan-community-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|
|
7
7
|
"files": [
|
|
8
8
|
"scanner",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"LICENSE_EE.md",
|
|
10
|
+
"LICENSE.md"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"eslint": "9.29.0",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"semver": "7.7.3",
|
|
19
19
|
"typescript": "6.0.2",
|
|
20
20
|
"tmp": "0.2.4",
|
|
21
|
-
"@n8n/eslint-plugin-community-nodes": "0.
|
|
21
|
+
"@n8n/eslint-plugin-community-nodes": "0.25.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"vitest": "^4.1.9",
|
|
25
|
-
"@n8n/vitest-config": "1.
|
|
25
|
+
"@n8n/vitest-config": "1.18.0"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://n8n.io",
|
|
28
28
|
"author": {
|
package/scanner/provenance.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const NPM_PROVENANCE_PREDICATE_TYPE = 'https://slsa.dev/provenance/v1';
|
|
1
|
+
export const NPM_PROVENANCE_PREDICATE_TYPE = 'https://slsa.dev/provenance/v1';
|
|
2
2
|
const N8N_COMMUNITY_NODE_PUBLISH_DOCS_URL =
|
|
3
3
|
'https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/';
|
|
4
4
|
|
package/scanner/scanner.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import glob from 'fast-glob';
|
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { defineConfig } from 'eslint/config';
|
|
13
13
|
|
|
14
|
-
import { checkPackageProvenance } from './provenance.mjs';
|
|
14
|
+
import { checkPackageProvenance, NPM_PROVENANCE_PREDICATE_TYPE } from './provenance.mjs';
|
|
15
15
|
|
|
16
16
|
const { stdout } = process;
|
|
17
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -117,6 +117,110 @@ const downloadAndExtractPackage = async (packageName, version) => {
|
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Extracts the source repository and commit a package was built from, out of
|
|
122
|
+
* its npm provenance attestation. Provenance is already mandatory for the
|
|
123
|
+
* scan to proceed, so any package that reaches this point attests exactly
|
|
124
|
+
* which source produced the published artifact.
|
|
125
|
+
*
|
|
126
|
+
* Returns `{ owner, repo, gitCommit }`, or `null` when the attestation is
|
|
127
|
+
* missing, malformed, or points at an unsupported host.
|
|
128
|
+
*/
|
|
129
|
+
export const parseSourceRepo = (attestations) => {
|
|
130
|
+
const provenance = attestations?.find((a) => a.predicateType === NPM_PROVENANCE_PREDICATE_TYPE);
|
|
131
|
+
const payload = provenance?.bundle?.dsseEnvelope?.payload;
|
|
132
|
+
if (!payload) return null;
|
|
133
|
+
|
|
134
|
+
let statement;
|
|
135
|
+
try {
|
|
136
|
+
statement = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const dependency = statement?.predicate?.buildDefinition?.resolvedDependencies?.[0];
|
|
142
|
+
const gitCommit = dependency?.digest?.gitCommit;
|
|
143
|
+
// ponytail: GitHub only — add a host→archive-URL mapping if GitLab-built packages show up
|
|
144
|
+
const match =
|
|
145
|
+
/^git\+https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:@|$)/.exec(
|
|
146
|
+
dependency?.uri ?? '',
|
|
147
|
+
);
|
|
148
|
+
if (!match || !/^[0-9a-f]{40,64}$/i.test(gitCommit ?? '')) return null;
|
|
149
|
+
|
|
150
|
+
return { owner: match[1], repo: match[2], gitCommit };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// A source fetch failure fails the scan outright, so bound the requests —
|
|
154
|
+
// a stalled connection must not hang the gate.
|
|
155
|
+
const SOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
156
|
+
|
|
157
|
+
const fetchSourceInfo = async (packageName, version) => {
|
|
158
|
+
const { data } = await axios.get(`${registry}-/npm/v1/attestations/${packageName}@${version}`, {
|
|
159
|
+
timeout: SOURCE_FETCH_TIMEOUT_MS,
|
|
160
|
+
});
|
|
161
|
+
return parseSourceRepo(data.attestations);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Finds the directory inside a source checkout whose package.json declares
|
|
166
|
+
* the given package name — handles both single-package repos and monorepos.
|
|
167
|
+
*/
|
|
168
|
+
export const findPackageRoot = (sourceDir, packageName) => {
|
|
169
|
+
const packageJsonPaths = glob.sync('**/package.json', {
|
|
170
|
+
cwd: sourceDir,
|
|
171
|
+
absolute: true,
|
|
172
|
+
ignore: ['**/node_modules/**'],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
176
|
+
try {
|
|
177
|
+
if (JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).name === packageName) {
|
|
178
|
+
return path.dirname(packageJsonPath);
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Unparseable package.json (e.g. a fixture) — keep looking
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const downloadAndExtractSource = async ({ owner, repo, gitCommit }, packageName) => {
|
|
189
|
+
const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/${gitCommit}`;
|
|
190
|
+
const { data } = await axios.get(url, {
|
|
191
|
+
responseType: 'arraybuffer',
|
|
192
|
+
timeout: SOURCE_FETCH_TIMEOUT_MS,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const tarballName = `source-${gitCommit}.tgz`;
|
|
196
|
+
fs.writeFileSync(safeJoinPath(TEMP_DIR, tarballName), Buffer.from(data));
|
|
197
|
+
|
|
198
|
+
const sourceDir = safeJoinPath(TEMP_DIR, `source-${gitCommit}`);
|
|
199
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
200
|
+
const tarResult = spawnSync(
|
|
201
|
+
'tar',
|
|
202
|
+
['-xzf', tarballName, '-C', sourceDir, '--strip-components=1'],
|
|
203
|
+
{
|
|
204
|
+
cwd: TEMP_DIR,
|
|
205
|
+
stdio: 'pipe',
|
|
206
|
+
shell: process.platform === 'win32',
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
if (tarResult.status !== 0) {
|
|
210
|
+
throw new Error(`tar extraction failed: ${tarResult.stderr?.toString()}`);
|
|
211
|
+
}
|
|
212
|
+
fs.unlinkSync(safeJoinPath(TEMP_DIR, tarballName));
|
|
213
|
+
|
|
214
|
+
return findPackageRoot(sourceDir, packageName);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* What `n8n-node lint` covers at dev time: the shippable node/credential
|
|
219
|
+
* sources plus package.json. Deliberately excludes repo dev files (gulpfile,
|
|
220
|
+
* test configs, committed dist/) that never end up in the published package.
|
|
221
|
+
*/
|
|
222
|
+
export const SOURCE_FILE_PATTERNS = ['package.json', '{nodes,credentials}/**/*.{js,ts,json}'];
|
|
223
|
+
|
|
120
224
|
/**
|
|
121
225
|
* Builds the flat ESLint config the scanner lints packages with. Exported so
|
|
122
226
|
* tests can assert the external `eslint-plugin-n8n-nodes-base` plugin and its
|
|
@@ -137,18 +241,12 @@ export const buildScanConfig = async () => {
|
|
|
137
241
|
// Register the full `eslint-plugin-n8n-nodes-base` plugin and apply its
|
|
138
242
|
// three rulesets so the scan gate enforces the same rules as
|
|
139
243
|
// `n8n-node lint` (see node-cli/src/configs/eslint.ts). The off-overrides
|
|
140
|
-
// below are kept identical.
|
|
141
|
-
//
|
|
142
|
-
// published
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
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.
|
|
244
|
+
// below are kept identical. The `.ts` globs only ever match the
|
|
245
|
+
// provenance-attested source checkout — the tarball leg lints compiled
|
|
246
|
+
// `.js` and the published package.json only, where these rules would
|
|
247
|
+
// no-op (the description AST is buried in a constructor) or
|
|
248
|
+
// false-positive (the filename-convention rules hard-code a `.ts`
|
|
249
|
+
// suffix that compiled output can never satisfy).
|
|
152
250
|
{ plugins: { 'n8n-nodes-base': n8nNodesPlugin } },
|
|
153
251
|
{
|
|
154
252
|
files: ['package.json'],
|
|
@@ -193,7 +291,10 @@ export const buildScanConfig = async () => {
|
|
|
193
291
|
);
|
|
194
292
|
};
|
|
195
293
|
|
|
196
|
-
export const analyzePackage = async (
|
|
294
|
+
export const analyzePackage = async (
|
|
295
|
+
packageDir,
|
|
296
|
+
filePatterns = ['**/*.js', '**/*.ts', '**/*.json'],
|
|
297
|
+
) => {
|
|
197
298
|
const eslint = new ESLint({
|
|
198
299
|
cwd: packageDir,
|
|
199
300
|
allowInlineConfig: false,
|
|
@@ -206,7 +307,7 @@ export const analyzePackage = async (packageDir) => {
|
|
|
206
307
|
// such as `no-overrides-field`, `valid-peer-dependencies`, and
|
|
207
308
|
// `package-name-convention` only run against `package.json`. Without
|
|
208
309
|
// it the scanner silently skips every package.json-based rule.
|
|
209
|
-
const filesToLint = glob.sync(
|
|
310
|
+
const filesToLint = glob.sync(filePatterns, {
|
|
210
311
|
cwd: packageDir,
|
|
211
312
|
absolute: true,
|
|
212
313
|
ignore: ['node_modules/**', '**/package-lock.json'],
|
|
@@ -287,6 +388,46 @@ export const analyzePackageByName = async (packageName, version) => {
|
|
|
287
388
|
|
|
288
389
|
stdout.write(`✅ Provenance check passed for ${label} \n`);
|
|
289
390
|
|
|
391
|
+
// Lint the source the provenance attestation points at: the
|
|
392
|
+
// node/credential rules are written for `.ts` sources and mostly no-op
|
|
393
|
+
// (or false-positive on filenames) against the compiled output shipped
|
|
394
|
+
// in the tarball. An unreachable source is a hard failure — falling
|
|
395
|
+
// back to a tarball-only scan would silently reintroduce that blind
|
|
396
|
+
// spot.
|
|
397
|
+
stdout.write(`Fetching source for ${label}...`);
|
|
398
|
+
let sourceDir = null;
|
|
399
|
+
let sourceInfo = null;
|
|
400
|
+
let sourceError = null;
|
|
401
|
+
try {
|
|
402
|
+
sourceInfo = await fetchSourceInfo(packageName, exactVersion);
|
|
403
|
+
if (sourceInfo) {
|
|
404
|
+
sourceDir = await downloadAndExtractSource(sourceInfo, packageName);
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
sourceError = error;
|
|
408
|
+
}
|
|
409
|
+
if (stdout.TTY) {
|
|
410
|
+
stdout.clearLine(0);
|
|
411
|
+
stdout.cursorTo(0);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!sourceDir) {
|
|
415
|
+
const reason = sourceError?.message ?? 'unsupported or unlocatable source repository';
|
|
416
|
+
stdout.write(`❌ Could not fetch source for ${label} \n`);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
packageName,
|
|
420
|
+
version: exactVersion,
|
|
421
|
+
passed: false,
|
|
422
|
+
message: `Could not fetch the source repository recorded in the package's npm provenance (${reason}). The scan lints the attested source, so it must be reachable — publish with provenance from a public GitHub repository.`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const shortCommit = sourceInfo.gitCommit.slice(0, 7);
|
|
427
|
+
stdout.write(
|
|
428
|
+
`✅ Fetched source from github.com/${sourceInfo.owner}/${sourceInfo.repo}@${shortCommit} \n`,
|
|
429
|
+
);
|
|
430
|
+
|
|
290
431
|
stdout.write(`Downloading ${label}...`);
|
|
291
432
|
const packageDir = await downloadAndExtractPackage(packageName, exactVersion);
|
|
292
433
|
if (stdout.TTY) {
|
|
@@ -296,7 +437,19 @@ export const analyzePackageByName = async (packageName, version) => {
|
|
|
296
437
|
stdout.write(`✅ Downloaded ${label} \n`);
|
|
297
438
|
|
|
298
439
|
stdout.write(`Analyzing ${label}...`);
|
|
299
|
-
|
|
440
|
+
// The source checkout gets the full rule set on real `.ts` sources.
|
|
441
|
+
// The shipped artifact must stay scanned too: provenance pins the
|
|
442
|
+
// source commit, not the build output — a build step can emit anything
|
|
443
|
+
// into `dist/`. Scope the tarball leg to compiled `.js` and the
|
|
444
|
+
// published package.json; `.ts`/`.d.ts` declarations are covered better
|
|
445
|
+
// by the source scan and only false-positive on filename rules here.
|
|
446
|
+
const sourceResult = await analyzePackage(sourceDir, SOURCE_FILE_PATTERNS);
|
|
447
|
+
const distResult = await analyzePackage(packageDir, ['**/*.js', 'package.json']);
|
|
448
|
+
const analysisResult = {
|
|
449
|
+
passed: sourceResult.passed && distResult.passed,
|
|
450
|
+
message: [sourceResult, distResult].find((r) => !r.passed)?.message,
|
|
451
|
+
details: [sourceResult.details, distResult.details].filter(Boolean).join('\n') || undefined,
|
|
452
|
+
};
|
|
300
453
|
if (stdout.TTY) {
|
|
301
454
|
stdout.clearLine(0);
|
|
302
455
|
stdout.cursorTo(0);
|
package/scanner/scanner.test.mjs
CHANGED
|
@@ -3,7 +3,13 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
analyzePackage,
|
|
8
|
+
buildScanConfig,
|
|
9
|
+
findPackageRoot,
|
|
10
|
+
parseSourceRepo,
|
|
11
|
+
SOURCE_FILE_PATTERNS,
|
|
12
|
+
} from './scanner.mjs';
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* Build a temporary package directory on disk so we can hand it to
|
|
@@ -73,6 +79,125 @@ describe('buildScanConfig', () => {
|
|
|
73
79
|
});
|
|
74
80
|
});
|
|
75
81
|
|
|
82
|
+
describe('parseSourceRepo', () => {
|
|
83
|
+
const makeAttestation = (predicate) => [
|
|
84
|
+
{
|
|
85
|
+
predicateType: 'https://slsa.dev/provenance/v1',
|
|
86
|
+
bundle: {
|
|
87
|
+
dsseEnvelope: {
|
|
88
|
+
payload: Buffer.from(JSON.stringify({ predicate })).toString('base64'),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const commit = 'a'.repeat(40);
|
|
95
|
+
|
|
96
|
+
it('extracts owner, repo and commit from a GitHub attestation', () => {
|
|
97
|
+
const attestations = makeAttestation({
|
|
98
|
+
buildDefinition: {
|
|
99
|
+
resolvedDependencies: [
|
|
100
|
+
{
|
|
101
|
+
uri: `git+https://github.com/acme/n8n-nodes-foo@refs/heads/main`,
|
|
102
|
+
digest: { gitCommit: commit },
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(parseSourceRepo(attestations)).toEqual({
|
|
109
|
+
owner: 'acme',
|
|
110
|
+
repo: 'n8n-nodes-foo',
|
|
111
|
+
gitCommit: commit,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('supports repo names containing dots', () => {
|
|
116
|
+
const attestations = makeAttestation({
|
|
117
|
+
buildDefinition: {
|
|
118
|
+
resolvedDependencies: [
|
|
119
|
+
{
|
|
120
|
+
uri: 'git+https://github.com/acme/n8n-nodes-foo.bar@refs/heads/main',
|
|
121
|
+
digest: { gitCommit: commit },
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(parseSourceRepo(attestations)).toEqual({
|
|
128
|
+
owner: 'acme',
|
|
129
|
+
repo: 'n8n-nodes-foo.bar',
|
|
130
|
+
gitCommit: commit,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns null for non-GitHub source hosts', () => {
|
|
135
|
+
const attestations = makeAttestation({
|
|
136
|
+
buildDefinition: {
|
|
137
|
+
resolvedDependencies: [
|
|
138
|
+
{
|
|
139
|
+
uri: `git+https://gitlab.com/acme/n8n-nodes-foo@refs/heads/main`,
|
|
140
|
+
digest: { gitCommit: commit },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(parseSourceRepo(attestations)).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns null when the commit digest is not a git SHA', () => {
|
|
150
|
+
const attestations = makeAttestation({
|
|
151
|
+
buildDefinition: {
|
|
152
|
+
resolvedDependencies: [
|
|
153
|
+
{
|
|
154
|
+
uri: 'git+https://github.com/acme/n8n-nodes-foo@refs/heads/main',
|
|
155
|
+
digest: { gitCommit: 'not-a-sha' },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(parseSourceRepo(attestations)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns null when there is no provenance attestation', () => {
|
|
165
|
+
expect(parseSourceRepo(undefined)).toBeNull();
|
|
166
|
+
expect(parseSourceRepo([])).toBeNull();
|
|
167
|
+
expect(parseSourceRepo([{ predicateType: 'something-else' }])).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('findPackageRoot', () => {
|
|
172
|
+
let fixtureDir;
|
|
173
|
+
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
if (fixtureDir) {
|
|
176
|
+
fs.rmSync(fixtureDir, { recursive: true, force: true });
|
|
177
|
+
fixtureDir = undefined;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('finds the package directory in a monorepo by package.json name', () => {
|
|
182
|
+
fixtureDir = makeFixturePackage({
|
|
183
|
+
'package.json': { name: 'monorepo-root', private: true },
|
|
184
|
+
'packages/foo/package.json': { name: 'n8n-nodes-foo', version: '1.0.0' },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(findPackageRoot(fixtureDir, 'n8n-nodes-foo')).toBe(
|
|
188
|
+
path.join(fixtureDir, 'packages', 'foo'),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns null when no package.json declares the package name', () => {
|
|
193
|
+
fixtureDir = makeFixturePackage({
|
|
194
|
+
'package.json': { name: 'something-else' },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(findPackageRoot(fixtureDir, 'n8n-nodes-foo')).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
76
201
|
describe('analyzePackage', () => {
|
|
77
202
|
let fixtureDir;
|
|
78
203
|
|
|
@@ -92,7 +217,7 @@ describe('analyzePackage', () => {
|
|
|
92
217
|
peerDependencies: { 'n8n-workflow': '*' },
|
|
93
218
|
overrides: { 'change-case': '4.1.2' },
|
|
94
219
|
},
|
|
95
|
-
'index.js':
|
|
220
|
+
'index.js': 'module.exports = {};\n',
|
|
96
221
|
});
|
|
97
222
|
|
|
98
223
|
const result = await analyzePackage(fixtureDir);
|
|
@@ -113,7 +238,7 @@ describe('analyzePackage', () => {
|
|
|
113
238
|
peerDependencies: { 'n8n-workflow': '*' },
|
|
114
239
|
n8n: { n8nNodesApiVersion: 1, nodes: ['dist/nodes/Foo/Foo.node.js'] },
|
|
115
240
|
},
|
|
116
|
-
'index.js':
|
|
241
|
+
'index.js': 'module.exports = {};\n',
|
|
117
242
|
});
|
|
118
243
|
|
|
119
244
|
const result = await analyzePackage(fixtureDir);
|
|
@@ -130,7 +255,7 @@ describe('analyzePackage', () => {
|
|
|
130
255
|
peerDependencies: { 'n8n-workflow': '*' },
|
|
131
256
|
scripts: { postinstall: 'node ./malicious.js' },
|
|
132
257
|
},
|
|
133
|
-
'index.js':
|
|
258
|
+
'index.js': 'module.exports = {};\n',
|
|
134
259
|
});
|
|
135
260
|
|
|
136
261
|
const result = await analyzePackage(fixtureDir);
|
|
@@ -167,7 +292,8 @@ describe('analyzePackage', () => {
|
|
|
167
292
|
};
|
|
168
293
|
}
|
|
169
294
|
`,
|
|
170
|
-
'dist/nodes/Foo/Foo.node.js':
|
|
295
|
+
'dist/nodes/Foo/Foo.node.js':
|
|
296
|
+
'"use strict";\nObject.defineProperty(exports, "__esModule", { value: true });\nexports.Foo = void 0;\nclass Foo {}\nexports.Foo = Foo;\n',
|
|
171
297
|
});
|
|
172
298
|
|
|
173
299
|
const result = await analyzePackage(fixtureDir);
|
|
@@ -175,6 +301,47 @@ describe('analyzePackage', () => {
|
|
|
175
301
|
expect(result.passed).toBe(true);
|
|
176
302
|
});
|
|
177
303
|
|
|
304
|
+
// CE-1713: source checkouts are linted with dev-scoped globs — only the
|
|
305
|
+
// shippable `nodes/**` / `credentials/**` sources and package.json, so repo
|
|
306
|
+
// dev files (gulpfile, test configs) can't produce gate-only violations.
|
|
307
|
+
it('ignores repo dev files when linting with source file patterns', async () => {
|
|
308
|
+
fixtureDir = makeFixturePackage({
|
|
309
|
+
'package.json': {
|
|
310
|
+
name: 'n8n-nodes-fixture',
|
|
311
|
+
version: '1.0.0',
|
|
312
|
+
description: 'A fixture community node package',
|
|
313
|
+
license: 'MIT',
|
|
314
|
+
author: { name: 'Test Author', email: 'test@example.com' },
|
|
315
|
+
keywords: ['n8n-community-node-package'],
|
|
316
|
+
peerDependencies: { 'n8n-workflow': '*' },
|
|
317
|
+
n8n: { n8nNodesApiVersion: 1, nodes: ['dist/nodes/Foo/Foo.node.js'] },
|
|
318
|
+
},
|
|
319
|
+
'gulpfile.js': "console.log('building');\n",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const result = await analyzePackage(fixtureDir, SOURCE_FILE_PATTERNS);
|
|
323
|
+
|
|
324
|
+
expect(result.passed).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('flags violations in node sources when linting with source file patterns', async () => {
|
|
328
|
+
fixtureDir = makeFixturePackage({
|
|
329
|
+
'package.json': {
|
|
330
|
+
name: 'n8n-nodes-fixture',
|
|
331
|
+
version: '1.0.0',
|
|
332
|
+
keywords: ['n8n-community-node-package'],
|
|
333
|
+
peerDependencies: { 'n8n-workflow': '*' },
|
|
334
|
+
n8n: { n8nNodesApiVersion: 1, nodes: ['dist/nodes/Foo/Foo.node.js'] },
|
|
335
|
+
},
|
|
336
|
+
'nodes/Foo/Foo.node.ts': "console.log('debug');\nexport class Foo {}\n",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const result = await analyzePackage(fixtureDir, SOURCE_FILE_PATTERNS);
|
|
340
|
+
|
|
341
|
+
expect(result.passed).toBe(false);
|
|
342
|
+
expect(result.details).toContain('no-console');
|
|
343
|
+
});
|
|
344
|
+
|
|
178
345
|
it('returns passed when the package contains no lintable files', async () => {
|
|
179
346
|
fixtureDir = makeFixturePackage({
|
|
180
347
|
'README.md': '# empty package\n',
|