mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
fix(core): Lint package.json in community node tooling (no-changelog) (#29864)
Some checks are pending
Build: Benchmark Image / build (push) Waiting to run
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Util: Sync API Docs / sync-public-api (push) Waiting to run
Some checks are pending
Build: Benchmark Image / build (push) Waiting to run
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Util: Sync API Docs / sync-public-api (push) Waiting to run
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d63e1ae84e
commit
8b54333739
78
packages/@n8n/node-cli/src/configs/eslint.test.ts
Normal file
78
packages/@n8n/node-cli/src/configs/eslint.test.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { ESLint } from 'eslint';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { describe, expect } from 'vitest';
|
||||
|
||||
import { config, configWithoutCloudSupport } from './eslint';
|
||||
import { tmpdirTest } from '../test-utils/temp-fs';
|
||||
|
||||
/**
|
||||
* These tests guard against regressions in the flat-config layout where
|
||||
* `@n8n/eslint-plugin-community-nodes` rules that target `package.json`
|
||||
* (e.g. `no-overrides-field`) get loaded but never actually fire because
|
||||
* they are wrapped inside a `**\/*.ts`-scoped `extends:` block. See CE-1023
|
||||
* for the analogous bug in `@n8n/scan-community-package`.
|
||||
*/
|
||||
async function lintFile(filePath: string, eslintConfig: typeof config) {
|
||||
const eslint = new ESLint({
|
||||
cwd: path.dirname(filePath),
|
||||
overrideConfigFile: true,
|
||||
// `tseslint.config()` returns the typescript-eslint `ConfigArray`, which
|
||||
// is structurally a `Linter.Config[]` but nominally a different type due
|
||||
// to duplicated `Parser` definitions across packages.
|
||||
overrideConfig: eslintConfig as unknown as ESLint.Options['overrideConfig'],
|
||||
});
|
||||
const [result] = await eslint.lintFiles([filePath]);
|
||||
return result;
|
||||
}
|
||||
|
||||
const packageJsonWithOverrides = `{
|
||||
"name": "n8n-nodes-fixture",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["n8n-community-node-package"],
|
||||
"peerDependencies": { "n8n-workflow": "*" },
|
||||
"overrides": { "change-case": "4.1.2" }
|
||||
}`;
|
||||
|
||||
const packageJsonWithLifecycleScript = `{
|
||||
"name": "n8n-nodes-fixture",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["n8n-community-node-package"],
|
||||
"peerDependencies": { "n8n-workflow": "*" },
|
||||
"scripts": { "postinstall": "node ./malicious.js" }
|
||||
}`;
|
||||
|
||||
describe('@n8n/node-cli eslint config', () => {
|
||||
tmpdirTest(
|
||||
'flags an `overrides` field in package.json (regression for CE-1023)',
|
||||
async ({ tmpdir }) => {
|
||||
const pkgPath = path.join(tmpdir, 'package.json');
|
||||
await fs.writeFile(pkgPath, packageJsonWithOverrides);
|
||||
|
||||
const result = await lintFile(pkgPath, config);
|
||||
|
||||
const ruleIds = result.messages.map((m) => m.ruleId);
|
||||
expect(ruleIds).toContain('@n8n/community-nodes/no-overrides-field');
|
||||
},
|
||||
);
|
||||
|
||||
tmpdirTest('flags forbidden lifecycle scripts in package.json', async ({ tmpdir }) => {
|
||||
const pkgPath = path.join(tmpdir, 'package.json');
|
||||
await fs.writeFile(pkgPath, packageJsonWithLifecycleScript);
|
||||
|
||||
const result = await lintFile(pkgPath, config);
|
||||
|
||||
const ruleIds = result.messages.map((m) => m.ruleId);
|
||||
expect(ruleIds).toContain('@n8n/community-nodes/no-forbidden-lifecycle-scripts');
|
||||
});
|
||||
|
||||
tmpdirTest('configWithoutCloudSupport also lints package.json rules', async ({ tmpdir }) => {
|
||||
const pkgPath = path.join(tmpdir, 'package.json');
|
||||
await fs.writeFile(pkgPath, packageJsonWithOverrides);
|
||||
|
||||
const result = await lintFile(pkgPath, configWithoutCloudSupport);
|
||||
|
||||
const ruleIds = result.messages.map((m) => m.ruleId);
|
||||
expect(ruleIds).toContain('@n8n/community-nodes/no-overrides-field');
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,10 @@ import n8nNodesPlugin from 'eslint-plugin-n8n-nodes-base';
|
|||
import tseslint, { type ConfigArray } from 'typescript-eslint';
|
||||
|
||||
function createConfig(supportCloud = true): ConfigArray {
|
||||
const communityNodesRecommended = supportCloud
|
||||
? n8nCommunityNodesPlugin.configs.recommended
|
||||
: n8nCommunityNodesPlugin.configs.recommendedWithoutN8nCloudSupport;
|
||||
|
||||
return tseslint.config(
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
|
|
@ -14,9 +18,7 @@ function createConfig(supportCloud = true): ConfigArray {
|
|||
extends: [
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
supportCloud
|
||||
? n8nCommunityNodesPlugin.configs.recommended
|
||||
: n8nCommunityNodesPlugin.configs.recommendedWithoutN8nCloudSupport,
|
||||
communityNodesRecommended,
|
||||
importPlugin.configs['flat/recommended'],
|
||||
],
|
||||
rules: {
|
||||
|
|
@ -32,6 +34,13 @@ function createConfig(supportCloud = true): ConfigArray {
|
|||
},
|
||||
{
|
||||
files: ['package.json'],
|
||||
// Apply the community-nodes recommended config here as well so that
|
||||
// rules gating on `package.json` (e.g. no-overrides-field,
|
||||
// valid-peer-dependencies, no-forbidden-lifecycle-scripts) actually
|
||||
// fire. The `**/*.ts` block above scopes its `extends:` to TypeScript
|
||||
// only, which means ESLint never lints package.json under that block —
|
||||
// see CE-1023 for the analogous issue in @n8n/scan-community-package.
|
||||
extends: [communityNodesRecommended],
|
||||
rules: {
|
||||
...n8nNodesPlugin.configs.community.rules,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,12 +10,21 @@
|
|||
"files": [
|
||||
"scanner"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:dev": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"axios": "catalog:",
|
||||
"@n8n/eslint-plugin-community-nodes": "workspace:*",
|
||||
"@typescript-eslint/parser": "^8.35.0",
|
||||
"semver": "^7.5.4",
|
||||
"tmp": "0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,29 +117,47 @@ const downloadAndExtractPackage = async (packageName, version) => {
|
|||
}
|
||||
};
|
||||
|
||||
const analyzePackage = async (packageDir) => {
|
||||
export const analyzePackage = async (packageDir) => {
|
||||
const { n8nCommunityNodesPlugin } = await import('@n8n/eslint-plugin-community-nodes');
|
||||
const tsParser = await import('@typescript-eslint/parser');
|
||||
|
||||
const eslint = new ESLint({
|
||||
cwd: packageDir,
|
||||
allowInlineConfig: false,
|
||||
overrideConfigFile: true,
|
||||
overrideConfig: defineConfig(n8nCommunityNodesPlugin.configs.recommended, {
|
||||
rules: { 'no-console': 'error' },
|
||||
}),
|
||||
overrideConfig: defineConfig(
|
||||
n8nCommunityNodesPlugin.configs.recommended,
|
||||
{
|
||||
rules: { 'no-console': 'error' },
|
||||
},
|
||||
// JSON files (notably `package.json`) are not parseable by ESLint's
|
||||
// default JS parser, so register the TypeScript parser for them. The
|
||||
// community-nodes rules that gate on `package.json` walk a TSESTree
|
||||
// `ObjectExpression` AST, which `@typescript-eslint/parser` produces
|
||||
// when given a top-level JSON object literal.
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
languageOptions: { parser: tsParser.default ?? tsParser },
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
try {
|
||||
const jsFiles = glob.sync('**/*.js', {
|
||||
// Lint both JS and JSON files. JSON inclusion is required because rules
|
||||
// such as `no-overrides-field`, `valid-peer-dependencies`, and
|
||||
// `package-name-convention` only run against `package.json`. Without
|
||||
// it the scanner silently skips every package.json-based rule.
|
||||
const filesToLint = glob.sync(['**/*.js', '**/*.json'], {
|
||||
cwd: packageDir,
|
||||
absolute: true,
|
||||
ignore: ['node_modules/**'],
|
||||
ignore: ['node_modules/**', '**/package-lock.json'],
|
||||
});
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
return { passed: true, message: 'No JavaScript files found to analyze' };
|
||||
if (filesToLint.length === 0) {
|
||||
return { passed: true, message: 'No files found to analyze' };
|
||||
}
|
||||
|
||||
const results = await eslint.lintFiles(jsFiles);
|
||||
const results = await eslint.lintFiles(filesToLint);
|
||||
const violations = results.filter((result) => result.errorCount > 0);
|
||||
|
||||
if (violations.length > 0) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { analyzePackage } from './scanner.mjs';
|
||||
|
||||
/**
|
||||
* Build a temporary package directory on disk so we can hand it to
|
||||
* `analyzePackage` exactly the way the scanner would after extracting a
|
||||
* tarball from npm.
|
||||
*/
|
||||
function makeFixturePackage(files) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scan-fixture-'));
|
||||
for (const [relativePath, contents] of Object.entries(files)) {
|
||||
const fullPath = path.join(dir, relativePath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
fullPath,
|
||||
typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2),
|
||||
);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe('analyzePackage', () => {
|
||||
let fixtureDir;
|
||||
|
||||
afterEach(() => {
|
||||
if (fixtureDir) {
|
||||
fs.rmSync(fixtureDir, { recursive: true, force: true });
|
||||
fixtureDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('lints package.json and flags forbidden `overrides` field (regression for CE-1023)', async () => {
|
||||
fixtureDir = makeFixturePackage({
|
||||
'package.json': {
|
||||
name: 'n8n-nodes-fixture',
|
||||
version: '1.0.0',
|
||||
keywords: ['n8n-community-node-package'],
|
||||
peerDependencies: { 'n8n-workflow': '*' },
|
||||
overrides: { 'change-case': '4.1.2' },
|
||||
},
|
||||
'index.js': "module.exports = {};\n",
|
||||
});
|
||||
|
||||
const result = await analyzePackage(fixtureDir);
|
||||
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.details).toContain('no-overrides-field');
|
||||
});
|
||||
|
||||
it('passes a clean package that does not violate any error-level rules', async () => {
|
||||
fixtureDir = makeFixturePackage({
|
||||
'package.json': {
|
||||
name: 'n8n-nodes-fixture',
|
||||
version: '1.0.0',
|
||||
keywords: ['n8n-community-node-package'],
|
||||
peerDependencies: { 'n8n-workflow': '*' },
|
||||
},
|
||||
'index.js': "module.exports = {};\n",
|
||||
});
|
||||
|
||||
const result = await analyzePackage(fixtureDir);
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it('flags forbidden lifecycle scripts in package.json', async () => {
|
||||
fixtureDir = makeFixturePackage({
|
||||
'package.json': {
|
||||
name: 'n8n-nodes-fixture',
|
||||
version: '1.0.0',
|
||||
keywords: ['n8n-community-node-package'],
|
||||
peerDependencies: { 'n8n-workflow': '*' },
|
||||
scripts: { postinstall: 'node ./malicious.js' },
|
||||
},
|
||||
'index.js': "module.exports = {};\n",
|
||||
});
|
||||
|
||||
const result = await analyzePackage(fixtureDir);
|
||||
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.details).toContain('no-forbidden-lifecycle-scripts');
|
||||
});
|
||||
|
||||
it('returns passed when the package contains no lintable files', async () => {
|
||||
fixtureDir = makeFixturePackage({
|
||||
'README.md': '# empty package\n',
|
||||
});
|
||||
|
||||
const result = await analyzePackage(fixtureDir);
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.message).toBe('No files found to analyze');
|
||||
});
|
||||
});
|
||||
1
packages/@n8n/scan-community-package/vitest.config.ts
Normal file
1
packages/@n8n/scan-community-package/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { vitestConfig as default } from '@n8n/vitest-config/node';
|
||||
|
|
@ -2246,6 +2246,9 @@ importers:
|
|||
'@n8n/eslint-plugin-community-nodes':
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-plugin-community-nodes
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^8.35.0
|
||||
version: 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@6.0.2)
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
|
|
@ -2261,6 +2264,13 @@ importers:
|
|||
tmp:
|
||||
specifier: 0.2.4
|
||||
version: 0.2.4
|
||||
devDependencies:
|
||||
'@n8n/vitest-config':
|
||||
specifier: workspace:*
|
||||
version: link:../vitest-config
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
|
||||
packages/@n8n/stylelint-config:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user