chore: Enforce extraneous dependencies in Oxlint (no-changelog) (#39281)

This commit is contained in:
Matsu
2026-09-22 12:48:49 +00:00
committed by GitHub
parent dfe014a2ba
commit 1bb166dbaa
10 changed files with 71 additions and 67 deletions
+5
View File
@@ -19,6 +19,10 @@
"./nodes": {
"types": "./dist/configs/nodes.d.ts",
"default": "./dist/configs/nodes.js"
},
"./import-x-alias": {
"types": "./dist/import-x-alias.d.ts",
"default": "./dist/import-x-alias.js"
}
},
"scripts": {
@@ -41,6 +45,7 @@
"dependencies": {
"@n8n/eslint-config": "workspace:*",
"@stylistic/eslint-plugin": "^5.0.0",
"eslint-plugin-import-x": "^4.15.2",
"eslint-plugin-lodash": "^8.0.0",
"eslint-plugin-unused-imports": "^4.1.4",
"oxlint": "catalog:"
@@ -19,6 +19,7 @@ export const baseConfig = defineConfig({
plugins: ['typescript', 'import', 'unicorn', 'oxc'],
jsPlugins: [
'@n8n/eslint-config/plugin',
'@n8n/oxlint-config/import-x-alias',
'@stylistic/eslint-plugin',
'eslint-plugin-unused-imports',
'eslint-plugin-lodash',
@@ -173,6 +174,26 @@ export const baseConfig = defineConfig({
// ----------------------------------
'import/no-cycle': ['error', { ignoreExternal: false, maxDepth: 3 }],
'import/no-duplicates': 'error',
'import-x-alias/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/test/**',
'**/__tests__/**',
'**/*.test.ts',
'**/*.test.utils.ts',
'**/*.spec.ts',
'**/integration-tests/**',
'**/test-utils/**',
'**/*.config.ts',
'**/*.config.js',
'**/scripts/*.ts',
'**/scripts/*.js',
'**/*.stories.ts',
],
optionalDependencies: false,
},
],
// ----------------------------------
// unicorn
@@ -209,6 +230,13 @@ export const baseConfig = defineConfig({
'n8n-local-rules/no-useless-catch-throw': 'error',
},
overrides: [
{
files: ['eslint.config.mjs', 'eslint.*.config.mjs', 'oxlint.config.mts'],
jsPlugins: ['@n8n/oxlint-config/import-x-alias'],
rules: {
'import-x-alias/no-extraneous-dependencies': 'off',
},
},
{
files: ['test/**/*.ts', '**/__tests__/*.ts', '**/*.test.ts'],
// An override that names a jsPlugin rule must re-declare the plugin.
@@ -0,0 +1,19 @@
import importXPlugin from 'eslint-plugin-import-x';
interface OxlintJsPlugin {
meta: {
name: string;
version?: string;
};
rules: Record<string, unknown>;
}
const importXAlias: OxlintJsPlugin = {
...importXPlugin,
meta: {
...importXPlugin.meta,
name: 'import-x-alias',
},
};
export default importXAlias;
-44
View File
@@ -1,44 +0,0 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import { functionalGuardrailsConfig } from '@n8n/eslint-config/functional-guardrails';
import { localRulesPlugin } from '@n8n/eslint-config/plugin';
import cliConfig from './eslint.config.mjs';
const directivePluginConfigs = cliConfig.flatMap(({ plugins }) => {
const externalPlugins = Object.fromEntries(
Object.entries(plugins ?? {}).filter(([name]) => name !== 'n8n-local-rules'),
);
return Object.keys(externalPlugins).length === 0 ? [] : [{ plugins: externalPlugins }];
});
const dependencyRuleConfigs = cliConfig.flatMap(({ files, ignores, settings, rules }) => {
const rule = rules?.['import-x/no-extraneous-dependencies'];
if (!rule) return [];
return [
{
...(files ? { files } : {}),
...(ignores ? { ignores } : {}),
...(settings ? { settings } : {}),
rules: { 'import-x/no-extraneous-dependencies': rule },
},
];
});
export default defineConfig(
globalIgnores([
'dist/**',
'coverage/**',
'eslint.config.mjs',
'eslint.guardrails.config.mjs',
'oxlint.config.mts',
'scripts/**/*.mjs',
'vitest.*.ts',
]),
{
plugins: { 'n8n-local-rules': localRulesPlugin },
},
functionalGuardrailsConfig,
...directivePluginConfigs,
...dependencyRuleConfigs,
);
+1 -2
View File
@@ -44,8 +44,7 @@ export default defineConfig({
rules: {
'n8n-local-rules/no-dynamic-import-template': 'error',
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
// Ratchets: the allowlists below only shrink, so an inline disable is the one way to add a
// violation. The narrow ESLint guardrail config owns no-unsealed-workflow-entity-write.
// Ratchets: the allowlists below only shrink, so an inline disable is the one way to add a violation.
'n8n-local-rules/no-guardrail-disable': [
'error',
{
+2 -4
View File
@@ -17,10 +17,8 @@
"dev:webhook": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"pnpm run watch\" \"nodemon webhook\"",
"format": "biome format --write .",
"format:check": "biome ci .",
"lint": "pnpm run lint:base && pnpm run lint:guardrails",
"lint:base": "oxlint --type-aware . --quiet",
"lint:fix": "oxlint --type-aware . --fix && pnpm run lint:guardrails",
"lint:guardrails": "cross-env NODE_OPTIONS=\"--max-old-space-size=6144\" eslint --config eslint.guardrails.config.mjs . --quiet",
"lint": "oxlint --type-aware . --quiet",
"lint:fix": "oxlint --type-aware . --fix",
"recording:list": "node scripts/agent-integration-recordings.mjs --list",
"recording:export": "node scripts/agent-integration-recordings.mjs",
"start": "node ../../scripts/os-normalize.mjs --dir bin n8n",
+3
View File
@@ -3689,6 +3689,9 @@ importers:
'@stylistic/eslint-plugin':
specifier: ^5.0.0
version: 5.0.0([email protected]([email protected])([email protected]))
eslint-plugin-import-x:
specifier: ^4.15.2
version: 4.15.2(@typescript-eslint/[email protected](@typescript/[email protected])([email protected]([email protected])([email protected]))([email protected]))([email protected]([email protected]))([email protected]([email protected])([email protected]))([email protected])
eslint-plugin-lodash:
specifier: ^8.0.0
version: 8.0.0([email protected]([email protected])([email protected]))
@@ -1,6 +1,6 @@
# Oxlint migration blockers
Last reviewed: 2026-09-18
Last reviewed: 2026-09-22
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.
@@ -21,7 +21,6 @@ These rules come from the shared base or backend layers. Resolve these first bec
| Priority | Rule | Blocker | Suggested next action |
| --- | --- | --- | --- |
| P0 | `import-x/no-extraneous-dependencies` | Oxlint reserves the `import-x` plugin name. The JS plugin bridge has not been validated with an alias. | Test `eslint-plugin-import-x` under an alias. If that is unstable, implement a native package-manifest check. |
| P1 | `no-restricted-syntax` | Oxlint has no AST-selector rule. The base layer uses it for the raw-enum restriction. | Add a purpose-built syntax rule for each active selector. Start with the raw-enum rule. |
| P1 | `typescript/consistent-type-imports` | Oxlint does not honor the `emitDecoratorMetadata` exception. Its fixer can remove imports that DI needs at runtime. | Wait for upstream support or add a tested shim that skips decorated files. Do not enable the current fixer. |
| P1 | `typescript/prefer-optional-chain` | The native nursery rule changes guarded undeclared globals incorrectly. | Re-test after the rule leaves nursery or after the upstream fixer issue is resolved. |
@@ -150,13 +149,12 @@ The main exceptions are:
## Work order
1. Resolve or isolate `import-x/no-extraneous-dependencies`.
2. Replace active `no-restricted-syntax` selectors with focused rules.
3. Resolve decorator-safe `consistent-type-imports` behavior.
4. Audit and reduce `naming-convention` package overrides.
5. Re-test the two nursery rules.
6. Run a JS-plugin bridge spike for `n8n-nodes-base`.
7. Keep a template-only ESLint pass for Vue until native template support exists.
1. Replace active `no-restricted-syntax` selectors with focused rules.
2. Resolve decorator-safe `consistent-type-imports` behavior.
3. Audit and reduce `naming-convention` package overrides.
4. Re-test the two nursery rules.
5. Run a JS-plugin bridge spike for `n8n-nodes-base`.
6. Keep a template-only ESLint pass for Vue until native template support exists.
## Validation commands
-7
View File
@@ -21,13 +21,6 @@
"impact": "Near zero.",
"replacement": "No action unless non-module source becomes supported."
},
"import-x/no-extraneous-dependencies": {
"layer": "base",
"native": null,
"reason": "`import-x` is a reserved plugin name in oxlint, so `eslint-plugin-import-x` cannot load as a jsPlugin under its own name (`Plugin name 'import-x' is reserved`). An alias would permanently change the rule ids, and these rules are the slowest in ESLint and unvalidated through the alpha bridge.",
"impact": "A package can import a dependency it does not declare. Left out of the spike deliberately.",
"replacement": "Validate eslint-plugin-import-x with an alias, or replace the dependency check with a native package-manifest rule."
},
"typescript/prefer-optional-chain": {
"layer": "base",
"native": "typescript/prefer-optional-chain",
+6 -1
View File
@@ -61,6 +61,7 @@ const SCOPE_BY_NAMESPACE = {
/** Namespaces oxlint can only reach through a jsPlugin, so ids stay as ESLint wrote them. */
const JS_PLUGIN_NAMESPACES = new Set([
'@stylistic',
'import-x-alias',
'lodash',
'unused-imports',
'n8n-local-rules',
@@ -77,13 +78,17 @@ function oxlintRuleIds() {
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
});
const start = out.stdout.indexOf('[');
const arrayLine = out.stdout.lastIndexOf('\n[');
const start = arrayLine === -1 ? out.stdout.indexOf('[') : arrayLine + 1;
if (start === -1) throw new Error(`oxlint --rules failed: ${(out.stderr || out.stdout).trim()}`);
return new Set(JSON.parse(out.stdout.slice(start)).map((r) => `${r.scope}/${r.value}`));
}
/** ESLint id -> the oxlint id we would write in a config, or null if oxlint has no such rule. */
function toOxlintId(eslintId, native) {
if (eslintId === 'import-x/no-extraneous-dependencies') {
return 'import-x-alias/no-extraneous-dependencies';
}
const [ns, name] = splitId(eslintId);
if (JS_PLUGIN_NAMESPACES.has(ns)) return eslintId;
if (ns === '') return native.has(`eslint/${name}`) ? name : null;