chore: Repair Oxlint migration verification (no-changelog) (#40087)

This commit is contained in:
Matsu 2026-10-02 11:08:39 +00:00 committed by GitHub
parent deaa9b1054
commit aae8016c84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 83 additions and 27 deletions

View File

@ -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"
},

View File

@ -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<string, string>,
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<string[]> {
@ -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 () => {

View File

@ -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<CodeHealthContext> {
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<Violation[]> {
@ -132,8 +135,8 @@ export class EncryptionBoundaryRule extends BaseRule<CodeHealthContext> {
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<CodeHealthContext> {
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<CodeHealthContext> {
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<CodeHealthContext> {
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<CodeHealthContext> {
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.`,
);
}
}

View File

@ -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:

View File

@ -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';

View File

@ -11,7 +11,7 @@
* `--threshold N` (default 10) is the count at which a rule moves into `base`.
* `--layer <name>` 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}`);
}

View File

@ -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"

View File

@ -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"]
},