mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
chore: Enforce extraneous dependencies in Oxlint (no-changelog) (#39281)
This commit is contained in:
@@ -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;
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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',
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+3
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user