From aae8016c84ad0de4a425f99db1ab33717caede64 Mon Sep 17 00:00:00 2001 From: Matsu Date: Fri, 2 Oct 2026 11:08:39 +0000 Subject: [PATCH] chore: Repair Oxlint migration verification (no-changelog) (#40087) --- packages/@n8n/oxlint-config/package.json | 3 +- .../rules/encryption-boundary.rule.test.ts | 43 +++++++++++++++++-- .../src/rules/encryption-boundary.rule.ts | 25 ++++++----- .../lint-parity/OXLINT_MIGRATION_BLOCKERS.md | 2 + scripts/lint-parity/config-blocks.mjs | 5 +-- scripts/lint-parity/majority.mjs | 16 ++++++- scripts/lint-parity/samples.json | 7 --- turbo.json | 9 ++++ 8 files changed, 83 insertions(+), 27 deletions(-) diff --git a/packages/@n8n/oxlint-config/package.json b/packages/@n8n/oxlint-config/package.json index 0d8b33ed50e..6c70bd3ebaa 100644 --- a/packages/@n8n/oxlint-config/package.json +++ b/packages/@n8n/oxlint-config/package.json @@ -33,7 +33,8 @@ "format": "biome format --write .", "format:check": "biome ci .", "lint": "node -e \"process.exit(0)\"", - "test": "node -e \"process.exit(0)\"", + "test": "node ../../../scripts/lint-parity/verify-raw-enum-bridge.mjs", + "test:unit": "node ../../../scripts/lint-parity/verify-raw-enum-bridge.mjs", "typecheck": "tsc --noEmit", "watch": "tsc --watch" }, diff --git a/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.test.ts b/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.test.ts index 47559fd7d76..9580be891cb 100644 --- a/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.test.ts +++ b/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.test.ts @@ -24,6 +24,18 @@ import { baseConfig } from '@n8n/eslint-config/base'; export default defineConfig(baseConfig); `; +const OXLINT_BOUNDARY_CONFIG = `import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ extends: [backendConfig] }); +`; + +const OXLINT_NODES_CONFIG = `import { nodesConfig } from '@n8n/oxlint-config/nodes'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ extends: [nodesConfig] }); +`; + describe('EncryptionBoundaryRule', () => { let rootDir: string; let rule: EncryptionBoundaryRule; @@ -49,12 +61,13 @@ describe('EncryptionBoundaryRule', () => { deps: Record, section = 'dependencies', config: string | null = BOUNDARY_CONFIG, + configFilename = 'eslint.config.mjs', ): void { write( `${dir}/package.json`, JSON.stringify({ name: path.basename(dir), [section]: deps }, null, 2), ); - if (config !== null) write(`${dir}/eslint.config.mjs`, config); + if (config !== null) write(`${dir}/${configFilename}`, config); } async function analyze(): Promise { @@ -76,6 +89,30 @@ describe('EncryptionBoundaryRule', () => { expect(await analyze()).toEqual([]); }); + it('accepts an Oxlint package that extends backendConfig', async () => { + writePackage( + 'packages/a', + { 'n8n-core': 'workspace:*' }, + 'dependencies', + OXLINT_BOUNDARY_CONFIG, + 'oxlint.config.mts', + ); + + expect(await analyze()).toEqual([]); + }); + + it('accepts an Oxlint package on nodesConfig', async () => { + writePackage( + 'packages/a', + { 'n8n-core': 'workspace:*' }, + 'dependencies', + OXLINT_NODES_CONFIG, + 'oxlint.config.mts', + ); + + expect(await analyze()).toEqual([]); + }); + it('flags a package on baseConfig only', async () => { writePackage('packages/a', { 'n8n-core': 'workspace:*' }, 'dependencies', BASE_CONFIG); @@ -93,14 +130,14 @@ describe('EncryptionBoundaryRule', () => { expect(await analyze()).toHaveLength(1); }); - it('flags a package with no ESLint config at all', async () => { + it('flags a package with no supported lint config', async () => { writePackage('packages/a', { 'n8n-core': 'workspace:*' }, 'dependencies', null); const violations = await analyze(); expect(violations).toHaveLength(1); expect(violations[0]).toContain('packages/a/package.json:1'); - expect(violations[0]).toContain('has no ESLint config'); + expect(violations[0]).toContain('has no supported lint config'); }); it('treats @n8n/db and devDependencies as triggers too', async () => { diff --git a/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.ts b/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.ts index 4398aa4a787..b7e427f5256 100644 --- a/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.ts +++ b/packages/quality/policy/code-health/src/rules/encryption-boundary.rule.ts @@ -21,18 +21,21 @@ const DEFAULT_TRIGGER_DEPENDENCIES = ['n8n-core', '@n8n/db']; /** The rules `encryptionBoundaryConfig` enables, without the plugin prefix. */ const DEFAULT_GUARDED_RULES = ['no-encryption-guardrail-disable']; -const BOUNDARY_CONFIG_PATH = 'packages/@n8n/eslint-config/src/configs/encryption-boundary.ts'; +const BOUNDARY_CONFIG_PATHS = + 'packages/@n8n/eslint-config/src/configs/encryption-boundary.ts or packages/@n8n/oxlint-config/src/configs/encryption-boundary.ts'; const CONFIG_FILENAMES = [ 'eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs', 'eslint.config.ts', + 'oxlint.config.mts', + 'oxlint.config.ts', ]; /** `backendConfig` composes the boundary, and `nodesConfig` composes `backendConfig`. */ const BOUNDARY_IMPORT = - /import\s*\{([^}]*)\}\s*from\s+['"]@n8n\/eslint-config\/(?:backend|nodes)['"]/; + /import\s*\{([^}]*)\}\s*from\s+['"]@n8n\/(?:eslint|oxlint)-config\/(?:backend|nodes)['"]/; const BOUNDARY_EXPORTS = /^(backendConfig|nodesConfig)(?:\s+as\s+(\w+))?$/; /** A guarded rule configured to anything weaker than "error" in an ESLint config. */ @@ -86,7 +89,7 @@ export class EncryptionBoundaryRule extends BaseRule { readonly id = 'encryption-boundary'; readonly name = 'Encryption Boundary Coverage'; readonly description = - 'Packages that depend on n8n-core or @n8n/db must compose the encryption-boundary ESLint config, keep its rules at "error", and contain no ESLint directive that silences them.'; + 'Packages that depend on n8n-core or @n8n/db must compose the encryption-boundary lint config, keep its rules at "error", and contain no lint directive that silences them.'; readonly severity = 'error' as const; async analyze(context: CodeHealthContext): Promise { @@ -132,8 +135,8 @@ export class EncryptionBoundaryRule extends BaseRule { packageJsonPath, 1, 1, - `${packageName} depends on ${trigger} but has no ESLint config, so the encryption boundary is not linted there.`, - 'Add an eslint.config.mjs that extends `backendConfig` from @n8n/eslint-config/backend (or `nodesConfig`).', + `${packageName} depends on ${trigger} but has no supported lint config, so the encryption boundary is not linted there.`, + 'Add an eslint.config.mjs or oxlint.config.mts that extends the matching `backendConfig` (or `nodesConfig`).', ), ]; } @@ -147,8 +150,8 @@ export class EncryptionBoundaryRule extends BaseRule { configPath, 1, 1, - `${packageName} depends on ${trigger} but its ESLint config does not compose the encryption boundary.`, - "Extend `backendConfig` from '@n8n/eslint-config/backend' (or `nodesConfig`) and add it to the exported config.", + `${packageName} depends on ${trigger} but its lint config does not compose the encryption boundary.`, + 'Extend `backendConfig` (or `nodesConfig`) from the matching shared lint package and add it to the exported config.', ), ); } @@ -161,8 +164,8 @@ export class EncryptionBoundaryRule extends BaseRule { configPath, index + 1, match.index + 1, - `The ESLint config sets \`n8n-local-rules/${match[1]}\` to ${match[2]}; the encryption guardrails must stay at "error".`, - `Remove the override. Widen the boundary in ${BOUNDARY_CONFIG_PATH} instead.`, + `The lint config sets \`n8n-local-rules/${match[1]}\` to ${match[2]}; the encryption guardrails must stay at "error".`, + `Remove the override. Widen the boundary in ${BOUNDARY_CONFIG_PATHS} instead.`, ), ); }); @@ -220,7 +223,7 @@ export class EncryptionBoundaryRule extends BaseRule { lineNumber, column, `An inline \`${form}\` configuration comment reconfigures the encryption guardrail \`${rule}\`.`, - `Remove the comment. Widen the boundary in ${BOUNDARY_CONFIG_PATH} instead.`, + `Remove the comment. Widen the boundary in ${BOUNDARY_CONFIG_PATHS} instead.`, ); } @@ -251,7 +254,7 @@ export class EncryptionBoundaryRule extends BaseRule { lineNumber, column, `\`${form}\` names the encryption guardrail \`${rule}\`.`, - `Remove the directive. Widen the boundary in ${BOUNDARY_CONFIG_PATH} instead.`, + `Remove the directive. Widen the boundary in ${BOUNDARY_CONFIG_PATHS} instead.`, ); } } diff --git a/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md b/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md index d20c6756215..020e37e7353 100644 --- a/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md +++ b/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md @@ -5,6 +5,8 @@ Oxlint version: 1.78.0 This file is the working backlog for the ESLint to Oxlint migration. It focuses on rules that block an Oxlint-only lint command for many packages. `oxlint-gap.json` is the machine-readable source of truth for shared-layer parity. +Oxlint configs are the executable policy for migrated packages. ESLint policy twins may remain temporarily for parity review, but migration tooling must also support packages that have removed them. + ## Completion criteria A blocker is complete when all of these conditions are true: diff --git a/scripts/lint-parity/config-blocks.mjs b/scripts/lint-parity/config-blocks.mjs index 8832c4b5c6e..5bba2ae2c62 100644 --- a/scripts/lint-parity/config-blocks.mjs +++ b/scripts/lint-parity/config-blocks.mjs @@ -7,9 +7,8 @@ * the shared layer's blocks mixed in, and the question here is always "what does * THIS package decide", which is a property of the file. * - * ponytail: a block assembled at runtime (spread of a computed array, a rule id - * held in a variable) is invisible. `verify.mjs` cross-checks the totals against - * `--print-config`, which would catch a package hiding decisions that way. + * A block assembled at runtime (spread of a computed array, a rule id held in a + * variable) is invisible. Use resolved config snapshots when exact parity matters. */ import { readFileSync } from 'node:fs'; diff --git a/scripts/lint-parity/majority.mjs b/scripts/lint-parity/majority.mjs index 05679397b7c..1ed1786d073 100644 --- a/scripts/lint-parity/majority.mjs +++ b/scripts/lint-parity/majority.mjs @@ -11,7 +11,7 @@ * `--threshold N` (default 10) is the count at which a rule moves into `base`. * `--layer ` restricts the report to one layer. */ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -49,10 +49,17 @@ const get = (id) => { return stats.get(id); }; +const skipped = []; + for (const [pkg, layer] of layerOf) { if (layer === 'exempt') continue; if (onlyLayer && layer !== onlyLayer) continue; - for (const block of readConfigBlocks(join(root, pkg, 'eslint.config.mjs'))) { + const configPath = join(root, pkg, 'eslint.config.mjs'); + if (!existsSync(configPath)) { + skipped.push(pkg); + continue; + } + for (const block of readConfigBlocks(configPath)) { for (const rule of block.rules) { const s = get(rule.id); (rule.severity === 'error' ? s.on : s.down).add(pkg); @@ -102,3 +109,8 @@ if (neverOn.length) { console.log(`\nnever enabled in any package (${neverOn.length}):`); for (const r of neverOn) console.log(` ${r.id} (down ${r.down})`); } + +if (skipped.length > 0) { + console.log(`\nskipped ${skipped.length} package(s) without an ESLint policy config:`); + for (const pkg of skipped) console.log(` ${pkg}`); +} diff --git a/scripts/lint-parity/samples.json b/scripts/lint-parity/samples.json index 47525b4bfa0..e305ce9c35a 100644 --- a/scripts/lint-parity/samples.json +++ b/scripts/lint-parity/samples.json @@ -54,11 +54,6 @@ "src/configs/activity-log.config.ts" ], "packages/@n8n/constants": ["src/api.ts"], - "packages/@n8n/crdt": [ - "src/__tests__/helpers.ts", - "src/awareness/awareness.test.ts", - "src/awareness/index.ts" - ], "packages/@n8n/db": [ "src/connection/__tests__/backoff.test.ts", "src/connection/backoff.ts", @@ -68,12 +63,10 @@ "src/__tests__/memoized.test.ts", "src/auth-handler/auth-handler-metadata.ts" ], - "packages/@n8n/di": ["src/__tests__/circular-depedency.test.ts", "src/di.ts"], "packages/@n8n/engine": [ "src/admittance/__tests__/allow-all-admittance.test.ts", "src/admittance/admittance-rejected.error.ts" ], - "packages/@n8n/errors": ["src/application.error.ts"], "packages/@n8n/eslint-plugin-community-nodes": [ "src/plugin.ts", "src/rules/ai-node-package-json.test.ts" diff --git a/turbo.json b/turbo.json index fbbeb821bd2..a836ffae610 100644 --- a/turbo.json +++ b/turbo.json @@ -131,6 +131,12 @@ "@n8n/mcp-browser#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, + "@n8n/node-cli#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/node-engine-compatibility#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, "@n8n/n8n-benchmark#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, @@ -149,6 +155,9 @@ "@n8n/task-runner#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, + "@n8n/test-impact#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, "@n8n/workflow-sdk#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] },