mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
chore: Remove dead ESLint config and add lint parity tooling (no-changelog) (#38343)
Co-authored-by: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -15,6 +15,7 @@ Flag a diff that **adds** entries to any of these, and ask for the fix instead:
|
||||
| `.boundaries-baseline.json` | `turbo boundaries` issue count |
|
||||
| `packages/testing/playwright/.janitor-baseline.json` | Playwright janitor findings |
|
||||
| `packages/cli/eslint.config.mjs` | the `misplaced-n8n-typeorm-import` and public-API allowlists, each captioned "NEVER add to this list" |
|
||||
| `.code-health-baseline.json` (`lint-config-layering`) | package-wide rule downgrades left in package ESLint configs |
|
||||
|
||||
Removals are the healthy direction and need no comment.
|
||||
|
||||
@@ -33,3 +34,11 @@ Flag a lint rule moved from `'error'` to `'warn'` or `'off'`, and a new
|
||||
`eslint-disable` for one of the guarded rules. Most packages run
|
||||
`eslint . --quiet`, so a warning never fails CI — downgrading to `'warn'` is
|
||||
functionally deleting the rule while appearing to keep it.
|
||||
|
||||
In a package `eslint.config.mjs` the `lint-config-layering` code-health rule
|
||||
catches this for you: a package-wide downgrade fails Static Analysis unless it
|
||||
is already in the baseline. What it cannot judge is a downgrade dressed as a
|
||||
scoped block. Check that a new `files` glob names the paths that actually need
|
||||
the exception, and that a rule retired in `@n8n/eslint-config/src/configs/base.ts`
|
||||
comes with the count of packages that had already stopped enforcing it
|
||||
(`node scripts/lint-parity/majority.mjs`).
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"issues": 164
|
||||
"issues": 163
|
||||
}
|
||||
|
||||
@@ -1310,6 +1310,904 @@
|
||||
"message": "A bare `eslint-disable` directive silences every lint rule in its range, including the encryption guardrails.",
|
||||
"hash": "7d9779f96d99"
|
||||
}
|
||||
],
|
||||
"packages/core/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 14,
|
||||
"message": "packages/core turns 'no-prototype-builtins' down to 'warn' for the whole package.",
|
||||
"hash": "f6d9c62ac3fa"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 15,
|
||||
"message": "packages/core turns 'no-ex-assign' down to 'warn' for the whole package.",
|
||||
"hash": "1bb332c34de8"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 16,
|
||||
"message": "packages/core turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "8c7b005323e5"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 17,
|
||||
"message": "packages/core turns '@typescript-eslint/no-require-imports' down to 'warn' for the whole package.",
|
||||
"hash": "efdd5147a463"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 18,
|
||||
"message": "packages/core turns '@typescript-eslint/no-base-to-string' down to 'warn' for the whole package.",
|
||||
"hash": "5328912625c6"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 19,
|
||||
"message": "packages/core turns '@typescript-eslint/prefer-optional-chain' down to 'warn' for the whole package.",
|
||||
"hash": "eddccedaafb7"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 20,
|
||||
"message": "packages/core turns '@typescript-eslint/no-array-delete' down to 'warn' for the whole package.",
|
||||
"hash": "c05a6f2ae2f1"
|
||||
}
|
||||
],
|
||||
"packages/cli/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 90,
|
||||
"message": "packages/cli turns '@typescript-eslint/ban-ts-comment' down to 'off' for the whole package.",
|
||||
"hash": "7e7e5e67871c"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 91,
|
||||
"message": "packages/cli turns 'import-x/no-cycle' down to 'warn' for the whole package.",
|
||||
"hash": "e53210bcd5e5"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 92,
|
||||
"message": "packages/cli turns 'import-x/extensions' down to 'off' for the whole package.",
|
||||
"hash": "e9ac61307734"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 93,
|
||||
"message": "packages/cli turns 'no-ex-assign' down to 'warn' for the whole package.",
|
||||
"hash": "9555ecb54f1c"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 94,
|
||||
"message": "packages/cli turns 'no-case-declarations' down to 'warn' for the whole package.",
|
||||
"hash": "ac66463536bc"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 95,
|
||||
"message": "packages/cli turns 'no-fallthrough' down to 'warn' for the whole package.",
|
||||
"hash": "d27fa33c682d"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 96,
|
||||
"message": "packages/cli turns 'no-unsafe-optional-chaining' down to 'warn' for the whole package.",
|
||||
"hash": "ea7a4933c714"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 97,
|
||||
"message": "packages/cli turns 'no-async-promise-executor' down to 'warn' for the whole package.",
|
||||
"hash": "af41798867ce"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 98,
|
||||
"message": "packages/cli turns 'complexity' down to 'off' for the whole package.",
|
||||
"hash": "55ccec6cbe85"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 99,
|
||||
"message": "packages/cli turns '@typescript-eslint/prefer-promise-reject-errors' down to 'warn' for the whole package.",
|
||||
"hash": "27aa46ca0c89"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 100,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-explicit-any' down to 'warn' for the whole package.",
|
||||
"hash": "ec58ba458ff5"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 101,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-base-to-string' down to 'warn' for the whole package.",
|
||||
"hash": "ada384a6c58c"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 102,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-redundant-type-constituents' down to 'warn' for the whole package.",
|
||||
"hash": "fb40bae42683"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 103,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-restricted-types' down to 'warn' for the whole package.",
|
||||
"hash": "ee8be51f48f3"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 104,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-unsafe-enum-comparison' down to 'warn' for the whole package.",
|
||||
"hash": "e1d09e3cb444"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 105,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-unsafe-declaration-merging' down to 'warn' for the whole package.",
|
||||
"hash": "abf613bdcbec"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 106,
|
||||
"message": "packages/cli turns '@typescript-eslint/only-throw-error' down to 'warn' for the whole package.",
|
||||
"hash": "f6b9cb56f2da"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 107,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-require-imports' down to 'warn' for the whole package.",
|
||||
"hash": "ce5c4ddb8b21"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 108,
|
||||
"message": "packages/cli turns '@typescript-eslint/array-type' down to 'warn' for the whole package.",
|
||||
"hash": "06eb8c68c0e3"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 109,
|
||||
"message": "packages/cli turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "581e9d0a38b0"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 110,
|
||||
"message": "packages/cli turns '@typescript-eslint/prefer-optional-chain' down to 'warn' for the whole package.",
|
||||
"hash": "60a340796f69"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 111,
|
||||
"message": "packages/cli turns '@typescript-eslint/no-duplicate-type-constituents' down to 'warn' for the whole package.",
|
||||
"hash": "499e9bcc262e"
|
||||
}
|
||||
],
|
||||
"packages/node-dev/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 7,
|
||||
"message": "packages/node-dev turns 'unicorn/filename-case' down to 'warn' for the whole package.",
|
||||
"hash": "55a9d5b897d9"
|
||||
}
|
||||
],
|
||||
"packages/nodes-base/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 27,
|
||||
"message": "packages/nodes-base turns 'no-ex-assign' down to 'warn' for the whole package.",
|
||||
"hash": "405b1823ac6e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 28,
|
||||
"message": "packages/nodes-base turns 'no-control-regex' down to 'warn' for the whole package.",
|
||||
"hash": "be29a0fb44a3"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 29,
|
||||
"message": "packages/nodes-base turns 'no-constant-condition' down to 'warn' for the whole package.",
|
||||
"hash": "f7df4e39eb2e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 30,
|
||||
"message": "packages/nodes-base turns 'no-dupe-else-if' down to 'warn' for the whole package.",
|
||||
"hash": "f5be7d5a7f26"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 31,
|
||||
"message": "packages/nodes-base turns 'no-fallthrough' down to 'warn' for the whole package.",
|
||||
"hash": "c899a707272e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 33,
|
||||
"message": "packages/nodes-base turns 'import-x/export' down to 'warn' for the whole package.",
|
||||
"hash": "9ed273bfcc65"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 34,
|
||||
"message": "packages/nodes-base turns 'import-x/no-extraneous-dependencies' down to 'warn' for the whole package.",
|
||||
"hash": "5eb27aed13a1"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 38,
|
||||
"message": "packages/nodes-base turns '@typescript-eslint/ban-ts-comment' down to 'off' for the whole package.",
|
||||
"hash": "496821aa3902"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 40,
|
||||
"message": "packages/nodes-base turns '@typescript-eslint/prefer-promise-reject-errors' down to 'warn' for the whole package.",
|
||||
"hash": "2d61f9d00a79"
|
||||
}
|
||||
],
|
||||
"packages/workflow/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 12,
|
||||
"message": "packages/workflow turns 'id-denylist' down to 'warn' for the whole package.",
|
||||
"hash": "03927870bdf1"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 13,
|
||||
"message": "packages/workflow turns 'no-fallthrough' down to 'warn' for the whole package.",
|
||||
"hash": "cd442ee0d22b"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 14,
|
||||
"message": "packages/workflow turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "b0c2a209531b"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 15,
|
||||
"message": "packages/workflow turns 'no-extra-boolean-cast' down to 'warn' for the whole package.",
|
||||
"hash": "f1b59e9a9170"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 16,
|
||||
"message": "packages/workflow turns 'no-case-declarations' down to 'warn' for the whole package.",
|
||||
"hash": "e0313976c4b5"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 17,
|
||||
"message": "packages/workflow turns 'no-prototype-builtins' down to 'warn' for the whole package.",
|
||||
"hash": "974eaf5efe54"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 18,
|
||||
"message": "packages/workflow turns '@typescript-eslint/no-base-to-string' down to 'warn' for the whole package.",
|
||||
"hash": "d3d0cc6967be"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 19,
|
||||
"message": "packages/workflow turns '@typescript-eslint/no-redundant-type-constituents' down to 'warn' for the whole package.",
|
||||
"hash": "39ee215cccec"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 20,
|
||||
"message": "packages/workflow turns '@typescript-eslint/prefer-optional-chain' down to 'warn' for the whole package.",
|
||||
"hash": "155e0b154873"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 22,
|
||||
"message": "packages/workflow turns '@typescript-eslint/no-duplicate-type-constituents' down to 'warn' for the whole package.",
|
||||
"hash": "b3f813b386ed"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/ai-node-sdk/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 6,
|
||||
"message": "packages/@n8n/ai-node-sdk turns '@typescript-eslint/no-explicit-any' down to 'warn' for the whole package.",
|
||||
"hash": "8e1ddde4bb6d"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/ai-utilities/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 9,
|
||||
"message": "packages/@n8n/ai-utilities turns '@typescript-eslint/no-explicit-any' down to 'warn' for the whole package.",
|
||||
"hash": "d5a8d7cd9636"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 10,
|
||||
"message": "packages/@n8n/ai-utilities turns 'no-case-declarations' down to 'warn' for the whole package.",
|
||||
"hash": "b95bfb2a3056"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 17,
|
||||
"message": "packages/@n8n/ai-utilities turns 'unicorn/filename-case' down to 'off' for the whole package.",
|
||||
"hash": "de46ec6eb650"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/backend-common/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 8,
|
||||
"message": "packages/@n8n/backend-common turns '@typescript-eslint/no-wrapper-object-types' down to 'warn' for the whole package.",
|
||||
"hash": "c1e3aa8a2e1c"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/codemirror-lang/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 6,
|
||||
"message": "packages/@n8n/codemirror-lang turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "98b3e5d89db4"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/codemirror-lang-sql/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 6,
|
||||
"message": "packages/@n8n/codemirror-lang-sql turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "b03974f4c1da"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 7,
|
||||
"message": "packages/@n8n/codemirror-lang-sql turns '@typescript-eslint/no-unsafe-enum-comparison' down to 'off' for the whole package.",
|
||||
"hash": "3ee104dc1cd2"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/db/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 25,
|
||||
"message": "packages/@n8n/db turns '@typescript-eslint/no-base-to-string' down to 'warn' for the whole package.",
|
||||
"hash": "21a54ac84b21"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 26,
|
||||
"message": "packages/@n8n/db turns '@typescript-eslint/no-restricted-types' down to 'warn' for the whole package.",
|
||||
"hash": "1b7a8eee4264"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 27,
|
||||
"message": "packages/@n8n/db turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "aa31a2b9878a"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/decorators/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 8,
|
||||
"message": "packages/@n8n/decorators turns '@typescript-eslint/no-base-to-string' down to 'warn' for the whole package.",
|
||||
"hash": "b4aab632b998"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 9,
|
||||
"message": "packages/@n8n/decorators turns 'import-x/export' down to 'warn' for the whole package.",
|
||||
"hash": "efaf9102fe79"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/engine/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 11,
|
||||
"message": "packages/@n8n/engine turns 'unicorn/filename-case' down to 'off' for the whole package.",
|
||||
"hash": "2455a576216c"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/eslint-plugin-community-nodes/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 18,
|
||||
"message": "packages/@n8n/eslint-plugin-community-nodes turns 'eslint-plugin/require-meta-docs-url' down to 'off' for the whole package.",
|
||||
"hash": "160308f43107"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 20,
|
||||
"message": "packages/@n8n/eslint-plugin-community-nodes turns 'eslint-plugin/require-meta-default-options' down to 'off' for the whole package.",
|
||||
"hash": "fb3cdbc0b797"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 22,
|
||||
"message": "packages/@n8n/eslint-plugin-community-nodes turns '@typescript-eslint/naming-convention' down to 'off' for the whole package.",
|
||||
"hash": "b9b7b80295d3"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 24,
|
||||
"message": "packages/@n8n/eslint-plugin-community-nodes turns 'import-x/no-default-export' down to 'off' for the whole package.",
|
||||
"hash": "3f7dfc88d2cc"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/extension-sdk/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 7,
|
||||
"message": "packages/@n8n/extension-sdk turns '@typescript-eslint/no-unnecessary-boolean-literal-compare' down to 'warn' for the whole package.",
|
||||
"hash": "1358f2270558"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 8,
|
||||
"message": "packages/@n8n/extension-sdk turns '@typescript-eslint/no-floating-promises' down to 'warn' for the whole package.",
|
||||
"hash": "a01fcd4e7fd1"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/imap/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 9,
|
||||
"message": "packages/@n8n/imap turns '@typescript-eslint/no-unnecessary-boolean-literal-compare' down to 'warn' for the whole package.",
|
||||
"hash": "c01451c28a03"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 10,
|
||||
"message": "packages/@n8n/imap turns '@typescript-eslint/no-floating-promises' down to 'warn' for the whole package.",
|
||||
"hash": "9466b50c2cbc"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/instance-ai/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 100,
|
||||
"message": "packages/@n8n/instance-ai turns 'unicorn/filename-case' down to 'off' for the whole package.",
|
||||
"hash": "b76f6179d75b"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/json-schema-to-zod/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 8,
|
||||
"message": "packages/@n8n/json-schema-to-zod turns 'import-x/no-cycle' down to 'off' for the whole package.",
|
||||
"hash": "bd1fd9f55f2e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 12,
|
||||
"message": "packages/@n8n/json-schema-to-zod turns 'no-constant-condition' down to 'warn' for the whole package.",
|
||||
"hash": "e7995ba65bba"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/node-cli/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 21,
|
||||
"message": "packages/@n8n/node-cli turns 'unicorn/filename-case' down to 'off' for the whole package.",
|
||||
"hash": "450dc7735f37"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/task-runner/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 10,
|
||||
"message": "packages/@n8n/task-runner turns '@typescript-eslint/no-require-imports' down to 'warn' for the whole package.",
|
||||
"hash": "9eab9fc92af8"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/tournament/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 8,
|
||||
"message": "packages/@n8n/tournament turns '@typescript-eslint/no-restricted-types' down to 'off' for the whole package.",
|
||||
"hash": "a7bd4d72aeaa"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 9,
|
||||
"message": "packages/@n8n/tournament turns '@typescript-eslint/no-implied-eval' down to 'off' for the whole package.",
|
||||
"hash": "38c062765f50"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 10,
|
||||
"message": "packages/@n8n/tournament turns '@typescript-eslint/no-explicit-any' down to 'off' for the whole package.",
|
||||
"hash": "228a8d26e7bf"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 17,
|
||||
"message": "packages/@n8n/tournament turns 'unicorn/filename-case' down to 'off' for the whole package.",
|
||||
"hash": "a8dd0ff69014"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/utils/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 7,
|
||||
"message": "packages/@n8n/utils turns 'no-prototype-builtins' down to 'warn' for the whole package.",
|
||||
"hash": "cc7739fda55b"
|
||||
}
|
||||
],
|
||||
"packages/frontend/editor-ui/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 55,
|
||||
"message": "packages/frontend/editor-ui turns 'no-restricted-syntax' down to 'off' for the whole package.",
|
||||
"hash": "991d49a8245c"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 234,
|
||||
"message": "packages/frontend/editor-ui turns 'n8n-local-rules/no-internal-package-import' down to 'warn' for the whole package.",
|
||||
"hash": "4b3a85b06980"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 235,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/ban-ts-comment' down to 'off' for the whole package.",
|
||||
"hash": "e8c5a011088f"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 236,
|
||||
"message": "packages/frontend/editor-ui turns 'id-denylist' down to 'warn' for the whole package.",
|
||||
"hash": "bd2977006e41"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 237,
|
||||
"message": "packages/frontend/editor-ui turns 'no-case-declarations' down to 'warn' for the whole package.",
|
||||
"hash": "e305144fdb0f"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 238,
|
||||
"message": "packages/frontend/editor-ui turns 'no-useless-escape' down to 'warn' for the whole package.",
|
||||
"hash": "9280bef13c13"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 239,
|
||||
"message": "packages/frontend/editor-ui turns 'no-prototype-builtins' down to 'warn' for the whole package.",
|
||||
"hash": "352fae23e0d1"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 240,
|
||||
"message": "packages/frontend/editor-ui turns 'no-fallthrough' down to 'warn' for the whole package.",
|
||||
"hash": "db7c8593f721"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 241,
|
||||
"message": "packages/frontend/editor-ui turns 'no-extra-boolean-cast' down to 'warn' for the whole package.",
|
||||
"hash": "0cbbb7983686"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 242,
|
||||
"message": "packages/frontend/editor-ui turns 'no-sparse-arrays' down to 'warn' for the whole package.",
|
||||
"hash": "750f119df597"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 243,
|
||||
"message": "packages/frontend/editor-ui turns 'no-control-regex' down to 'warn' for the whole package.",
|
||||
"hash": "5f096c4e5a30"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 244,
|
||||
"message": "packages/frontend/editor-ui turns 'import-x/extensions' down to 'warn' for the whole package.",
|
||||
"hash": "301322f12ba8"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 245,
|
||||
"message": "packages/frontend/editor-ui turns 'import-x/no-cycle' down to 'warn' for the whole package.",
|
||||
"hash": "3f9ee03a211f"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 246,
|
||||
"message": "packages/frontend/editor-ui turns 'import-x/no-duplicates' down to 'warn' for the whole package.",
|
||||
"hash": "7ff9cd71e584"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 247,
|
||||
"message": "packages/frontend/editor-ui turns 'no-unsafe-optional-chaining' down to 'warn' for the whole package.",
|
||||
"hash": "29c386f9afa1"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 248,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-restricted-types' down to 'warn' for the whole package.",
|
||||
"hash": "674bc7c925bb"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 249,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/dot-notation' down to 'warn' for the whole package.",
|
||||
"hash": "b01af73ed4af"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 250,
|
||||
"message": "packages/frontend/editor-ui turns '@stylistic/lines-between-class-members' down to 'warn' for the whole package.",
|
||||
"hash": "abd5d6c83376"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 251,
|
||||
"message": "packages/frontend/editor-ui turns '@stylistic/member-delimiter-style' down to 'warn' for the whole package.",
|
||||
"hash": "c48bcd875f1a"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 252,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-empty-interface' down to 'warn' for the whole package.",
|
||||
"hash": "271c5309b542"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 253,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-for-in-array' down to 'warn' for the whole package.",
|
||||
"hash": "249d350f8066"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 254,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-loop-func' down to 'warn' for the whole package.",
|
||||
"hash": "fa39d584681d"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 255,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-non-null-assertion' down to 'warn' for the whole package.",
|
||||
"hash": "875c5c8d162e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 256,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-shadow' down to 'warn' for the whole package.",
|
||||
"hash": "b4bfbb79e513"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 257,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-this-alias' down to 'warn' for the whole package.",
|
||||
"hash": "cd21cdd292fa"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 258,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-unnecessary-boolean-literal-compare' down to 'warn' for the whole package.",
|
||||
"hash": "59d1ce2379ac"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 259,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-unnecessary-type-assertion' down to 'warn' for the whole package.",
|
||||
"hash": "2eb8b8a9ba23"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 260,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-unused-expressions' down to 'warn' for the whole package.",
|
||||
"hash": "27661db19f4f"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 261,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-var-requires' down to 'warn' for the whole package.",
|
||||
"hash": "32b4b7a30c8e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 262,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/prefer-optional-chain' down to 'warn' for the whole package.",
|
||||
"hash": "8379fe8350b5"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 263,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/restrict-plus-operands' down to 'warn' for the whole package.",
|
||||
"hash": "f41881f25dad"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 264,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-redundant-type-constituents' down to 'warn' for the whole package.",
|
||||
"hash": "edfe82c98ea3"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 265,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-unsafe-enum-comparison' down to 'warn' for the whole package.",
|
||||
"hash": "577beb091f06"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 266,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/prefer-promise-reject-errors' down to 'warn' for the whole package.",
|
||||
"hash": "cb1d57707d43"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 267,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/no-base-to-string' down to 'warn' for the whole package.",
|
||||
"hash": "96f813e59fa6"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 268,
|
||||
"message": "packages/frontend/editor-ui turns 'vue/attribute-hyphenation' down to 'warn' for the whole package.",
|
||||
"hash": "33e956db8dec"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 269,
|
||||
"message": "packages/frontend/editor-ui turns '@typescript-eslint/restrict-template-expressions' down to 'warn' for the whole package.",
|
||||
"hash": "affee27335fc"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/workflow-sdk/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 71,
|
||||
"message": "packages/@n8n/workflow-sdk turns 'n8n-local-rules/no-interpolation-in-regular-string' down to 'off' for the whole package.",
|
||||
"hash": "e5999d7c77a9"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 73,
|
||||
"message": "packages/@n8n/workflow-sdk turns 'id-denylist' down to 'off' for the whole package.",
|
||||
"hash": "0f94a71d5ae8"
|
||||
}
|
||||
],
|
||||
"packages/testing/playwright/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 21,
|
||||
"message": "packages/testing/playwright turns '@typescript-eslint/no-unused-expressions' down to 'off' for the whole package.",
|
||||
"hash": "e2f93b5b591a"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 22,
|
||||
"message": "packages/testing/playwright turns '@typescript-eslint/no-use-before-define' down to 'off' for the whole package.",
|
||||
"hash": "75d3ec1677ba"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 23,
|
||||
"message": "packages/testing/playwright turns '@typescript-eslint/promise-function-async' down to 'off' for the whole package.",
|
||||
"hash": "35dbe9e44ada"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 24,
|
||||
"message": "packages/testing/playwright turns 'playwright/expect-expect' down to 'warn' for the whole package.",
|
||||
"hash": "e8b5f4b0f0dd"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 25,
|
||||
"message": "packages/testing/playwright turns 'playwright/max-nested-describe' down to 'warn' for the whole package.",
|
||||
"hash": "77d82ee8a802"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 27,
|
||||
"message": "packages/testing/playwright turns 'playwright/no-skipped-test' down to 'warn' for the whole package.",
|
||||
"hash": "14eb9eecd1b4"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 78,
|
||||
"message": "packages/testing/playwright turns 'unicorn/filename-case' down to 'off' for the whole package.",
|
||||
"hash": "de86b9645930"
|
||||
}
|
||||
],
|
||||
"packages/frontend/@n8n/design-system/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 12,
|
||||
"message": "packages/frontend/@n8n/design-system turns 'no-prototype-builtins' down to 'warn' for the whole package.",
|
||||
"hash": "f7e604999631"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 13,
|
||||
"message": "packages/frontend/@n8n/design-system turns '@typescript-eslint/prefer-optional-chain' down to 'warn' for the whole package.",
|
||||
"hash": "e6d8e187bb0b"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 14,
|
||||
"message": "packages/frontend/@n8n/design-system turns '@typescript-eslint/restrict-template-expressions' down to 'warn' for the whole package.",
|
||||
"hash": "86a0b84b8415"
|
||||
}
|
||||
],
|
||||
"packages/frontend/@n8n/i18n/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 6,
|
||||
"message": "packages/frontend/@n8n/i18n turns '@typescript-eslint/no-unnecessary-type-assertion' down to 'warn' for the whole package.",
|
||||
"hash": "5836fd4581b2"
|
||||
}
|
||||
],
|
||||
"packages/frontend/@n8n/stores/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 6,
|
||||
"message": "packages/frontend/@n8n/stores turns '@typescript-eslint/no-unnecessary-type-assertion' down to 'warn' for the whole package.",
|
||||
"hash": "63e2dd35dc47"
|
||||
}
|
||||
],
|
||||
"packages/modules/insights/frontend/eslint.config.mjs": [
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 24,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/naming-convention' down to 'warn' for the whole package.",
|
||||
"hash": "3da608aa064b"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 25,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/no-unsafe-argument' down to 'warn' for the whole package.",
|
||||
"hash": "e9ae1f320e01"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 26,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/no-unsafe-assignment' down to 'warn' for the whole package.",
|
||||
"hash": "a488f2f44a8e"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 27,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/no-unsafe-call' down to 'warn' for the whole package.",
|
||||
"hash": "58fd1d708522"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 28,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/no-unsafe-member-access' down to 'warn' for the whole package.",
|
||||
"hash": "7de9b4bde334"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 29,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/no-unsafe-return' down to 'warn' for the whole package.",
|
||||
"hash": "14aa32c2f578"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 30,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/await-thenable' down to 'warn' for the whole package.",
|
||||
"hash": "f6b34bf81039"
|
||||
},
|
||||
{
|
||||
"rule": "lint-config-layering",
|
||||
"line": 31,
|
||||
"message": "packages/modules/insights/frontend turns '@typescript-eslint/require-await' down to 'warn' for the whole package.",
|
||||
"hash": "b857963c49d1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,14 +265,39 @@ a new import (or an inline `eslint-disable` of the rule) fails CI.
|
||||
- Pushing `.manager` / `createQueryBuilder` into business logic to avoid an
|
||||
operator import — trades a visible leak for an invisible one.
|
||||
|
||||
### ESLint configuration layers
|
||||
|
||||
Rule policy lives in four shared configs in `@n8n/eslint-config`, and a package
|
||||
config picks exactly one:
|
||||
|
||||
| layer | subpath | for |
|
||||
|---|---|---|
|
||||
| `baseConfig` | `@n8n/eslint-config/base` | runtime-agnostic libraries |
|
||||
| `backendConfig` | `@n8n/eslint-config/backend` | anything that runs on Node; adds the network and encryption boundaries |
|
||||
| `frontendConfig` | `@n8n/eslint-config/frontend` | Vue packages |
|
||||
| `nodesConfig` | `@n8n/eslint-config/nodes` | `n8n-nodes-base` and `@n8n/nodes-langchain`; adds the node and credential file rules |
|
||||
|
||||
A package config may add `ignores`, an additive plugin config, a block that
|
||||
raises rules to `error`, and blocks scoped to `files`. It must not turn a rule
|
||||
down for the whole package: every lint script runs with `--quiet`, so a `warn`
|
||||
enforces nothing and reads as if it did. The code-health rule
|
||||
`lint-config-layering` enforces this, with existing debt in
|
||||
`.code-health-baseline.json`, which only shrinks.
|
||||
|
||||
To stop enforcing a rule everywhere, retire it in `base.ts` with the count
|
||||
behind the decision. To enforce one again in a package that is ready, set it to
|
||||
`error` there. `node scripts/lint-parity/majority.mjs` prints how many packages
|
||||
downgrade each rule, and `scripts/lint-parity/snapshot.mjs` plus `diff.mjs`
|
||||
prove a config change only altered what you meant it to.
|
||||
|
||||
### Encryption boundary
|
||||
|
||||
New code encrypts and decrypts only through `cipher.encryptV2()` /
|
||||
`cipher.decryptV2()` — the key-manager module decides which key is used and in
|
||||
which output format. Enforced in CI by the rules in
|
||||
`packages/@n8n/eslint-config/src/configs/encryption-boundary.ts` (part of
|
||||
`nodeConfig`; baseConfig packages that depend on `n8n-core` or `@n8n/db`
|
||||
compose it directly):
|
||||
`backendConfig`, and so of `nodesConfig`; every package that runs on Node
|
||||
extends one of those layers):
|
||||
|
||||
- The deprecated `Cipher.encrypt` / `Cipher.decrypt` are banned outside tests.
|
||||
- The raw AES classes and `encryptWithKey` / `decryptWithKey` stay inside
|
||||
@@ -283,8 +308,9 @@ compose it directly):
|
||||
- Inline disables that name these rules, and bare line-form disables, are
|
||||
themselves lint errors. The code-health rule `encryption-boundary` (CI
|
||||
"Static Analysis") is the enforcement layer: it checks that every package
|
||||
that depends on `n8n-core` or `@n8n/db` composes the boundary config at
|
||||
`error` severity, and rejects every directive form that would silence the
|
||||
that depends on `n8n-core` or `@n8n/db` extends `backendConfig` (or
|
||||
`nodesConfig`) at `error` severity, and rejects every directive form that
|
||||
would silence the
|
||||
rules in non-test code (`eslint-disable*` and inline `eslint` configuration
|
||||
comments). Widening the boundary happens in `encryption-boundary.ts` only;
|
||||
that file and the rule files require security (IAM) approval via OWNERS.
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
const AI_SDK_LAZY_IMPORT_MESSAGE =
|
||||
"Import runtime values from 'ai' through the lazy loader in src/runtime/lazy-ai.ts or a dynamic import at the call site, so @n8n/agents stays light at boot.";
|
||||
|
||||
export default defineConfig(
|
||||
{ ignores: ['examples/**', 'vitest.integration.config.*', 'vitest.integration.setup.ts', 'src/__tests__/fixtures/**'] },
|
||||
nodeConfig,
|
||||
{
|
||||
ignores: [
|
||||
'examples/**',
|
||||
'vitest.integration.config.*',
|
||||
'vitest.integration.setup.ts',
|
||||
'src/__tests__/fixtures/**',
|
||||
],
|
||||
},
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(nodeConfig, globalIgnores(['dist/**']), {
|
||||
export default defineConfig(backendConfig, globalIgnores(['dist/**']), {
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(nodeConfig, globalIgnores(['scripts/**']), {
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
export default defineConfig(
|
||||
backendConfig,
|
||||
globalIgnores(['scripts/**']),
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
// Debt: the base layer enforces kebab-case filenames and this package has
|
||||
// 31 files that predate it. Rename them, then delete this block.
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores([
|
||||
'coverage/**',
|
||||
'vitest.config.*.ts',
|
||||
'evaluations/programmatic/python/.venv/**',
|
||||
]),
|
||||
nodeConfig,
|
||||
globalIgnores(['coverage/**', 'vitest.config.*.ts', 'evaluations/programmatic/python/.venv/**']),
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
complexity: 'error',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
rules: {
|
||||
complexity: 'error',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
files: ['./src/test/**/*.ts', './**/*.test.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
{
|
||||
files: ['./src/test/**/*.ts', './**/*.test.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
// The eval harness is dev-only tooling (excluded from the build output),
|
||||
// so devDependencies are fine.
|
||||
files: ['./evaluations/**/*.ts'],
|
||||
rules: {
|
||||
'import-x/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
{
|
||||
// The eval harness is dev-only tooling (excluded from the build output),
|
||||
// so devDependencies (e.g. n8n-core for __schema__ resolution) are fine.
|
||||
files: ['./evaluations/**/*.ts'],
|
||||
rules: {
|
||||
'import-x/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-wrapper-object-types': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
/**
|
||||
* This package is full of HTTP header maps (`Content-Type`, `Authorization`, `X-Custom-Header`)
|
||||
* and charset identifiers (`iso-8859-15`, `windows-1252`).
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
// Depends on @n8n/db, so the encryption guardrails must run here even
|
||||
// though it is not on nodeConfig.
|
||||
encryptionBoundaryConfig,
|
||||
{
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
},
|
||||
},
|
||||
);
|
||||
export default defineConfig(backendConfig);
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
globalIgnores(['scenarios/**', 'scripts/**']),
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
complexity: 'error',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'no-empty': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
// Relax type-aware unsafe rules for untyped mock plumbing, mirroring n8n-core
|
||||
files: ['**/__tests__/**/*.ts'],
|
||||
@@ -14,8 +14,6 @@ export default defineConfig(
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
/**
|
||||
* This package is full of AWS SDK params (`Bucket`, `Key`, `Delete`) and
|
||||
* HTTP header maps (`content-type`, `x-amz-meta-filename`).
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
// oclif requires default exports for command classes
|
||||
'import-x/no-default-export': 'off',
|
||||
// This is a standalone CLI — no n8n-workflow dependency available
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'off',
|
||||
},
|
||||
export default defineConfig(backendConfig, {
|
||||
// The oclif command registry requires colon-separated keys (e.g. 'workflow:list')
|
||||
// The HTTP client uses standard header names (X-N8N-API-KEY, Content-Type)
|
||||
files: ['src/index.ts', 'src/client.ts', 'src/commands/login.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
{
|
||||
// The oclif command registry requires colon-separated keys (e.g. 'workflow:list')
|
||||
// The HTTP client uses standard header names (X-N8N-API-KEY, Content-Type)
|
||||
files: ['src/index.ts', 'src/client.ts', 'src/commands/login.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'off',
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,4 +19,12 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/client-oauth2.ts'],
|
||||
// This package is a standalone OAuth2 client and predates the
|
||||
// backend-network factory. Route it through the factory, then delete this.
|
||||
rules: {
|
||||
'n8n-local-rules/no-uncentralized-http': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3,11 +3,7 @@ import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, globalIgnores(['src/grammar*.ts']), {
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,9 +3,6 @@ import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, globalIgnores(['src/expressions/grammar*.ts']), {
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(nodeConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
export default defineConfig(backendConfig);
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
},
|
||||
export default defineConfig(backendConfig, {
|
||||
files: ['**/*.config.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-untyped-config-class-field': 'error',
|
||||
},
|
||||
{
|
||||
files: ['**/*.config.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-untyped-config-class-field': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(nodeConfig);
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -15,32 +14,17 @@ export default defineConfig(
|
||||
{
|
||||
ignores: ['scripts/**'],
|
||||
},
|
||||
baseConfig,
|
||||
// This package holds the DeploymentKey entity/repository, so the
|
||||
// encryption guardrails must run here even though it is not on nodeConfig.
|
||||
encryptionBoundaryConfig,
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'n8n-local-rules/project-owned-entity-transfer': [
|
||||
'error',
|
||||
{ acknowledged: acknowledgedProjectOwnedEntities },
|
||||
],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/no-restricted-types': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-empty': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'import-x/export': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'import-x/order': 'warn',
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
{ ignores: ['compiled/**', 'vitest.integration.config.ts'] },
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
{
|
||||
// Debt: the base layer enforces kebab-case filenames and this package has
|
||||
// 2 files that predate it. Rename them, then delete this block.
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,70 +1,71 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@n8n/eslint-config",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"exports": {
|
||||
"./base": {
|
||||
"types": "./dist/configs/base.d.js",
|
||||
"default": "./dist/configs/base.js"
|
||||
},
|
||||
"./frontend": {
|
||||
"types": "./dist/configs/frontend.d.js",
|
||||
"default": "./dist/configs/frontend.js"
|
||||
},
|
||||
"./node": {
|
||||
"types": "./dist/configs/node.d.js",
|
||||
"default": "./dist/configs/node.js"
|
||||
},
|
||||
"./encryption-boundary": {
|
||||
"types": "./dist/configs/encryption-boundary.d.js",
|
||||
"default": "./dist/configs/encryption-boundary.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:unchecked": "tsc --noCheck",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:dev": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@typescript-eslint/rule-tester": "catalog:",
|
||||
"@typescript-eslint/utils": "catalog:",
|
||||
"@typescript/native": "catalog:typescript-tooling",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-import-x": "^4.15.2",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"globals": "^16.2.0",
|
||||
"typescript": "catalog:typescript-tooling",
|
||||
"typescript-eslint": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "catalog:"
|
||||
},
|
||||
"license": "LicenseRef-n8n-sustainable-use",
|
||||
"dependencies": {
|
||||
"@n8n/eslint-plugin-design-system": "workspace:*"
|
||||
}
|
||||
"private": true,
|
||||
"name": "@n8n/eslint-config",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"exports": {
|
||||
"./base": {
|
||||
"types": "./dist/configs/base.d.js",
|
||||
"default": "./dist/configs/base.js"
|
||||
},
|
||||
"./backend": {
|
||||
"types": "./dist/configs/backend.d.js",
|
||||
"default": "./dist/configs/backend.js"
|
||||
},
|
||||
"./frontend": {
|
||||
"types": "./dist/configs/frontend.d.js",
|
||||
"default": "./dist/configs/frontend.js"
|
||||
},
|
||||
"./nodes": {
|
||||
"types": "./dist/configs/nodes.d.js",
|
||||
"default": "./dist/configs/nodes.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:unchecked": "tsc --noCheck",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:dev": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@typescript-eslint/rule-tester": "catalog:",
|
||||
"@typescript-eslint/utils": "catalog:",
|
||||
"@typescript/native": "catalog:typescript-tooling",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-import-x": "^4.15.2",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"globals": "^16.2.0",
|
||||
"typescript": "catalog:typescript-tooling",
|
||||
"typescript-eslint": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "catalog:"
|
||||
},
|
||||
"license": "LicenseRef-n8n-sustainable-use",
|
||||
"dependencies": {
|
||||
"@n8n/eslint-plugin-design-system": "workspace:*",
|
||||
"eslint-plugin-n8n-nodes-base": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import tseslint from 'typescript-eslint';
|
||||
* Backend outbound HTTP must go through the `@n8n/backend-network` factory so
|
||||
* SSRF/DNS guarding and proxy handling stay centrally controlled. This turns on
|
||||
* `n8n-local-rules/no-uncentralized-http` for every Node backend package (it is
|
||||
* part of `nodeConfig`).
|
||||
* part of `backendConfig`).
|
||||
*
|
||||
* Out of natural scope:
|
||||
* - Frontend packages (they use `frontendConfig`, not `nodeConfig`)
|
||||
* - `@n8n/backend-network` itself (it uses `baseConfig`, not `nodeConfig`)
|
||||
* - Frontend packages (they use `frontendConfig`, not `backendConfig`)
|
||||
*
|
||||
* Note that only the `Agent` class is restricted from `node:http`/`node:https`,
|
||||
* so a package that starts a server with `createServer` needs nothing here.
|
||||
*
|
||||
* Prefer an inline `// eslint-disable-next-line ... -- <reason>` for a single
|
||||
* callsite. Use the lists below only for whole-path scope exclusions or tracked
|
||||
@@ -21,7 +23,11 @@ export const backendNetworkBoundaryConfig = tseslint.config({
|
||||
'n8n-local-rules/no-uncentralized-http': [
|
||||
'error',
|
||||
{
|
||||
allow: ['packages/@n8n/benchmark/'],
|
||||
allow: [
|
||||
// The factory itself: this is where the guarded client is built.
|
||||
'packages/@n8n/backend-network/',
|
||||
'packages/@n8n/benchmark/',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import globals from 'globals';
|
||||
import { baseConfig } from './base.js';
|
||||
import { backendNetworkBoundaryConfig } from './backend-network-boundary.js';
|
||||
import { encryptionBoundaryConfig } from './encryption-boundary.js';
|
||||
|
||||
/**
|
||||
* Every package that runs on Node: services, CLIs, backend modules and the
|
||||
* repo's own tooling. It carries the two guardrails that only make sense off
|
||||
* the browser, so a package cannot reach the cipher primitives or make an
|
||||
* unguarded outbound request without opting into them by hand.
|
||||
*
|
||||
* `nodesConfig` builds on this, so the two nodes packages are covered too.
|
||||
*/
|
||||
export const backendConfig = tseslint.config(
|
||||
baseConfig,
|
||||
backendNetworkBoundaryConfig,
|
||||
encryptionBoundaryConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -85,13 +85,6 @@ export const baseConfig = tseslint.config(
|
||||
*/
|
||||
'no-void': ['error', { allowAsStatement: true }],
|
||||
|
||||
/**
|
||||
* https://eslint.org/docs/latest/rules/indent
|
||||
*
|
||||
* Delegated to Prettier.
|
||||
*/
|
||||
indent: 'off',
|
||||
|
||||
/**
|
||||
* https://eslint.org/docs/latest/rules/no-constant-binary-expression
|
||||
*/
|
||||
@@ -187,45 +180,6 @@ export const baseConfig = tseslint.config(
|
||||
},
|
||||
],
|
||||
|
||||
// Not needed because we use Biome formatting
|
||||
'@stylistic/ident': 'off',
|
||||
|
||||
/**
|
||||
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md
|
||||
*/
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'default',
|
||||
format: ['camelCase'],
|
||||
},
|
||||
{
|
||||
selector: 'import',
|
||||
format: ['camelCase', 'PascalCase'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
trailingUnderscore: 'allowSingleOrDouble',
|
||||
},
|
||||
{
|
||||
selector: 'property',
|
||||
format: ['camelCase', 'snake_case', 'UPPER_CASE'],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
trailingUnderscore: 'allowSingleOrDouble',
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: ['PascalCase'],
|
||||
},
|
||||
{
|
||||
selector: ['method', 'function', 'parameter'],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-void-type.md
|
||||
*/
|
||||
@@ -266,11 +220,6 @@ export const baseConfig = tseslint.config(
|
||||
*/
|
||||
'@typescript-eslint/no-unused-expressions': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md
|
||||
*/
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-optional-chain.md
|
||||
*/
|
||||
@@ -305,26 +254,6 @@ export const baseConfig = tseslint.config(
|
||||
*/
|
||||
'import-x/no-cycle': ['error', { ignoreExternal: false, maxDepth: 3 }],
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
|
||||
*/
|
||||
'import-x/no-default-export': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/order.md
|
||||
*/
|
||||
'import-x/order': [
|
||||
'error',
|
||||
{
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
groups: [['builtin', 'external'], 'internal', ['parent', 'index', 'sibling'], 'object'],
|
||||
'newlines-between': 'always',
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/HEAD/docs/rules/no-duplicates.md
|
||||
*/
|
||||
@@ -424,7 +353,16 @@ export const baseConfig = tseslint.config(
|
||||
/**
|
||||
* https://github.com/sweepline/eslint-plugin-unused-imports/blob/master/docs/rules/no-unused-imports.md
|
||||
*/
|
||||
'unused-imports/no-unused-imports': process.env.NODE_ENV === 'development' ? 'warn' : 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
|
||||
*
|
||||
* Set here because 35 packages had each set it for themselves. The
|
||||
* `frontend` and `nodes` layers turn it off: a Vue component, a
|
||||
* composable and a node file all carry a meaningful capital letter.
|
||||
*/
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
/** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unnecessary-await.md */
|
||||
'unicorn/no-unnecessary-await': 'error',
|
||||
@@ -434,18 +372,47 @@ export const baseConfig = tseslint.config(
|
||||
|
||||
'lodash/path-style': ['error', 'as-needed'],
|
||||
'lodash/import-scope': ['error', 'method'],
|
||||
|
||||
/**
|
||||
* Rules the repo had already stopped enforcing.
|
||||
*
|
||||
* Each of these was switched off or downgraded in ten or more of the
|
||||
* 72 packages, one config at a time, and every lint script runs with
|
||||
* `--quiet`, so a downgrade to `warn` enforced nothing either. Turning
|
||||
* them off here states that once, instead of in fifty places.
|
||||
*
|
||||
* To enforce one again, set it to `error` in the package that is ready
|
||||
* for it; a local upgrade is allowed and is how `naming-convention`
|
||||
* still runs in twelve packages. Deleting a line from this list is a
|
||||
* repo-wide change and needs the violations fixed first.
|
||||
*
|
||||
* Counted by `scripts/lint-parity/majority.mjs`.
|
||||
*/
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'import-x/no-default-export': 'off',
|
||||
'import-x/order': 'off',
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'off',
|
||||
'no-empty': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Rules for unit tests
|
||||
files: ['test/**/*.ts', '**/__tests__/*.ts', '**/*.test.ts', '**/*.cy.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
// Test code casts mocks into position; the rule's assignability check reads
|
||||
// those casts as redundant and removing them breaks the build.
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'n8n-local-rules/no-skipped-tests': process.env.NODE_ENV === 'development' ? 'warn' : 'error',
|
||||
'n8n-local-rules/no-skipped-tests': 'error',
|
||||
'n8n-local-rules/no-error-instance-in-to-throw': 'error',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'off',
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import { configs as designSystemConfigs } from '@n8n/eslint-plugin-design-system
|
||||
import globals from 'globals';
|
||||
import { baseConfig } from './base.js';
|
||||
|
||||
const isCI = process.env.CI === 'true';
|
||||
const extraFileExtensions = ['.vue'];
|
||||
const allGlobals = { NodeJS: true, ...globals.node, ...globals.browser };
|
||||
|
||||
@@ -18,9 +17,11 @@ export const frontendConfig = tseslint.config(
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-debugger': isCI ? 'error' : 'off',
|
||||
semi: [2, 'always'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
|
||||
// A component file is PascalCase and a composable is `useThing.ts`, so
|
||||
// the kebab-case default from the base layer does not apply here.
|
||||
'unicorn/filename-case': 'off',
|
||||
|
||||
'@typescript-eslint/no-use-before-define': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'n8n-local-rules/no-reka-ui-pagination': 'error',
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import globals from 'globals';
|
||||
import { baseConfig } from './base.js';
|
||||
import { backendNetworkBoundaryConfig } from './backend-network-boundary.js';
|
||||
import { encryptionBoundaryConfig } from './encryption-boundary.js';
|
||||
|
||||
export const nodeConfig = tseslint.config(
|
||||
baseConfig,
|
||||
backendNetworkBoundaryConfig,
|
||||
encryptionBoundaryConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,178 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import nodesBasePlugin from 'eslint-plugin-n8n-nodes-base';
|
||||
|
||||
import { backendConfig } from './backend.js';
|
||||
|
||||
/**
|
||||
* The two packages that ship nodes: `n8n-nodes-base` and
|
||||
* `@n8n/nodes-langchain`.
|
||||
*
|
||||
* They had kept the same ~100 `n8n-nodes-base/*` rules in their own configs,
|
||||
* which had already drifted (a duplicated key in each). The rules describe the
|
||||
* node and credential file formats, so they belong to the node format, not to
|
||||
* one package.
|
||||
*
|
||||
* The globs stay relative: ESLint resolves `files` against the config that
|
||||
* consumes this one, so `credentials/*.ts` means each package's own tree.
|
||||
*
|
||||
* The `@n8n/community-nodes` rules stay in the two packages. That plugin peer-
|
||||
* depends on `n8n-workflow`, which reaches this package again through
|
||||
* `@n8n/utils`, and turbo's build graph rejects the cycle. Only the two rules
|
||||
* both packages agree on are duplicated, against 97 lifted here.
|
||||
*/
|
||||
export const nodesConfig = tseslint.config(
|
||||
backendConfig,
|
||||
{
|
||||
plugins: {
|
||||
'n8n-nodes-base': nodesBasePlugin,
|
||||
},
|
||||
|
||||
rules: {
|
||||
// A node file is `Slack.node.ts` and a credential is
|
||||
// `SlackApi.credentials.ts`, so the kebab-case default cannot apply.
|
||||
'unicorn/filename-case': 'off',
|
||||
|
||||
'n8n-local-rules/no-dynamic-regexp': 'error',
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { allowTernary: true }],
|
||||
|
||||
/**
|
||||
* Retired for both nodes packages, which had each downgraded all of
|
||||
* these. Node source is largely generated or transcribed from a
|
||||
* vendor API, so it does not read like the rest of the repo.
|
||||
*/
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-duplicate-type-constituents': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
eqeqeq: 'off',
|
||||
'id-denylist': 'off',
|
||||
'import-x/extensions': 'off',
|
||||
'n8n-local-rules/no-argument-spread': 'off',
|
||||
'no-async-promise-executor': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'no-extra-boolean-cast': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['credentials/*.ts'],
|
||||
rules: {
|
||||
'n8n-nodes-base/cred-class-field-authenticate-type-assertion': 'error',
|
||||
'n8n-nodes-base/cred-class-field-display-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/cred-class-field-documentation-url-missing': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-uppercase-first-char': 'error',
|
||||
'n8n-nodes-base/cred-class-field-properties-assertion': 'error',
|
||||
'n8n-nodes-base/cred-class-field-type-options-password-missing': 'error',
|
||||
'n8n-nodes-base/cred-class-name-missing-oauth2-suffix': 'error',
|
||||
'n8n-nodes-base/cred-class-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-filename-against-convention': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['nodes/**/*.ts'],
|
||||
rules: {
|
||||
'n8n-nodes-base/node-class-description-credentials-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-class-description-icon-not-svg': 'off',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-missing-subtitle': 'error',
|
||||
'n8n-nodes-base/node-class-description-non-core-color-present': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
|
||||
'n8n-nodes-base/node-dirname-against-convention': 'error',
|
||||
'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error',
|
||||
'n8n-nodes-base/node-execute-block-wrong-error-thrown': 'error',
|
||||
'n8n-nodes-base/node-filename-against-convention': 'error',
|
||||
'n8n-nodes-base/node-param-array-type-assertion': 'error',
|
||||
'n8n-nodes-base/node-param-color-type-unused': 'error',
|
||||
'n8n-nodes-base/node-param-default-missing': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-number': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-boolean-without-whether': 'error',
|
||||
'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error',
|
||||
'n8n-nodes-base/node-param-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-description-identical-to-display-name': 'error',
|
||||
'n8n-nodes-base/node-param-description-line-break-html-tag': 'error',
|
||||
'n8n-nodes-base/node-param-description-lowercase-first-char': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-json': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-url': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error',
|
||||
'n8n-nodes-base/node-param-description-unneeded-backticks': 'error',
|
||||
'n8n-nodes-base/node-param-description-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-description-url-missing-protocol': 'error',
|
||||
'n8n-nodes-base/node-param-description-weak': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-not-first-position': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error',
|
||||
'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-action-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-description-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-without-action': 'error',
|
||||
'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-option-description-identical-to-name': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-containing-star': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-option-value-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-missing-email': 'error',
|
||||
'n8n-nodes-base/node-param-required-false': 'error',
|
||||
'n8n-nodes-base/node-param-resource-with-plural-option': 'error',
|
||||
'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-type-options-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-type-options-password-missing': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', '**/test/**/*.ts', '**/__test__/**/*.ts', '**/__tests__/**/*.ts'],
|
||||
rules: {
|
||||
'import-x/no-extraneous-dependencies': 'off',
|
||||
'n8n-nodes-base/node-filename-against-convention': 'off',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
declare module 'eslint-plugin-lodash';
|
||||
declare module 'eslint-plugin-n8n-nodes-base';
|
||||
|
||||
@@ -5,7 +5,6 @@ import { NoUnusedParamInCatchClauseRule } from './no-unused-param-catch-clause.j
|
||||
import { NoUselessCatchThrowRule } from './no-useless-catch-throw.js';
|
||||
import { NoSkippedTestsRule } from './no-skipped-tests.js';
|
||||
import { NoInterpolationInRegularStringRule } from './no-interpolation-in-regular-string.js';
|
||||
import { NoPlainErrorsRule } from './no-plain-errors.js';
|
||||
import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.js';
|
||||
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
||||
import { NoGuardrailDisableRule } from './no-guardrail-disable.js';
|
||||
@@ -16,7 +15,6 @@ import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-mo
|
||||
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
||||
import { NoArgumentSpreadRule } from './no-argument-spread.js';
|
||||
import { NoInternalPackageImportRule } from './no-internal-package-import.js';
|
||||
import { NoImportEnterpriseEditionRule } from './no-import-enterprise-edition.js';
|
||||
import { NoTypeOnlyImportInDiRule } from './no-type-only-import-in-di.js';
|
||||
import { NoErrorInstanceInToThrowRule } from './no-error-instance-in-to-throw.js';
|
||||
import { NoAwsCredentialDiscoveryImportsRule } from './no-aws-credential-discovery-imports.js';
|
||||
@@ -44,7 +42,6 @@ export const rules = {
|
||||
'no-useless-catch-throw': NoUselessCatchThrowRule,
|
||||
'no-skipped-tests': NoSkippedTestsRule,
|
||||
'no-interpolation-in-regular-string': NoInterpolationInRegularStringRule,
|
||||
'no-plain-errors': NoPlainErrorsRule,
|
||||
'no-dynamic-import-template': NoDynamicImportTemplateRule,
|
||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
||||
'no-guardrail-disable': NoGuardrailDisableRule,
|
||||
@@ -54,7 +51,6 @@ export const rules = {
|
||||
'no-constructor-in-backend-module': NoConstructorInBackendModuleRule,
|
||||
'no-argument-spread': NoArgumentSpreadRule,
|
||||
'no-internal-package-import': NoInternalPackageImportRule,
|
||||
'no-import-enterprise-edition': NoImportEnterpriseEditionRule,
|
||||
'no-type-only-import-in-di': NoTypeOnlyImportInDiRule,
|
||||
'no-error-instance-in-to-throw': NoErrorInstanceInToThrowRule,
|
||||
'no-aws-credential-discovery-imports': NoAwsCredentialDiscoveryImportsRule,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoImportEnterpriseEditionRule } from './no-import-enterprise-edition.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-import-enterprise-edition', NoImportEnterpriseEditionRule, {
|
||||
valid: [
|
||||
{
|
||||
// Non-enterprise code importing from non-enterprise directories
|
||||
code: 'import { SomeService } from "./services/some-service"',
|
||||
filename: '/Users/test/project/src/services/regular-service.ts',
|
||||
},
|
||||
{
|
||||
code: 'import { Utils } from "../utils/helper"',
|
||||
filename: '/Users/test/project/src/controllers/controller.ts',
|
||||
},
|
||||
{
|
||||
code: 'import { Config } from "@n8n/config"',
|
||||
filename: '/Users/test/project/src/services/service.ts',
|
||||
},
|
||||
|
||||
// enterprise code importing from ee directories
|
||||
{
|
||||
code: 'import { EnterpriseService } from "../environments.ee/services/enterprise-service"',
|
||||
filename: '/Users/test/project/src/environments.ee/controllers/enterprise-controller.ts',
|
||||
},
|
||||
|
||||
// enterprise code importing from non-enterprise directories
|
||||
{
|
||||
code: 'import { RegularService } from "../services/regular-service"',
|
||||
filename: '/Users/test/project/src/environments.ee/controllers/enterprise-controller.ts',
|
||||
},
|
||||
{
|
||||
code: 'import { Config } from "@n8n/config"',
|
||||
filename: '/Users/test/project/src/environments.ee/services/service.ts',
|
||||
},
|
||||
|
||||
// integration test files can import from .ee directories
|
||||
{
|
||||
code: 'import { EnterpriseService } from "../environments.ee/services/enterprise-service"',
|
||||
filename:
|
||||
'/Users/test/project/packages/cli/test/integration/services/enterprise.integration.test.ts',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import { something } from "@n8n/package/environments.ee/file"',
|
||||
filename: '/Users/test/project/src/index.ts',
|
||||
errors: [{ messageId: 'noImportEnterpriseEdition' }],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { RegularService } from "./regular-service";
|
||||
import { EnterpriseService } from "environments.ee/enterprise-service";
|
||||
`,
|
||||
filename: '/Users/test/project/src/services/service.ts',
|
||||
errors: [{ messageId: 'noImportEnterpriseEdition' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoImportEnterpriseEditionRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow imports from .ee directories in non-enterprise code. Only code in .ee directories can import from other .ee directories.',
|
||||
},
|
||||
messages: {
|
||||
noImportEnterpriseEdition:
|
||||
'Non-enterprise code cannot import from .ee directories. Only code in .ee directories can import from other .ee directories.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
const filename = context.filename;
|
||||
const isEnterpriseEditionFile = filename.includes('.ee/');
|
||||
const isIntegrationTestFile = filename.includes('packages/cli/test/integration/');
|
||||
|
||||
if (isEnterpriseEditionFile || isIntegrationTestFile) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
|
||||
const isEnterpriseEditionImport = importPath.includes('.ee/');
|
||||
|
||||
if (isEnterpriseEditionImport) {
|
||||
context.report({
|
||||
node: node.source,
|
||||
messageId: 'noImportEnterpriseEdition',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoPlainErrorsRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Only `UserError`, `OperationalError`, `UnexpectedError` (from the `n8n-workflow` package) or their child classes must be thrown. This ensures the error will be normalized when reported to Sentry, if applicable.',
|
||||
},
|
||||
messages: {
|
||||
useN8nError:
|
||||
'Throw a `UserError`, `OperationalError` or `UnexpectedError` (from the `n8n-workflow` package) or one of their child classes.',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
ThrowStatement(node) {
|
||||
if (!node.argument) return;
|
||||
|
||||
const isNewError =
|
||||
node.argument.type === TSESTree.AST_NODE_TYPES.NewExpression &&
|
||||
node.argument.callee.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
||||
node.argument.callee.name === 'Error';
|
||||
|
||||
const isNewlessError =
|
||||
node.argument.type === TSESTree.AST_NODE_TYPES.CallExpression &&
|
||||
node.argument.callee.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
||||
node.argument.callee.name === 'Error';
|
||||
|
||||
if (isNewError || isNewlessError) {
|
||||
return context.report({
|
||||
messageId: 'useN8nError',
|
||||
node,
|
||||
fix: (fixer) =>
|
||||
fixer.replaceText(
|
||||
node,
|
||||
`throw new UnexpectedError(${(node.argument as TSESTree.CallExpression).arguments
|
||||
.map((arg) => context.sourceCode.getText(arg))
|
||||
.join(', ')})`,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
import eslintPlugin from 'eslint-plugin-eslint-plugin';
|
||||
|
||||
export default defineConfig([
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
eslintPlugin.configs.recommended,
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
|
||||
@@ -3,12 +3,8 @@ import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'import-x/order': 'warn',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'import-x/order': 'warn',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
const LAZY_RUNTIME_IMPORT_MESSAGE =
|
||||
'Use an existing lazy loader, or add one near first use. Static runtime imports of this dependency undo the idle-memory guardrail.';
|
||||
@@ -18,7 +18,7 @@ const restrictedLazyRuntimeImports = [
|
||||
}));
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
ignores: [
|
||||
'scripts/**/*.cjs',
|
||||
@@ -93,4 +93,19 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Debt: the base layer enforces kebab-case filenames and this package has
|
||||
// 6 files that predate it. Rename them, then delete this block.
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['evaluations/clients/n8n-client.ts'],
|
||||
// An evaluation harness that talks to a local instance it started
|
||||
// itself, so the guarded client buys nothing here.
|
||||
rules: {
|
||||
'n8n-local-rules/no-uncentralized-http': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
baseConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/no-duplicate-imports': 'off',
|
||||
'import-x/no-cycle': 'off',
|
||||
complexity: 'error',
|
||||
|
||||
@@ -20,7 +18,7 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores(['electron-builder.config.js','scripts/**']),
|
||||
nodeConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
},
|
||||
globalIgnores(['electron-builder.config.js', 'scripts/**']),
|
||||
backendConfig,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(nodeConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
export default defineConfig(backendConfig);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores(['src/template/templates/**/template', 'src/template/templates/shared']),
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
{
|
||||
files: ['**/*.test.ts', 'src/test-utils/**/*'],
|
||||
rules: {
|
||||
@@ -14,4 +14,11 @@ export default defineConfig(
|
||||
files: ['src/commands/**/*.ts', 'src/modules.d.ts', 'src/configs/eslint.ts'],
|
||||
rules: { 'import-x/no-default-export': 'off', '@typescript-eslint/naming-convention': 'off' },
|
||||
},
|
||||
{
|
||||
// Debt: the base layer enforces kebab-case filenames and this package has
|
||||
// 22 files that predate it. Rename them, then delete this block.
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig({ ignores: ['dist/**', 'vitest.integration.config.ts'] }, nodeConfig, {
|
||||
files: ['src/__tests__/**'],
|
||||
rules: {
|
||||
// Workflow fixtures key connections by node name, e.g. "When clicking Execute"
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
export default defineConfig(
|
||||
{ ignores: ['dist/**', 'vitest.integration.config.ts'] },
|
||||
backendConfig,
|
||||
{
|
||||
files: ['src/__tests__/**'],
|
||||
rules: {
|
||||
// Workflow fixtures key connections by node name, e.g. "When clicking Execute"
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
@@ -1,175 +1,17 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import nodesBasePlugin from 'eslint-plugin-n8n-nodes-base';
|
||||
import { nodesConfig } from '@n8n/eslint-config/nodes';
|
||||
import { n8nCommunityNodesPlugin } from '@n8n/eslint-plugin-community-nodes';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
{
|
||||
plugins: {
|
||||
'@n8n/community-nodes': n8nCommunityNodesPlugin,
|
||||
},
|
||||
rules: {
|
||||
// TODO: remove all the following rules
|
||||
eqeqeq: 'warn',
|
||||
'id-denylist': 'warn',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'error',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-extra-boolean-cast': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-async-promise-executor': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
|
||||
'import-x/order': 'warn',
|
||||
'import-x/extensions': 'warn',
|
||||
|
||||
'n8n-local-rules/no-argument-spread': 'warn', // TODO: mark error
|
||||
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
||||
|
||||
'@n8n/community-nodes/credential-documentation-url': ['error', { allowSlugs: true }],
|
||||
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/naming-convention': ['error', { selector: 'memberLike', format: null }],
|
||||
'@typescript-eslint/no-explicit-any': 'warn', //812 warnings, better to fix in separate PR
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn', //665 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn', //7084 problems, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-call': 'warn', //541 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn', //4591 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-return': 'warn', //438 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { allowTernary: true }],
|
||||
'@typescript-eslint/restrict-template-expressions': 'warn', //1152 errors, better to fix in separate PR
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/restrict-plus-operands': 'warn',
|
||||
'@typescript-eslint/no-duplicate-type-constituents': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
},
|
||||
export default defineConfig(nodesConfig, {
|
||||
plugins: {
|
||||
'@n8n/community-nodes': n8nCommunityNodesPlugin,
|
||||
},
|
||||
{
|
||||
files: ['./credentials/*.ts'],
|
||||
plugins: {
|
||||
'n8n-nodes-base': nodesBasePlugin,
|
||||
},
|
||||
rules: {
|
||||
'n8n-nodes-base/cred-class-field-authenticate-type-assertion': 'error',
|
||||
'n8n-nodes-base/cred-class-field-display-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/cred-class-field-documentation-url-missing': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-uppercase-first-char': 'error',
|
||||
'n8n-nodes-base/cred-class-field-properties-assertion': 'error',
|
||||
'n8n-nodes-base/cred-class-field-type-options-password-missing': 'error',
|
||||
'n8n-nodes-base/cred-class-name-missing-oauth2-suffix': 'error',
|
||||
'n8n-nodes-base/cred-class-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-filename-against-convention': 'error',
|
||||
},
|
||||
rules: {
|
||||
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
||||
|
||||
'@n8n/community-nodes/credential-documentation-url': ['error', { allowSlugs: true }],
|
||||
|
||||
'@typescript-eslint/naming-convention': ['error', { selector: 'memberLike', format: null }],
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { allowTernary: true }],
|
||||
},
|
||||
{
|
||||
files: ['./nodes/**/*.ts'],
|
||||
plugins: {
|
||||
'n8n-nodes-base': nodesBasePlugin,
|
||||
},
|
||||
rules: {
|
||||
'n8n-nodes-base/node-class-description-credentials-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-class-description-icon-not-svg': 'off',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
|
||||
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-missing-subtitle': 'error',
|
||||
'n8n-nodes-base/node-class-description-non-core-color-present': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-dirname-against-convention': 'error',
|
||||
'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error',
|
||||
'n8n-nodes-base/node-execute-block-wrong-error-thrown': 'error',
|
||||
'n8n-nodes-base/node-filename-against-convention': 'error',
|
||||
'n8n-nodes-base/node-param-array-type-assertion': 'error',
|
||||
'n8n-nodes-base/node-param-color-type-unused': 'error',
|
||||
'n8n-nodes-base/node-param-default-missing': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-number': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-boolean-without-whether': 'error',
|
||||
'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error',
|
||||
'n8n-nodes-base/node-param-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-description-identical-to-display-name': 'error',
|
||||
'n8n-nodes-base/node-param-description-line-break-html-tag': 'error',
|
||||
'n8n-nodes-base/node-param-description-lowercase-first-char': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-json': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-url': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error',
|
||||
'n8n-nodes-base/node-param-description-unneeded-backticks': 'error',
|
||||
'n8n-nodes-base/node-param-description-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-description-url-missing-protocol': 'error',
|
||||
'n8n-nodes-base/node-param-description-weak': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-not-first-position': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error',
|
||||
'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-action-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-description-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-without-action': 'error',
|
||||
'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-option-description-identical-to-name': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-containing-star': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-option-value-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-missing-email': 'error',
|
||||
'n8n-nodes-base/node-param-required-false': 'error',
|
||||
'n8n-nodes-base/node-param-resource-with-plural-option': 'error',
|
||||
'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-type-options-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-type-options-password-missing': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', '**/test/**/*.ts', '**/__test__/**/*.ts', '**/__tests__/**/*.ts'],
|
||||
rules: {
|
||||
'import-x/no-extraneous-dependencies': 'warn',
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'warn',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -228,7 +228,6 @@
|
||||
"@types/sanitize-html": "catalog:",
|
||||
"@types/temp": "^0.9.1",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"eslint-plugin-n8n-nodes-base": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"@typescript/native": "catalog:typescript-tooling",
|
||||
"typescript": "catalog:typescript-tooling",
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'import-x/order': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
// Stryker-only config, not part of any tsconfig project (like the shared
|
||||
@@ -7,12 +7,7 @@ export default defineConfig(
|
||||
// ignored too: an interrupted mutation run leaves a sandbox behind that
|
||||
// would otherwise fail `pnpm lint` with parsing errors.
|
||||
globalIgnores(['stryker.config.mjs', 'vitest.stryker.config.ts', '.stryker-tmp/**']),
|
||||
baseConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
},
|
||||
backendConfig,
|
||||
{
|
||||
// This package must stay free of DB/DI coupling: it declares the contracts,
|
||||
// the host (cli's DurableScheduler) satisfies them.
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
},
|
||||
export default defineConfig(backendConfig, {
|
||||
files: ['**/*.config.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-untyped-config-class-field': 'error',
|
||||
},
|
||||
{
|
||||
files: ['**/*.config.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-untyped-config-class-field': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
// Depends on n8n-core, so the encryption guardrails must run here even
|
||||
// though it is not on nodeConfig.
|
||||
encryptionBoundaryConfig,
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
complexity: 'error',
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig({ ignores: ['bin/**'] }, baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
export default defineConfig({ ignores: ['bin/**'] }, baseConfig);
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||
'@typescript-eslint/no-restricted-types': 'off',
|
||||
'@typescript-eslint/no-implied-eval': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-types': 'off',
|
||||
'@typescript-eslint/no-implied-eval': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
// Debt: the base layer enforces kebab-case filenames and this package has
|
||||
// 12 files that predate it. Rename them, then delete this block.
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(nodeConfig, {
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'no-prototype-builtins': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
export function assert(condition: unknown, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
// eslint-disable-next-line n8n-local-rules/no-plain-errors
|
||||
throw new Error(message ?? 'Assertion failed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('createEventQueue', () => {
|
||||
const processedEvents: string[] = [];
|
||||
const processEvent = vi.fn(async (event: string) => {
|
||||
if (event === 'fail') {
|
||||
throw new Error('Processing error'); // eslint-disable-line n8n-local-rules/no-plain-errors
|
||||
throw new Error('Processing error');
|
||||
}
|
||||
processedEvents.push(event);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('retry', () => {
|
||||
|
||||
it('should reject if the function throws an error', async () => {
|
||||
const fn = vi.fn(async () => {
|
||||
throw new Error('Test error'); // eslint-disable-line n8n-local-rules/no-plain-errors
|
||||
throw new Error('Test error');
|
||||
});
|
||||
|
||||
// Since the error is thrown on the first call, no timer advancement is needed.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
import { n8nCommunityNodesPlugin } from '@n8n/eslint-plugin-community-nodes';
|
||||
|
||||
const ADM_ZIP_LAZY_IMPORT_MESSAGE =
|
||||
@@ -7,7 +7,7 @@ const ADM_ZIP_LAZY_IMPORT_MESSAGE =
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores(['test-fixtures/**', 'scripts/**']),
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
{
|
||||
plugins: {
|
||||
'@n8n/community-nodes': n8nCommunityNodesPlugin,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -40,11 +40,9 @@ const engineV2ModuleOnlyImport = {
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores(['scripts/**/*.mjs', 'vitest.*.ts', 'coverage/**']),
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
'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
|
||||
@@ -87,52 +85,27 @@ export default defineConfig(
|
||||
'error',
|
||||
{ acknowledged: acknowledgedProjectOwnedEntities },
|
||||
],
|
||||
// Disabled until we have a plan on how to fix these issues long term
|
||||
'n8n-local-rules/no-import-enterprise-edition': 'off',
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'import-x/no-cycle': 'warn',
|
||||
'import-x/extensions': [
|
||||
'warn',
|
||||
'never',
|
||||
{
|
||||
pathGroupOverrides: [
|
||||
{
|
||||
pattern:
|
||||
'**/*.{service,controller,registry,repository,entity,dto,middleware,module,strategy,handler,helper,error,request,response,mapper,schema,types,constants,config,util,utils}',
|
||||
action: 'ignore',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'import-x/order': 'warn',
|
||||
'import-x/extensions': 'off',
|
||||
'no-ex-assign': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
'no-unsafe-optional-chaining': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-async-promise-executor': 'warn',
|
||||
complexity: 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
complexity: 'off',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-restricted-types': 'warn',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
|
||||
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
|
||||
'@typescript-eslint/only-throw-error': 'warn',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/array-type': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/no-duplicate-type-constituents': 'warn',
|
||||
@@ -431,14 +404,7 @@ export default defineConfig(
|
||||
{
|
||||
files: ['./src/decorators/**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-types': [
|
||||
'warn',
|
||||
{
|
||||
types: {
|
||||
Function: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-restricted-types': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
backendConfig,
|
||||
globalIgnores(['bin/*.js', 'nodes-testing/*.ts', 'nodes-testing/*.cjs', 'coverage/*']),
|
||||
{
|
||||
rules: {
|
||||
// TODO: Lower the complexity threshold
|
||||
complexity: ['error', 27],
|
||||
'n8n-local-rules/no-dynamic-regexp': 'error',
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove these
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-ex-assign': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-array-delete': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, globalIgnores(['src/shims.d.ts']), {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove these
|
||||
'import-x/order': 'warn',
|
||||
'import-x/no-default-export': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
},
|
||||
});
|
||||
export default defineConfig(baseConfig, globalIgnores(['src/shims.d.ts']));
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { frontendConfig } from '@n8n/eslint-config/frontend';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig(frontendConfig, {
|
||||
rules: {
|
||||
// TODO: Remove these
|
||||
'no-empty': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
},
|
||||
});
|
||||
export default defineConfig(frontendConfig);
|
||||
|
||||
@@ -9,29 +9,16 @@ export default defineConfig(
|
||||
rules: {
|
||||
'vue/no-undef-components': ['error', { ignorePatterns: ['N8nDropdownMenuItem'] }],
|
||||
|
||||
// TODO: Remove these
|
||||
'import-x/no-default-export': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/restrict-template-expressions': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.stories.ts', 'src/**/*.vue', 'src/**/*.spec.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'warn',
|
||||
'off',
|
||||
{
|
||||
selector: ['variable', 'property'],
|
||||
format: ['PascalCase', 'camelCase', 'UPPER_CASE'],
|
||||
|
||||
@@ -3,11 +3,6 @@ import { frontendConfig } from '@n8n/eslint-config/frontend';
|
||||
|
||||
export default defineConfig(frontendConfig, {
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { frontendConfig } from '@n8n/eslint-config/frontend';
|
||||
|
||||
export default defineConfig(frontendConfig, {
|
||||
rules: {
|
||||
// TODO: Remove these
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
},
|
||||
});
|
||||
export default defineConfig(frontendConfig);
|
||||
|
||||
@@ -3,9 +3,6 @@ import { frontendConfig } from '@n8n/eslint-config/frontend';
|
||||
|
||||
export default defineConfig(frontendConfig, {
|
||||
rules: {
|
||||
//TODO: Remove these
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { frontendConfig } from '@n8n/eslint-config/frontend';
|
||||
import oxlint from 'eslint-plugin-oxlint';
|
||||
|
||||
/**
|
||||
* `ignorePatterns` says which files the oxlint pass skips, so ESLint covers them
|
||||
* instead. Handing the key to `eslint-plugin-oxlint` turns it into a global
|
||||
* ESLint ignore and both linters skip the file, so strip it here and build from
|
||||
* the rest.
|
||||
*/
|
||||
const oxlintConfig = (() => {
|
||||
const { ignorePatterns, ...rest } = JSON.parse(
|
||||
readFileSync(new URL('./.oxlintrc.json', import.meta.url), 'utf8'),
|
||||
);
|
||||
return rest;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Extraction ratchet: a feature that has become a module package must not reappear
|
||||
* under `src/features/`. Append one entry per extraction — this list only grows.
|
||||
@@ -39,7 +53,7 @@ export default defineConfig(
|
||||
// Guard: prevent direct node access on workflowsStore — use workflowDocumentStore instead.
|
||||
// Level: 'warn' during migration. Flip to 'error' when migration is complete.
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
'off',
|
||||
{
|
||||
selector: "MemberExpression[property.name='allNodes'][object.name='workflowsStore']",
|
||||
message: 'Use workflowDocumentStore.allNodes instead of workflowsStore.allNodes',
|
||||
@@ -218,19 +232,16 @@ export default defineConfig(
|
||||
],
|
||||
// TODO: Remove these
|
||||
'n8n-local-rules/no-internal-package-import': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'id-denylist': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
'no-extra-boolean-cast': 'warn',
|
||||
'no-sparse-arrays': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'import-x/extensions': 'warn',
|
||||
'import-x/no-default-export': 'warn',
|
||||
'import-x/order': 'off',
|
||||
'import-x/no-cycle': 'warn',
|
||||
'import-x/no-duplicates': 'warn',
|
||||
'no-unsafe-optional-chaining': 'warn',
|
||||
@@ -238,7 +249,6 @@ export default defineConfig(
|
||||
'@typescript-eslint/dot-notation': 'warn',
|
||||
'@stylistic/lines-between-class-members': 'warn',
|
||||
'@stylistic/member-delimiter-style': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/no-empty-interface': 'warn',
|
||||
'@typescript-eslint/no-for-in-array': 'warn',
|
||||
'@typescript-eslint/no-loop-func': 'warn',
|
||||
@@ -248,26 +258,15 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-var-requires': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/restrict-plus-operands': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'vue/attribute-hyphenation': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/restrict-template-expressions': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-restricted-imports': ['error', { patterns: extractedFeatures }],
|
||||
},
|
||||
},
|
||||
@@ -288,7 +287,7 @@ export default defineConfig(
|
||||
files: ['src/app/constants/modals.ts'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
'off',
|
||||
{
|
||||
selector:
|
||||
'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator[id.name!=/^MODAL_(CANCEL|CONFIRM|CLOSE)$/]',
|
||||
@@ -313,7 +312,7 @@ export default defineConfig(
|
||||
files: ['src/app/stores/defaults/modals.ts'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
'off',
|
||||
{
|
||||
selector:
|
||||
"VariableDeclarator[id.name='SHELL_MODAL_INITIAL_STATE'] > CallExpression > ObjectExpression > :matches(Property, SpreadElement)",
|
||||
@@ -350,6 +349,16 @@ export default defineConfig(
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/no-dynamic-regexp': 'off',
|
||||
|
||||
// A stub component keeps its Vue template in a plain string, where
|
||||
// `${...}` and backticks belong to the Vue expression and must stay
|
||||
// uninterpolated. Both rules read them as JavaScript.
|
||||
'n8n-local-rules/no-interpolation-in-regular-string': 'off',
|
||||
'n8n-local-rules/no-unneeded-backticks': 'off',
|
||||
|
||||
// A test parses fixtures it declares itself. An unexpected throw is
|
||||
// the signal the test wants, so it needs no guard.
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -384,5 +393,5 @@ export default defineConfig(
|
||||
],
|
||||
},
|
||||
},
|
||||
...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json'),
|
||||
...oxlint.buildFromOxlintConfig(oxlintConfig),
|
||||
);
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
// Depends on n8n-core, so the encryption guardrails must run here even
|
||||
// though it is not on nodeConfig.
|
||||
encryptionBoundaryConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'unicorn/filename-case': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
},
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'unicorn/filename-case': 'warn',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
import nodesBasePlugin from 'eslint-plugin-n8n-nodes-base';
|
||||
import { nodesConfig } from '@n8n/eslint-config/nodes';
|
||||
import { n8nCommunityNodesPlugin } from '@n8n/eslint-plugin-community-nodes';
|
||||
|
||||
import { databricksUserAgentRestriction } from './nodes/Databricks/eslint-user-agent-restriction.mjs';
|
||||
import { kafkaImportRestrictions } from './nodes/Kafka/eslint-import-restrictions.mjs';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
nodesConfig,
|
||||
// This config fragment lives under nodes/ so it sits next to the code it
|
||||
// governs, but it's not lintable TS source — same reason eslint.config.mjs
|
||||
// itself is never linted.
|
||||
@@ -24,182 +23,23 @@ export default defineConfig(
|
||||
|
||||
rules: {
|
||||
'@n8n/community-nodes/credential-documentation-url': ['error', { allowSlugs: true }],
|
||||
'@n8n/community-nodes/node-class-description-icon-missing': 'warn',
|
||||
'@n8n/community-nodes/cred-class-field-icon-missing': 'warn',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'error',
|
||||
|
||||
// TODO: remove all the following rules
|
||||
eqeqeq: 'warn',
|
||||
'id-denylist': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-ex-assign': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'no-constant-condition': 'warn',
|
||||
'no-async-promise-executor': 'warn',
|
||||
'no-dupe-else-if': 'warn',
|
||||
'no-extra-boolean-cast': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
|
||||
'import-x/extensions': 'warn',
|
||||
'import-x/no-default-export': 'warn',
|
||||
'import-x/export': 'warn',
|
||||
'import-x/order': 'warn',
|
||||
'import-x/no-default-export': 'warn',
|
||||
'import-x/no-extraneous-dependencies': 'warn',
|
||||
|
||||
'n8n-local-rules/no-argument-spread': 'warn', // TODO: mark error
|
||||
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
|
||||
'@typescript-eslint/naming-convention': ['warn'],
|
||||
'@typescript-eslint/no-explicit-any': 'warn', //812 warnings, better to fix in separate PR
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn', //665 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn', //7084 problems, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-call': 'warn', //541 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn', //4591 errors, better to fix in separate PR
|
||||
'@typescript-eslint/no-unsafe-return': 'warn', //438 errors, better to fix in separate PR
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { allowTernary: true }],
|
||||
'@typescript-eslint/restrict-template-expressions': 'warn', //1152 errors, better to fix in separate PR
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/restrict-plus-operands': 'warn',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-duplicate-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./credentials/*.ts'],
|
||||
plugins: {
|
||||
'n8n-nodes-base': nodesBasePlugin,
|
||||
},
|
||||
rules: {
|
||||
'n8n-nodes-base/cred-class-field-authenticate-type-assertion': 'error',
|
||||
'n8n-nodes-base/cred-class-field-display-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/cred-class-field-documentation-url-missing': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-uppercase-first-char': 'error',
|
||||
'n8n-nodes-base/cred-class-field-properties-assertion': 'error',
|
||||
'n8n-nodes-base/cred-class-field-type-options-password-missing': 'error',
|
||||
'n8n-nodes-base/cred-class-name-missing-oauth2-suffix': 'error',
|
||||
'n8n-nodes-base/cred-class-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-filename-against-convention': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./nodes/**/*.ts'],
|
||||
plugins: {
|
||||
'n8n-nodes-base': nodesBasePlugin,
|
||||
},
|
||||
rules: {
|
||||
'n8n-nodes-base/node-class-description-credentials-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-class-description-icon-not-svg': 'off',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-missing-subtitle': 'error',
|
||||
'n8n-nodes-base/node-class-description-non-core-color-present': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
|
||||
'n8n-nodes-base/node-dirname-against-convention': 'error',
|
||||
'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error',
|
||||
'n8n-nodes-base/node-execute-block-wrong-error-thrown': 'error',
|
||||
'n8n-nodes-base/node-filename-against-convention': 'error',
|
||||
'n8n-nodes-base/node-param-array-type-assertion': 'error',
|
||||
'n8n-nodes-base/node-param-color-type-unused': 'error',
|
||||
'n8n-nodes-base/node-param-default-missing': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-number': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-boolean-without-whether': 'error',
|
||||
'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error',
|
||||
'n8n-nodes-base/node-param-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-description-identical-to-display-name': 'error',
|
||||
'n8n-nodes-base/node-param-description-line-break-html-tag': 'error',
|
||||
'n8n-nodes-base/node-param-description-lowercase-first-char': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-json': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-url': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error',
|
||||
'n8n-nodes-base/node-param-description-unneeded-backticks': 'error',
|
||||
'n8n-nodes-base/node-param-description-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-description-url-missing-protocol': 'error',
|
||||
'n8n-nodes-base/node-param-description-weak': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-not-first-position': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error',
|
||||
'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-action-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-description-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-without-action': 'error',
|
||||
'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-option-description-identical-to-name': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-containing-star': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-get-many': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-option-value-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-missing-email': 'error',
|
||||
'n8n-nodes-base/node-param-required-false': 'error',
|
||||
'n8n-nodes-base/node-param-resource-with-plural-option': 'error',
|
||||
'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-type-options-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-type-options-password-missing': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', '**/test/**/*.ts'],
|
||||
rules: {
|
||||
'import-x/no-extraneous-dependencies': 'off',
|
||||
'n8n-nodes-base/node-filename-against-convention': 'off',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'off',
|
||||
},
|
||||
},
|
||||
...databricksUserAgentRestriction,
|
||||
...kafkaImportRestrictions,
|
||||
);
|
||||
|
||||
@@ -923,7 +923,6 @@
|
||||
"@types/ssh2-sftp-client": "^9.0.5",
|
||||
"@types/uuid": "catalog:",
|
||||
"@types/xml2js": "catalog:",
|
||||
"eslint-plugin-n8n-nodes-base": "catalog:",
|
||||
"n8n-containers": "workspace:*",
|
||||
"n8n-core": "workspace:*",
|
||||
"nock": "catalog:",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
ignores: ['coverage/**', 'dist/**'],
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { CodeHealthContext } from './context.js';
|
||||
import { CatalogViolationsRule } from './rules/catalog-violations.rule.js';
|
||||
import { EncryptionBoundaryRule } from './rules/encryption-boundary.rule.js';
|
||||
import { EndpointScopeCoverageRule } from './rules/endpoint-scope-coverage.rule.js';
|
||||
import { LintConfigLayeringRule } from './rules/lint-config-layering.rule.js';
|
||||
import { MigrationTimestampRule } from './rules/migration-timestamp.rule.js';
|
||||
import { SingleInstanceLibsRule } from './rules/single-instance-libs.rule.js';
|
||||
import { SingleInstanceLockfileRule } from './rules/single-instance-lockfile.rule.js';
|
||||
@@ -16,6 +17,7 @@ export type { CodeHealthContext } from './context.js';
|
||||
export { CatalogViolationsRule } from './rules/catalog-violations.rule.js';
|
||||
export { EncryptionBoundaryRule } from './rules/encryption-boundary.rule.js';
|
||||
export { EndpointScopeCoverageRule } from './rules/endpoint-scope-coverage.rule.js';
|
||||
export { LintConfigLayeringRule } from './rules/lint-config-layering.rule.js';
|
||||
export { MigrationTimestampRule } from './rules/migration-timestamp.rule.js';
|
||||
export { SingleInstanceLibsRule } from './rules/single-instance-libs.rule.js';
|
||||
export { SingleInstanceLockfileRule } from './rules/single-instance-lockfile.rule.js';
|
||||
@@ -55,6 +57,13 @@ const defaultRuleSettings: RuleSettingsMap = {
|
||||
severity: 'error',
|
||||
options: {},
|
||||
},
|
||||
'lint-config-layering': {
|
||||
enabled: true,
|
||||
severity: 'error',
|
||||
// This package is a dependency of @n8n/eslint-config, so extending it
|
||||
// would be a cycle.
|
||||
options: { exempt: ['packages/frontend/@n8n/eslint-plugin-design-system'] },
|
||||
},
|
||||
'stale-overrides': {
|
||||
enabled: true,
|
||||
severity: 'warning',
|
||||
@@ -136,6 +145,7 @@ export function createDefaultRunner(settings?: RuleSettingsMap): RuleRunner<Code
|
||||
runner.registerRule(new SingleInstanceLibsRule());
|
||||
runner.registerRule(new SingleInstanceLockfileRule());
|
||||
runner.registerRule(new EncryptionBoundaryRule());
|
||||
runner.registerRule(new LintConfigLayeringRule());
|
||||
runner.registerRule(new StaleOverridesRule());
|
||||
runner.registerRule(new EndpointScopeCoverageRule());
|
||||
runner.registerRule(new SubpathPurityRule());
|
||||
|
||||
@@ -7,16 +7,15 @@ import type { CodeHealthContext } from '../context.js';
|
||||
import { EncryptionBoundaryRule } from './encryption-boundary.rule.js';
|
||||
|
||||
const BOUNDARY_CONFIG = `import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(baseConfig, encryptionBoundaryConfig);
|
||||
export default defineConfig(backendConfig);
|
||||
`;
|
||||
|
||||
const NODE_CONFIG = `import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
const NODES_CONFIG = `import { defineConfig } from 'eslint/config';
|
||||
import { nodesConfig } from '@n8n/eslint-config/nodes';
|
||||
|
||||
export default defineConfig(nodeConfig);
|
||||
export default defineConfig(nodesConfig);
|
||||
`;
|
||||
|
||||
const BASE_CONFIG = `import { defineConfig } from 'eslint/config';
|
||||
@@ -65,14 +64,14 @@ describe('EncryptionBoundaryRule', () => {
|
||||
}
|
||||
|
||||
describe('config coverage', () => {
|
||||
it('accepts a package that composes encryptionBoundaryConfig', async () => {
|
||||
it('accepts a package that extends backendConfig', async () => {
|
||||
writePackage('packages/a', { 'n8n-core': 'workspace:*' });
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('accepts a package on nodeConfig', async () => {
|
||||
writePackage('packages/a', { 'n8n-core': 'workspace:*' }, 'dependencies', NODE_CONFIG);
|
||||
it('accepts a package on nodesConfig', async () => {
|
||||
writePackage('packages/a', { 'n8n-core': 'workspace:*' }, 'dependencies', NODES_CONFIG);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
@@ -87,8 +86,8 @@ describe('EncryptionBoundaryRule', () => {
|
||||
expect(violations[0]).toContain('does not compose the encryption boundary');
|
||||
});
|
||||
|
||||
it('flags a boundary import that is never used', async () => {
|
||||
const imported = `${BASE_CONFIG}import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';\n`;
|
||||
it('flags a layer import that is never used', async () => {
|
||||
const imported = `${BASE_CONFIG}import { backendConfig } from '@n8n/eslint-config/backend';\n`;
|
||||
writePackage('packages/a', { 'n8n-core': 'workspace:*' }, 'dependencies', imported);
|
||||
|
||||
expect(await analyze()).toHaveLength(1);
|
||||
@@ -132,9 +131,9 @@ export const extra = {
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(2);
|
||||
expect(violations[0]).toContain('eslint.config.mjs:8');
|
||||
expect(violations[0]).toContain('eslint.config.mjs:7');
|
||||
expect(violations[0]).toContain('no-deployment-key-delete');
|
||||
expect(violations[1]).toContain('eslint.config.mjs:9');
|
||||
expect(violations[1]).toContain('eslint.config.mjs:8');
|
||||
expect(violations[1]).toContain('no-legacy-cipher-methods');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,10 +35,10 @@ const CONFIG_FILENAMES = [
|
||||
'eslint.config.ts',
|
||||
];
|
||||
|
||||
/** `nodeConfig` composes `encryptionBoundaryConfig`, so either import brings the rules in. */
|
||||
/** `backendConfig` composes the boundary, and `nodesConfig` composes `backendConfig`. */
|
||||
const BOUNDARY_IMPORT =
|
||||
/import\s*\{([^}]*)\}\s*from\s+['"]@n8n\/eslint-config\/(?:node|encryption-boundary)['"]/;
|
||||
const BOUNDARY_EXPORTS = /^(nodeConfig|encryptionBoundaryConfig)(?:\s+as\s+(\w+))?$/;
|
||||
/import\s*\{([^}]*)\}\s*from\s+['"]@n8n\/eslint-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. */
|
||||
const DOWNGRADE =
|
||||
@@ -134,7 +134,7 @@ export class EncryptionBoundaryRule extends BaseRule<CodeHealthContext> {
|
||||
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 composes `encryptionBoundaryConfig` from @n8n/eslint-config/encryption-boundary (or `nodeConfig`).',
|
||||
'Add an eslint.config.mjs that extends `backendConfig` from @n8n/eslint-config/backend (or `nodesConfig`).',
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -149,7 +149,7 @@ export class EncryptionBoundaryRule extends BaseRule<CodeHealthContext> {
|
||||
1,
|
||||
1,
|
||||
`${packageName} depends on ${trigger} but its ESLint config does not compose the encryption boundary.`,
|
||||
"Import `encryptionBoundaryConfig` from '@n8n/eslint-config/encryption-boundary' (or use `nodeConfig`) and add it to the exported config.",
|
||||
"Extend `backendConfig` from '@n8n/eslint-config/backend' (or `nodesConfig`) and add it to the exported config.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CodeHealthContext } from '../context.js';
|
||||
import { LintConfigLayeringRule } from './lint-config-layering.rule.js';
|
||||
|
||||
const HEAD = `import { defineConfig } from 'eslint/config';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
`;
|
||||
|
||||
describe('LintConfigLayeringRule', () => {
|
||||
let rootDir: string;
|
||||
let rule: LintConfigLayeringRule;
|
||||
|
||||
beforeEach(() => {
|
||||
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-health-lint-layering-'));
|
||||
rule = new LintConfigLayeringRule();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writePackage(dir: string, config: string | null): void {
|
||||
const full = path.join(rootDir, dir);
|
||||
fs.mkdirSync(full, { recursive: true });
|
||||
fs.writeFileSync(path.join(full, 'package.json'), JSON.stringify({ name: dir }));
|
||||
if (config !== null) fs.writeFileSync(path.join(full, 'eslint.config.mjs'), config);
|
||||
}
|
||||
|
||||
async function analyze(options: Record<string, unknown> = {}): Promise<string[]> {
|
||||
rule.configure({ enabled: true, severity: 'error', options });
|
||||
const context: CodeHealthContext = { rootDir };
|
||||
const violations = await rule.analyze(context);
|
||||
return violations.map((v) => `${path.relative(rootDir, v.file)}:${v.line} ${v.message}`);
|
||||
}
|
||||
|
||||
describe('accepts', () => {
|
||||
it('a config that only extends its layer', async () => {
|
||||
writePackage('packages/a', `${HEAD}\nexport default defineConfig(backendConfig);\n`);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores and an additive plugin config', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}import playwright from 'eslint-plugin-playwright';\n
|
||||
export default defineConfig(
|
||||
{ ignores: ['dist/**'] },
|
||||
backendConfig,
|
||||
playwright.configs['flat/recommended'],
|
||||
);
|
||||
`,
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('a package-wide block that only raises rules', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: {
|
||||
complexity: ['error', 27],
|
||||
'@typescript-eslint/naming-convention': ['error', { selector: 'default', format: ['camelCase'] }],
|
||||
},
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('a relaxation scoped to real paths', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
files: ['src/legacy/thing.ts'],
|
||||
rules: { 'n8n-local-rules/no-uncentralized-http': 'off' },
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('a relaxation scoped to test files', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
files: ['**/*.test.ts'],
|
||||
rules: { '@typescript-eslint/no-explicit-any': 'off' },
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('a package with no ESLint config', async () => {
|
||||
writePackage('packages/a', null);
|
||||
|
||||
expect(await analyze()).toEqual([]);
|
||||
});
|
||||
|
||||
it('an exempt package', async () => {
|
||||
writePackage(
|
||||
'packages/frontend/@n8n/eslint-plugin-design-system',
|
||||
"import tseslint from 'typescript-eslint';\nexport default tseslint.config();\n",
|
||||
);
|
||||
|
||||
expect(
|
||||
await analyze({ exempt: ['packages/frontend/@n8n/eslint-plugin-design-system'] }),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reports', () => {
|
||||
it('a package-wide downgrade', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: { '@typescript-eslint/no-explicit-any': 'warn' },
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0]).toContain("turns '@typescript-eslint/no-explicit-any' down to 'warn'");
|
||||
});
|
||||
|
||||
it('a package-wide off, and a numeric severity', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: { 'no-empty': 'off', eqeqeq: 0 },
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
expect(await analyze()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('a downgrade in a block whose glob still covers the package', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
files: ['src/**/*.ts'],
|
||||
rules: { 'no-empty': 'off' },
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0]).toContain("turns 'no-empty' down to 'off'");
|
||||
});
|
||||
|
||||
it('a severity that is not a literal', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}
|
||||
export default defineConfig(backendConfig, {
|
||||
rules: { 'no-debugger': process.env.CI ? 'error' : 'off' },
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0]).toContain('is not a literal');
|
||||
});
|
||||
|
||||
it('two shared layers at once', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`${HEAD}import { frontendConfig } from '@n8n/eslint-config/frontend';\n
|
||||
export default defineConfig(backendConfig, frontendConfig);
|
||||
`,
|
||||
);
|
||||
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0]).toContain('extends more than one shared layer');
|
||||
});
|
||||
|
||||
it('an import of a retired subpath', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
`import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
|
||||
export default defineConfig(nodeConfig);
|
||||
`,
|
||||
);
|
||||
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(2);
|
||||
expect(violations[0]).toContain("'@n8n/eslint-config/node', which no longer exists");
|
||||
expect(violations[1]).toContain('does not extend a shared ESLint layer');
|
||||
});
|
||||
|
||||
it('a config that extends no layer', async () => {
|
||||
writePackage(
|
||||
'packages/a',
|
||||
"import tseslint from 'typescript-eslint';\nexport default tseslint.config();\n",
|
||||
);
|
||||
|
||||
const violations = await analyze();
|
||||
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0]).toContain('does not extend a shared ESLint layer');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
import { BaseRule } from '@n8n/rules-engine';
|
||||
import type { Violation } from '@n8n/rules-engine';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Project, ScriptKind, SyntaxKind } from 'ts-morph';
|
||||
import type { Node, ObjectLiteralExpression } from 'ts-morph';
|
||||
|
||||
import type { CodeHealthContext } from '../context.js';
|
||||
import { findPackageJsonFiles, relativeDir } from '../utils/package-json-scanner.js';
|
||||
|
||||
const CONFIG_FILENAMES = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
|
||||
|
||||
const LAYERS = ['base', 'backend', 'frontend', 'nodes'];
|
||||
const LAYER_IMPORT = /^@n8n\/eslint-config\/([a-z-]+)$/;
|
||||
|
||||
/** Retired export paths, replaced by the four layers. */
|
||||
const REMOVED_SUBPATHS = new Set(['node', 'encryption-boundary']);
|
||||
|
||||
/**
|
||||
* A `files` glob that covers effectively the whole package, so a block scoped to
|
||||
* it is a package-wide decision wearing a scope.
|
||||
*/
|
||||
const PACKAGE_WIDE_GLOB =
|
||||
/^\.?\/?(?:\*\*|\*\*\/\*|src\/\*\*|src\/\*\*\/\*)(?:\.[cm]?[jt]sx?|\.vue)?$/;
|
||||
|
||||
/** A glob that names test material, where relaxations are expected. */
|
||||
const TEST_GLOB = /(^|\/)(?:test|tests|__test__|__tests__)(\/|$)|\.(?:test|spec|cy|stories)\./;
|
||||
|
||||
const WEAK_SEVERITY = new Set(['off', 'warn', '0', '1']);
|
||||
|
||||
/**
|
||||
* Keeps the ESLint setup readable by keeping the decisions in one place.
|
||||
*
|
||||
* The repo used to carry a rule table in each of 72 package configs, which is
|
||||
* how it ended up with the same override written 35 times and four rules
|
||||
* silently shadowed by a duplicate key. The four shared layers hold the policy
|
||||
* now, so a package config may pick a layer and scope exceptions to paths, but
|
||||
* it may not quietly re-decide a rule for its whole tree.
|
||||
*
|
||||
* Allowed in a package config:
|
||||
* - exactly one shared layer
|
||||
* - `ignores`, and additive plugin configs
|
||||
* - any severity inside a block with a real `files` scope (that is a ratchet,
|
||||
* and it says which paths it covers)
|
||||
* - a package-wide block that only raises rules to `error`
|
||||
*
|
||||
* Reported: a package-wide `off`/`warn`, a severity that is not a literal (it
|
||||
* cannot be read from the config or translated to another linter), and any
|
||||
* import of a retired subpath. Existing debt lives in the baseline, which only
|
||||
* shrinks.
|
||||
*/
|
||||
export class LintConfigLayeringRule extends BaseRule<CodeHealthContext> {
|
||||
readonly id = 'lint-config-layering';
|
||||
readonly name = 'Lint Config Layering';
|
||||
readonly description =
|
||||
'Package ESLint configs must extend one shared layer from @n8n/eslint-config and scope every relaxation to a path, so rule policy stays in the shared layers.';
|
||||
readonly severity = 'error' as const;
|
||||
|
||||
async analyze(context: CodeHealthContext): Promise<Violation[]> {
|
||||
const { rootDir } = context;
|
||||
const options = this.getOptions();
|
||||
const exempt = Array.isArray(options.exempt) ? (options.exempt as string[]) : [];
|
||||
|
||||
const violations: Violation[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const packageJsonPath of await findPackageJsonFiles(rootDir)) {
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
const rel = relativeDir(rootDir, packageJsonPath);
|
||||
if (exempt.some((entry) => rel === entry || rel.startsWith(`${entry}/`))) continue;
|
||||
|
||||
const configPath = CONFIG_FILENAMES.map((name) => path.join(packageDir, name)).find((p) =>
|
||||
fs.existsSync(p),
|
||||
);
|
||||
if (!configPath || seen.has(configPath)) continue;
|
||||
seen.add(configPath);
|
||||
|
||||
violations.push(...this.checkConfig(configPath, rel));
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
private checkConfig(configPath: string, packageName: string): Violation[] {
|
||||
const text = fs.readFileSync(configPath, 'utf-8');
|
||||
const source = new Project({
|
||||
useInMemoryFileSystem: true,
|
||||
compilerOptions: { allowJs: true },
|
||||
}).createSourceFile(configPath, text, { scriptKind: ScriptKind.JS });
|
||||
|
||||
const violations: Violation[] = [];
|
||||
const at = (node: Node) => {
|
||||
const { line, column } = source.getLineAndColumnAtPos(node.getStart());
|
||||
return { line, column };
|
||||
};
|
||||
|
||||
// 1. exactly one layer, and no retired subpath
|
||||
const layerImports: string[] = [];
|
||||
for (const declaration of source.getImportDeclarations()) {
|
||||
const specifier = declaration.getModuleSpecifierValue();
|
||||
const match = LAYER_IMPORT.exec(specifier);
|
||||
if (!match) continue;
|
||||
const subpath = match[1];
|
||||
if (REMOVED_SUBPATHS.has(subpath)) {
|
||||
const { line, column } = at(declaration);
|
||||
violations.push(
|
||||
this.createViolation(
|
||||
configPath,
|
||||
line,
|
||||
column,
|
||||
`${packageName} imports '@n8n/eslint-config/${subpath}', which no longer exists.`,
|
||||
'Use one of the four layers: base, backend, frontend or nodes. The boundary configs are part of backendConfig.',
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (LAYERS.includes(subpath)) layerImports.push(subpath);
|
||||
}
|
||||
|
||||
if (layerImports.length === 0) {
|
||||
violations.push(
|
||||
this.createViolation(
|
||||
configPath,
|
||||
1,
|
||||
1,
|
||||
`${packageName} does not extend a shared ESLint layer.`,
|
||||
'Import baseConfig, backendConfig, frontendConfig or nodesConfig from @n8n/eslint-config and pass it to defineConfig.',
|
||||
),
|
||||
);
|
||||
} else if (layerImports.length > 1) {
|
||||
violations.push(
|
||||
this.createViolation(
|
||||
configPath,
|
||||
1,
|
||||
1,
|
||||
`${packageName} extends more than one shared layer (${layerImports.join(', ')}).`,
|
||||
'Pick the single layer that matches the package. The backend layer already contains the base layer, and the nodes layer contains the backend layer.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. every `rules` block: package-wide blocks may only raise to error
|
||||
for (const rulesProperty of source.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
||||
if (rulesProperty.getName().replace(/['"]/g, '') !== 'rules') continue;
|
||||
const rulesObject = rulesProperty.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
|
||||
if (!rulesObject) continue;
|
||||
|
||||
const owner = rulesProperty.getParentIfKind(SyntaxKind.ObjectLiteralExpression);
|
||||
if (!owner) continue;
|
||||
|
||||
for (const entry of rulesObject.getProperties()) {
|
||||
if (!entry.isKind(SyntaxKind.PropertyAssignment)) continue;
|
||||
const ruleId = entry.getName().replace(/['"]/g, '');
|
||||
const severity = readSeverity(entry.getInitializer());
|
||||
|
||||
if (severity === undefined) {
|
||||
const { line, column } = at(entry);
|
||||
violations.push(
|
||||
this.createViolation(
|
||||
configPath,
|
||||
line,
|
||||
column,
|
||||
`${packageName} sets '${ruleId}' to a severity that is not a literal.`,
|
||||
'Use a literal severity. A severity computed from the environment makes the config resolve differently per machine and cannot be translated to another linter.',
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!WEAK_SEVERITY.has(severity)) continue;
|
||||
if (isScopedToPaths(owner)) continue;
|
||||
|
||||
const { line, column } = at(entry);
|
||||
violations.push(
|
||||
this.createViolation(
|
||||
configPath,
|
||||
line,
|
||||
column,
|
||||
`${packageName} turns '${ruleId}' down to '${severity}' for the whole package.`,
|
||||
'Scope it to the paths that need it with `files`, or retire the rule in the shared layer if the whole repo has stopped enforcing it. Under `--quiet` a warning enforces nothing.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
}
|
||||
|
||||
/** The literal severity of a rule entry, or undefined when it is computed. */
|
||||
function readSeverity(initializer: Node | undefined): string | undefined {
|
||||
if (!initializer) return undefined;
|
||||
if (initializer.isKind(SyntaxKind.StringLiteral)) return initializer.getLiteralValue();
|
||||
if (initializer.isKind(SyntaxKind.NumericLiteral)) return initializer.getText();
|
||||
if (initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
||||
return readSeverity(initializer.getElements()[0]);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** True when the block names paths narrower than the package itself. */
|
||||
function isScopedToPaths(owner: ObjectLiteralExpression): boolean {
|
||||
const filesProperty = owner.getProperty('files');
|
||||
if (!filesProperty?.isKind(SyntaxKind.PropertyAssignment)) return false;
|
||||
const array = filesProperty.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
||||
if (!array) return false;
|
||||
|
||||
const globs = array
|
||||
.getElements()
|
||||
.filter((element) => element.isKind(SyntaxKind.StringLiteral))
|
||||
.map((element) => element.getLiteralValue());
|
||||
if (globs.length === 0) return false;
|
||||
|
||||
// test globs always count as a scope; otherwise at least one glob must be
|
||||
// narrower than the whole package
|
||||
return globs.some((glob) => TEST_GLOB.test(glob) || !PACKAGE_WIDE_GLOB.test(glob));
|
||||
}
|
||||
@@ -1,19 +1,30 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
// Add exception for Docker Compose labels
|
||||
{
|
||||
selector: 'objectLiteralProperty',
|
||||
format: null, // Allow any format
|
||||
filter: {
|
||||
regex: '^com\\.docker\\.',
|
||||
match: true,
|
||||
export default defineConfig(
|
||||
backendConfig,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
// Add exception for Docker Compose labels
|
||||
{
|
||||
selector: 'objectLiteralProperty',
|
||||
format: null, // Allow any format
|
||||
filter: {
|
||||
regex: '^com\\.docker\\.',
|
||||
match: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
files: ['services/keycloak.ts'],
|
||||
// Test infrastructure that talks to a container it started itself,
|
||||
// so the guarded client buys nothing here.
|
||||
rules: {
|
||||
'n8n-local-rules/no-uncentralized-http': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
ignores: ['coverage/**', 'bin/**'],
|
||||
},
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { encryptionBoundaryConfig } from '@n8n/eslint-config/encryption-boundary';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
import playwrightPlugin from 'eslint-plugin-playwright';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
// Depends on n8n-core and @n8n/db, so the encryption guardrails must run
|
||||
// here even though it is not on nodeConfig.
|
||||
...encryptionBoundaryConfig,
|
||||
...backendConfig,
|
||||
playwrightPlugin.configs['flat/recommended'],
|
||||
{
|
||||
ignores: [
|
||||
@@ -22,15 +18,9 @@ export default [
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'off',
|
||||
'playwright/expect-expect': 'warn',
|
||||
'playwright/max-nested-describe': 'warn',
|
||||
'playwright/no-conditional-in-test': 'error',
|
||||
@@ -81,4 +71,11 @@ export default [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Debt: the base layer enforces kebab-case filenames and this package has
|
||||
// 124 files that predate it. Rename them, then delete this block.
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{
|
||||
ignores: ['coverage/**', 'dist/**'],
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
import { backendConfig } from '@n8n/eslint-config/backend';
|
||||
|
||||
export default defineConfig(
|
||||
baseConfig,
|
||||
backendConfig,
|
||||
{ ignores: ['dist/**', 'coverage/**'] },
|
||||
{
|
||||
// Test fixtures use file paths, spec names and line numbers as object keys
|
||||
|
||||
@@ -7,28 +7,19 @@ export default defineConfig(
|
||||
rules: {
|
||||
'import-x/no-extraneous-dependencies': 'error',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'error',
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
complexity: ['error', 23],
|
||||
|
||||
// TODO: remove these
|
||||
'no-empty': 'warn',
|
||||
'id-denylist': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'import-x/order': 'warn',
|
||||
'no-extra-boolean-cast': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/return-await': ['error', 'always'],
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-duplicate-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -41,7 +32,7 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'n8n-local-rules/no-dynamic-regexp': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+22
-52
@@ -2495,6 +2495,9 @@ importers:
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.29.0([email protected])([email protected])
|
||||
eslint-plugin-n8n-nodes-base:
|
||||
specifier: 'catalog:'
|
||||
version: 1.16.7(@typescript/[email protected])([email protected]([email protected])([email protected]))([email protected])
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.29.0
|
||||
@@ -3658,9 +3661,6 @@ importers:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.9(@vitest/[email protected])([email protected])
|
||||
eslint-plugin-n8n-nodes-base:
|
||||
specifier: 'catalog:'
|
||||
version: 1.16.7(@typescript/[email protected])([email protected]([email protected])([email protected]))([email protected])
|
||||
fast-glob:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.3
|
||||
@@ -7380,9 +7380,6 @@ importers:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.9(@vitest/[email protected])([email protected])
|
||||
eslint-plugin-n8n-nodes-base:
|
||||
specifier: 'catalog:'
|
||||
version: 1.16.7([email protected]([email protected])([email protected]))([email protected])([email protected])
|
||||
n8n-containers:
|
||||
specifier: workspace:*
|
||||
version: link:../testing/containers
|
||||
@@ -34250,6 +34247,7 @@ snapshots:
|
||||
typescript: '@typescript/[email protected]'
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
@@ -34260,15 +34258,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.35.0([email protected])
|
||||
'@typescript-eslint/types': 8.35.0
|
||||
debug: 4.4.3([email protected])
|
||||
typescript: 7.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.68.0([email protected])
|
||||
@@ -34314,6 +34303,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.35.0
|
||||
'@typescript-eslint/visitor-keys': 8.35.0
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/[email protected]':
|
||||
dependencies:
|
||||
@@ -34328,15 +34318,12 @@ snapshots:
|
||||
'@typescript-eslint/[email protected](@typescript/[email protected])':
|
||||
dependencies:
|
||||
typescript: '@typescript/[email protected]'
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])':
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])':
|
||||
dependencies:
|
||||
typescript: 7.0.2
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])':
|
||||
dependencies:
|
||||
typescript: 6.0.2
|
||||
@@ -34394,6 +34381,7 @@ snapshots:
|
||||
typescript: '@typescript/[email protected]'
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
@@ -34411,22 +34399,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.35.0([email protected])([email protected])
|
||||
'@typescript-eslint/tsconfig-utils': 8.35.0([email protected])
|
||||
'@typescript-eslint/types': 8.35.0
|
||||
'@typescript-eslint/visitor-keys': 8.35.0
|
||||
debug: 4.4.3([email protected])
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.9
|
||||
semver: 7.7.3
|
||||
ts-api-utils: 2.1.0([email protected])
|
||||
typescript: 7.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.68.0([email protected])([email protected])
|
||||
@@ -34482,17 +34454,7 @@ snapshots:
|
||||
typescript: '@typescript/[email protected]'
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]([email protected])([email protected]))
|
||||
'@typescript-eslint/scope-manager': 8.35.0
|
||||
'@typescript-eslint/types': 8.35.0
|
||||
'@typescript-eslint/typescript-estree': 8.35.0([email protected])([email protected])
|
||||
eslint: 9.29.0([email protected])([email protected])
|
||||
typescript: 7.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
@@ -34527,6 +34489,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]([email protected])([email protected]))
|
||||
'@typescript-eslint/scope-manager': 8.70.0
|
||||
'@typescript-eslint/types': 8.70.0
|
||||
'@typescript-eslint/typescript-estree': 8.70.0([email protected])([email protected])
|
||||
eslint: 9.29.0([email protected])([email protected])
|
||||
typescript: 7.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.35.0
|
||||
@@ -38075,7 +38048,7 @@ snapshots:
|
||||
|
||||
[email protected](@typescript/[email protected])([email protected]([email protected])([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.35.0(@typescript/[email protected])([email protected]([email protected])([email protected]))([email protected])
|
||||
'@typescript-eslint/utils': 8.70.0(@typescript/[email protected])([email protected]([email protected])([email protected]))([email protected])
|
||||
camel-case: 4.1.2
|
||||
indefinite: 2.5.1
|
||||
pascal-case: 3.1.2
|
||||
@@ -38089,7 +38062,7 @@ snapshots:
|
||||
|
||||
[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.35.0([email protected]([email protected])([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.70.0([email protected]([email protected])([email protected]))([email protected])([email protected])
|
||||
camel-case: 4.1.2
|
||||
indefinite: 2.5.1
|
||||
pascal-case: 3.1.2
|
||||
@@ -45476,15 +45449,12 @@ snapshots:
|
||||
[email protected](@typescript/[email protected]):
|
||||
dependencies:
|
||||
typescript: '@typescript/[email protected]'
|
||||
optional: true
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
typescript: 7.0.2
|
||||
|
||||
[email protected](@typescript/[email protected]):
|
||||
dependencies:
|
||||
typescript: '@typescript/[email protected]'
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Reads a leaf `eslint.config.mjs` as text and returns its own config blocks:
|
||||
* whether each one is scoped to `files`, and which rules it sets to what.
|
||||
*
|
||||
* Text, not `import`, on purpose. Importing gives the fully expanded array with
|
||||
* 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.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
/** Blank out comments and string bodies so brace counting is not fooled by them. */
|
||||
function mask(text) {
|
||||
const out = text.split('');
|
||||
let i = 0;
|
||||
const n = text.length;
|
||||
while (i < n) {
|
||||
const c = text[i];
|
||||
if (c === '/' && text[i + 1] === '/') {
|
||||
while (i < n && text[i] !== '\n') out[i++] = ' ';
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && text[i + 1] === '*') {
|
||||
out[i++] = ' ';
|
||||
out[i++] = ' ';
|
||||
while (i < n && !(text[i] === '*' && text[i + 1] === '/')) out[i++] = ' ';
|
||||
if (i < n) {
|
||||
out[i++] = ' ';
|
||||
out[i++] = ' ';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c === "'" || c === '"' || c === '`') {
|
||||
i++;
|
||||
while (i < n && text[i] !== c) {
|
||||
if (text[i] === '\\') out[i++] = ' ';
|
||||
if (i < n) out[i++] = ' ';
|
||||
}
|
||||
if (i < n) i++;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
/** Index of the brace closing the one at `open`, using the masked copy. */
|
||||
function matchBrace(masked, open) {
|
||||
let depth = 0;
|
||||
for (let i = open; i < masked.length; i++) {
|
||||
const c = masked[i];
|
||||
if (c === '{' || c === '[') depth++;
|
||||
else if (c === '}' || c === ']') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const SEVERITY = { error: 'error', warn: 'warn', off: 'off', 2: 'error', 1: 'warn', 0: 'off' };
|
||||
|
||||
/** Rule entries declared directly in this `rules: { ... }` body. */
|
||||
function readRules(text, masked, open) {
|
||||
const end = matchBrace(masked, open);
|
||||
if (end === -1) return [];
|
||||
const rules = [];
|
||||
let i = open + 1;
|
||||
while (i < end) {
|
||||
// locate the next key on the masked copy, then read its name from the real
|
||||
// text at the same offset: mask() preserves offsets but empties string bodies
|
||||
const m = /(?:'[^']*'|"[^"]*"|[A-Za-z$_][\w$-]*)\s*:/.exec(masked.slice(i, end + 1));
|
||||
if (!m) break;
|
||||
const abs = i + m.index;
|
||||
const idMatch = /^(?:'([^']+)'|"([^"]+)"|([A-Za-z$_][\w$-]*))/.exec(text.slice(abs));
|
||||
const id = idMatch ? (idMatch[1] ?? idMatch[2] ?? idMatch[3]) : undefined;
|
||||
let v = abs + m[0].length;
|
||||
while (v < end && /\s/.test(masked[v])) v++;
|
||||
let severity;
|
||||
let hasOptions = false;
|
||||
if (masked[v] === '[') {
|
||||
const close = matchBrace(masked, v);
|
||||
const inner = text.slice(v + 1, close);
|
||||
const innerMasked = masked.slice(v + 1, close);
|
||||
const first = /^\s*(?:'([^']+)'|"([^"]+)"|(\d))/.exec(inner);
|
||||
severity = first ? SEVERITY[first[1] ?? first[2] ?? Number(first[3])] : undefined;
|
||||
// options exist only if a comma sits at this array's own depth
|
||||
let d = 0;
|
||||
for (let k = 0; k < innerMasked.length; k++) {
|
||||
const c = innerMasked[k];
|
||||
if (c === '[' || c === '{') d++;
|
||||
else if (c === ']' || c === '}') d--;
|
||||
else if (c === ',' && d === 0) hasOptions = true;
|
||||
}
|
||||
i = close + 1;
|
||||
} else {
|
||||
const lit = /^(?:'([^']+)'|"([^"]+)"|(\d))/.exec(text.slice(v, end + 1));
|
||||
severity = lit ? SEVERITY[lit[1] ?? lit[2] ?? Number(lit[3])] : undefined;
|
||||
const nested = masked[v] === '{' ? matchBrace(masked, v) : -1;
|
||||
i = nested === -1 ? v + (lit ? lit[0].length : 1) : nested + 1;
|
||||
}
|
||||
if (!id || severity === undefined) continue;
|
||||
rules.push({ id, severity, hasOptions, start: abs, end: i });
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
/** @returns {{scoped: boolean, files: string[], rules: {id,severity,hasOptions}[]}[]} */
|
||||
export function readConfigBlocks(configPath) {
|
||||
const text = readFileSync(configPath, 'utf8');
|
||||
const masked = mask(text);
|
||||
const blocks = [];
|
||||
|
||||
// every `rules:` in the file belongs to exactly one enclosing object literal
|
||||
for (let i = masked.indexOf('rules:'); i !== -1; i = masked.indexOf('rules:', i + 1)) {
|
||||
const brace = masked.indexOf('{', i);
|
||||
if (brace === -1) continue;
|
||||
|
||||
// find the object that owns this `rules:` key
|
||||
let depth = 0;
|
||||
let owner = -1;
|
||||
for (let j = i; j >= 0; j--) {
|
||||
const c = masked[j];
|
||||
if (c === '}' || c === ']') depth++;
|
||||
else if (c === '{' || c === '[') {
|
||||
if (depth === 0) {
|
||||
owner = j;
|
||||
break;
|
||||
}
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
const ownerEnd = owner === -1 ? text.length : matchBrace(masked, owner);
|
||||
const ownerText = owner === -1 ? '' : text.slice(owner, ownerEnd);
|
||||
const ownerMasked = owner === -1 ? '' : masked.slice(owner, ownerEnd);
|
||||
const filesKey = /(^|[{,\s])files\s*:/.exec(ownerMasked);
|
||||
const files = [];
|
||||
if (filesKey) {
|
||||
const arr = ownerText.indexOf('[', filesKey.index);
|
||||
const close = matchBrace(mask(ownerText), arr);
|
||||
for (const g of ownerText.slice(arr, close).matchAll(/'([^']+)'|"([^"]+)"/g)) {
|
||||
files.push(g[1] ?? g[2]);
|
||||
}
|
||||
}
|
||||
blocks.push({
|
||||
scoped: Boolean(filesKey),
|
||||
files,
|
||||
rules: readRules(text, masked, brace),
|
||||
rulesStart: brace,
|
||||
rulesEnd: matchBrace(masked, brace),
|
||||
});
|
||||
i = brace;
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Compares two `snapshot.mjs` outputs and fails on any effective-config change
|
||||
* that is not declared in an `--expected` file.
|
||||
*
|
||||
* The expected file is the point of the tool: a consolidation PR always changes
|
||||
* something, so the reviewer reads a short list of intended changes instead of
|
||||
* trusting that 300 files still lint the same way.
|
||||
*
|
||||
* Expected entries match on `pkg` (prefix), optional `file` (substring),
|
||||
* optional `rule`, and carry a `reason` that shows up in the summary.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const [beforeFile, afterFile] = process.argv.slice(2).filter((a) => !a.startsWith('--'));
|
||||
const expectedIndex = process.argv.indexOf('--expected');
|
||||
if (!beforeFile || !afterFile) {
|
||||
console.error('usage: diff.mjs <before.json> <after.json> [--expected <file.json>]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const before = JSON.parse(readFileSync(beforeFile, 'utf8'));
|
||||
const after = JSON.parse(readFileSync(afterFile, 'utf8'));
|
||||
const expected =
|
||||
expectedIndex === -1 ? [] : JSON.parse(readFileSync(process.argv[expectedIndex + 1], 'utf8'));
|
||||
|
||||
const stable = (v) =>
|
||||
JSON.stringify(v, (_, x) =>
|
||||
x && typeof x === 'object' && !Array.isArray(x)
|
||||
? Object.fromEntries(Object.entries(x).sort(([a], [b]) => a.localeCompare(b)))
|
||||
: x,
|
||||
);
|
||||
|
||||
const diffs = [];
|
||||
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
||||
|
||||
for (const key of keys) {
|
||||
const [pkg, file] = key.split('|');
|
||||
const b = before[key];
|
||||
const a = after[key];
|
||||
|
||||
if (b?.error || a?.error) {
|
||||
diffs.push({
|
||||
pkg, file, key: '(unresolved)',
|
||||
before: b?.error ? 'error' : 'ok',
|
||||
after: a?.error ? 'error' : 'ok',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!b || !a) {
|
||||
diffs.push({
|
||||
pkg,
|
||||
file,
|
||||
key: '(sample)',
|
||||
before: b ? 'present' : 'absent',
|
||||
after: a ? 'present' : 'absent',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// a file dropping out of linting is never acceptable silently
|
||||
if (Boolean(b.ignored) !== Boolean(a.ignored)) {
|
||||
diffs.push({
|
||||
pkg,
|
||||
file,
|
||||
key: '(linted)',
|
||||
before: b.ignored ? 'ignored' : 'linted',
|
||||
after: a.ignored ? 'ignored' : 'linted',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (b.ignored) continue;
|
||||
|
||||
for (const rule of [...new Set([...Object.keys(b.rules), ...Object.keys(a.rules)])].sort()) {
|
||||
const bv = b.rules[rule] ?? [0];
|
||||
const av = a.rules[rule] ?? [0];
|
||||
if (stable(bv) !== stable(av)) {
|
||||
const show = (v) =>
|
||||
v[0] === 0 ? 'off' : v.length > 1 ? `error ${stable(v.slice(1))}` : 'error';
|
||||
diffs.push({ pkg, file, key: rule, before: show(bv), after: show(av) });
|
||||
}
|
||||
}
|
||||
for (const field of ['plugins', 'settings', 'languageOptions']) {
|
||||
if (stable(b[field]) !== stable(a[field])) {
|
||||
diffs.push({
|
||||
pkg,
|
||||
file,
|
||||
key: `(${field})`,
|
||||
before: stable(b[field]),
|
||||
after: stable(a[field]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const matches = (d, e) =>
|
||||
d.pkg.startsWith(e.pkg) &&
|
||||
(e.file === undefined || d.file.includes(e.file)) &&
|
||||
(e.rule === undefined || d.key === e.rule) &&
|
||||
(e.key === undefined || d.key === e.key) &&
|
||||
// `error` in an expected entry also matches `error [options]`
|
||||
(e.before === undefined || d.before === e.before || d.before.startsWith(`${e.before} `)) &&
|
||||
(e.after === undefined || d.after === e.after || d.after.startsWith(`${e.after} `));
|
||||
|
||||
const unexpected = [];
|
||||
const accounted = new Map();
|
||||
for (const d of diffs) {
|
||||
const hit = expected.find((e) => matches(d, e));
|
||||
if (hit) accounted.set(hit.reason, (accounted.get(hit.reason) ?? 0) + 1);
|
||||
else unexpected.push(d);
|
||||
}
|
||||
|
||||
for (const [reason, count] of [...accounted].sort((x, y) => y[1] - x[1])) {
|
||||
console.log(`expected ${String(count).padStart(5)} ${reason}`);
|
||||
}
|
||||
|
||||
if (unexpected.length === 0) {
|
||||
console.log(`\nOK: ${diffs.length} difference(s), all expected.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`\nUNEXPECTED (${unexpected.length}):`);
|
||||
const w = (s, n) => String(s).slice(0, n).padEnd(n);
|
||||
console.log(w('package', 38), w('file', 38), w('key', 44), w('before', 22), 'after');
|
||||
for (const d of unexpected.slice(0, 60)) {
|
||||
console.log(
|
||||
w(d.pkg, 38),
|
||||
w(d.file, 38),
|
||||
w(d.key, 44),
|
||||
w(d.before, 22),
|
||||
String(d.after).slice(0, 40),
|
||||
);
|
||||
}
|
||||
if (unexpected.length > 60) console.log(`... and ${unexpected.length - 60} more`);
|
||||
process.exit(1);
|
||||
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"pkg": "packages/frontend/editor-ui",
|
||||
"key": "(linted)",
|
||||
"before": "ignored",
|
||||
"after": "linted",
|
||||
"reason": "editor-ui test files return to ESLint: the oxlint ignorePatterns no longer leak into the ESLint config"
|
||||
},
|
||||
{
|
||||
"pkg": "packages/frontend/editor-ui",
|
||||
"rule": "n8n-local-rules/no-interpolation-in-regular-string",
|
||||
"reason": "off for test files: a Vue stub template keeps ${...} uninterpolated in a plain string"
|
||||
},
|
||||
{
|
||||
"pkg": "packages/frontend/editor-ui",
|
||||
"rule": "n8n-local-rules/no-unneeded-backticks",
|
||||
"reason": "off for test files: backticks inside a Vue stub template are Vue syntax"
|
||||
},
|
||||
{
|
||||
"pkg": "packages/frontend/editor-ui",
|
||||
"rule": "n8n-local-rules/no-uncaught-json-parse",
|
||||
"reason": "off for test files: a throw on self-declared fixtures is the signal the test wants"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,288 @@
|
||||
[
|
||||
{
|
||||
"rule": "@typescript-eslint/naming-convention",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 39 of 72 packages (12 keep a local error upgrade)"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-unsafe-assignment",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 31 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-unsafe-member-access",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 23 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-unsafe-call",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 20 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/require-await",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 18 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-unsafe-return",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 17 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-unsafe-argument",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 15 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/prefer-nullish-coalescing",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 13 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/unbound-method",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 12 packages"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-uncaught-json-parse",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 12 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-unsafe-function-type",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 11 packages"
|
||||
},
|
||||
{
|
||||
"rule": "import-x/no-default-export",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 11 packages"
|
||||
},
|
||||
{
|
||||
"rule": "@typescript-eslint/no-empty-object-type",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 10 packages"
|
||||
},
|
||||
{
|
||||
"rule": "import-x/order",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 10 packages"
|
||||
},
|
||||
{
|
||||
"rule": "no-empty",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "retired in base: downgraded in 10 packages"
|
||||
},
|
||||
|
||||
{
|
||||
"rule": "unicorn/filename-case",
|
||||
"pkg": "packages/",
|
||||
"before": "off",
|
||||
"after": "error",
|
||||
"reason": "base now sets kebab-case, which 35 packages had each set for themselves"
|
||||
},
|
||||
|
||||
{
|
||||
"rule": "n8n-local-rules/no-uncentralized-http",
|
||||
"pkg": "packages/",
|
||||
"before": "off",
|
||||
"after": "error",
|
||||
"reason": "package moved from base to the backend layer, which carries the network boundary"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-legacy-cipher-methods",
|
||||
"pkg": "packages/",
|
||||
"before": "off",
|
||||
"after": "error",
|
||||
"reason": "package moved from base to the backend layer, which carries the encryption boundary"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-misplaced-cipher-primitives",
|
||||
"pkg": "packages/",
|
||||
"before": "off",
|
||||
"after": "error",
|
||||
"reason": "package moved from base to the backend layer, which carries the encryption boundary"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-deployment-key-delete",
|
||||
"pkg": "packages/",
|
||||
"before": "off",
|
||||
"after": "error",
|
||||
"reason": "package moved from base to the backend layer, which carries the encryption boundary"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-encryption-guardrail-disable",
|
||||
"pkg": "packages/",
|
||||
"before": "off",
|
||||
"after": "error",
|
||||
"reason": "package moved from base to the backend layer, which carries the encryption boundary"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-uncentralized-http",
|
||||
"pkg": "packages/",
|
||||
"before": "error",
|
||||
"after": "error",
|
||||
"reason": "the allow list gained @n8n/backend-network, which is the factory itself"
|
||||
},
|
||||
|
||||
{
|
||||
"rule": "n8n-local-rules/no-uncentralized-http",
|
||||
"pkg": "packages/@n8n/utils",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "runtime-agnostic library moved to base: no Node runtime, no HTTP imports"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-legacy-cipher-methods",
|
||||
"pkg": "packages/@n8n/utils",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "runtime-agnostic library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-misplaced-cipher-primitives",
|
||||
"pkg": "packages/@n8n/utils",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "runtime-agnostic library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-deployment-key-delete",
|
||||
"pkg": "packages/@n8n/utils",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "runtime-agnostic library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-encryption-guardrail-disable",
|
||||
"pkg": "packages/@n8n/utils",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "runtime-agnostic library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-uncentralized-http",
|
||||
"pkg": "packages/@n8n/json-schema-to-zod",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "pure library moved to base: no Node APIs"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-legacy-cipher-methods",
|
||||
"pkg": "packages/@n8n/json-schema-to-zod",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "pure library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-misplaced-cipher-primitives",
|
||||
"pkg": "packages/@n8n/json-schema-to-zod",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "pure library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-deployment-key-delete",
|
||||
"pkg": "packages/@n8n/json-schema-to-zod",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "pure library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-encryption-guardrail-disable",
|
||||
"pkg": "packages/@n8n/json-schema-to-zod",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "pure library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-uncentralized-http",
|
||||
"pkg": "packages/@n8n/crdt",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "yjs-only library moved to base: no Node APIs"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-legacy-cipher-methods",
|
||||
"pkg": "packages/@n8n/crdt",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "yjs-only library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-misplaced-cipher-primitives",
|
||||
"pkg": "packages/@n8n/crdt",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "yjs-only library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-deployment-key-delete",
|
||||
"pkg": "packages/@n8n/crdt",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "yjs-only library moved to base"
|
||||
},
|
||||
{
|
||||
"rule": "n8n-local-rules/no-encryption-guardrail-disable",
|
||||
"pkg": "packages/@n8n/crdt",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "yjs-only library moved to base"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "(languageOptions)",
|
||||
"pkg": "packages/",
|
||||
"reason": "Node globals and ecmaVersion 2024 follow the base/backend move"
|
||||
},
|
||||
|
||||
{
|
||||
"rule": "n8n-nodes-base/node-filename-against-convention",
|
||||
"pkg": "packages/@n8n/nodes-langchain",
|
||||
"before": "error",
|
||||
"after": "off",
|
||||
"reason": "the nodes layer relaxes it for test files, which nodes-base already did and langchain did not"
|
||||
},
|
||||
{
|
||||
"key": "(plugins)",
|
||||
"pkg": "packages/nodes-base",
|
||||
"reason": "the nodes layer registers eslint-plugin-n8n-nodes-base once, so it is visible to test files under credentials/ too"
|
||||
},
|
||||
{
|
||||
"key": "(plugins)",
|
||||
"pkg": "packages/@n8n/nodes-langchain",
|
||||
"reason": "the nodes layer registers eslint-plugin-n8n-nodes-base once, so it is visible to test files under credentials/ too"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"_comment": "Which shared layer each package extends after the consolidation. Generated by hand from the audit in the migration plan; `verify.mjs` checks it against the tree.",
|
||||
"base": [
|
||||
"packages/@n8n/api-types",
|
||||
"packages/@n8n/chat-hub",
|
||||
"packages/@n8n/codemirror-lang",
|
||||
"packages/@n8n/codemirror-lang-sql",
|
||||
"packages/@n8n/constants",
|
||||
"packages/@n8n/crdt",
|
||||
"packages/@n8n/di",
|
||||
"packages/@n8n/errors",
|
||||
"packages/@n8n/extension-sdk",
|
||||
"packages/@n8n/json-schema-to-zod",
|
||||
"packages/@n8n/mcp-browser-extension",
|
||||
"packages/@n8n/permissions",
|
||||
"packages/@n8n/telemetry",
|
||||
"packages/@n8n/tournament",
|
||||
"packages/@n8n/utils",
|
||||
"packages/extensions/insights",
|
||||
"packages/workflow"
|
||||
],
|
||||
"backend": [
|
||||
"packages/@n8n/agents",
|
||||
"packages/@n8n/ai-node-sdk",
|
||||
"packages/@n8n/ai-utilities",
|
||||
"packages/@n8n/ai-workflow-builder.ee",
|
||||
"packages/@n8n/backend-common",
|
||||
"packages/@n8n/backend-network",
|
||||
"packages/@n8n/backend-test-utils",
|
||||
"packages/@n8n/benchmark",
|
||||
"packages/@n8n/blob-storage",
|
||||
"packages/@n8n/cli",
|
||||
"packages/@n8n/client-oauth2",
|
||||
"packages/@n8n/computer-use",
|
||||
"packages/@n8n/config",
|
||||
"packages/@n8n/db",
|
||||
"packages/@n8n/decorators",
|
||||
"packages/@n8n/engine",
|
||||
"packages/@n8n/eslint-plugin-community-nodes",
|
||||
"packages/@n8n/imap",
|
||||
"packages/@n8n/instance-ai",
|
||||
"packages/@n8n/local-gateway",
|
||||
"packages/@n8n/mcp-browser",
|
||||
"packages/@n8n/node-cli",
|
||||
"packages/@n8n/node-engine-compatibility",
|
||||
"packages/@n8n/scheduler",
|
||||
"packages/@n8n/syslog-client",
|
||||
"packages/@n8n/task-runner",
|
||||
"packages/@n8n/workflow-sdk",
|
||||
"packages/cli",
|
||||
"packages/core",
|
||||
"packages/node-dev",
|
||||
"packages/testing/code-health",
|
||||
"packages/testing/containers",
|
||||
"packages/testing/janitor",
|
||||
"packages/testing/playwright",
|
||||
"packages/testing/rules-engine",
|
||||
"packages/testing/test-impact"
|
||||
],
|
||||
"frontend": [
|
||||
"packages/@n8n/mcp-apps",
|
||||
"packages/frontend/@n8n/chat",
|
||||
"packages/frontend/@n8n/composables",
|
||||
"packages/frontend/@n8n/design-system",
|
||||
"packages/frontend/@n8n/frontend-constants",
|
||||
"packages/frontend/@n8n/frontend-module-sdk",
|
||||
"packages/frontend/@n8n/frontend-test-utils",
|
||||
"packages/frontend/@n8n/frontend-utils",
|
||||
"packages/frontend/@n8n/i18n",
|
||||
"packages/frontend/@n8n/rest-api-client",
|
||||
"packages/frontend/@n8n/stores",
|
||||
"packages/frontend/@n8n/storybook",
|
||||
"packages/frontend/editor-ui",
|
||||
"packages/modules/insights/frontend",
|
||||
"packages/modules/instance-registry/frontend",
|
||||
"packages/modules/otel/frontend"
|
||||
],
|
||||
"nodes": ["packages/nodes-base", "packages/@n8n/nodes-langchain"],
|
||||
"exempt": ["packages/frontend/@n8n/eslint-plugin-design-system"]
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Counts how many packages downgrade each rule, so the shared layers are built
|
||||
* from the tree rather than from an impression of it.
|
||||
*
|
||||
* A rule that most packages switch off is not a standard; it is a rule the repo
|
||||
* has already stopped enforcing, one config at a time. Those belong in the
|
||||
* shared layer as `off`, where the decision is visible and reversible. The rest
|
||||
* stay on, and the packages that cannot pass them keep a local, marked `off`.
|
||||
*
|
||||
* `--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 { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { readConfigBlocks } from './config-blocks.mjs';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, '..', '..');
|
||||
const layers = JSON.parse(readFileSync(join(here, 'layers.json'), 'utf8'));
|
||||
|
||||
const arg = (name, fallback) => {
|
||||
const i = process.argv.indexOf(name);
|
||||
return i === -1 ? fallback : process.argv[i + 1];
|
||||
};
|
||||
const threshold = Number(arg('--threshold', 10));
|
||||
const onlyLayer = arg('--layer', undefined);
|
||||
|
||||
const layerOf = new Map();
|
||||
for (const [layer, pkgs] of Object.entries(layers)) {
|
||||
if (layer.startsWith('_')) continue;
|
||||
for (const pkg of pkgs) layerOf.set(pkg, layer);
|
||||
}
|
||||
|
||||
/** rule -> { down:Set<pkg>, on:Set<pkg>, wide:Set<pkg>, scoped:Set<pkg>, opts:Set<pkg> } */
|
||||
const stats = new Map();
|
||||
const get = (id) => {
|
||||
if (!stats.has(id)) {
|
||||
stats.set(id, {
|
||||
down: new Set(),
|
||||
on: new Set(),
|
||||
wide: new Set(),
|
||||
scoped: new Set(),
|
||||
opts: new Set(),
|
||||
});
|
||||
}
|
||||
return stats.get(id);
|
||||
};
|
||||
|
||||
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'))) {
|
||||
for (const rule of block.rules) {
|
||||
const s = get(rule.id);
|
||||
(rule.severity === 'error' ? s.on : s.down).add(pkg);
|
||||
(block.scoped ? s.scoped : s.wide).add(pkg);
|
||||
if (rule.hasOptions) s.opts.add(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [...stats.entries()]
|
||||
.map(([id, s]) => ({
|
||||
id,
|
||||
down: s.down.size,
|
||||
on: s.on.size,
|
||||
wide: [...s.wide].filter((p) => s.down.has(p)).length,
|
||||
opts: s.opts.size,
|
||||
layers: new Set([...s.down].map((p) => layerOf.get(p))),
|
||||
}))
|
||||
.filter((r) => r.down > 0)
|
||||
.sort((a, b) => b.down - a.down || a.id.localeCompare(b.id));
|
||||
|
||||
const w = (s, n) => String(s).padEnd(n);
|
||||
console.log(`threshold: a rule downgraded in >= ${threshold} packages moves into base as "off"\n`);
|
||||
console.log(w('down', 6), w('on', 4), w('wide', 6), w('opts', 6), w('layers', 26), 'rule');
|
||||
for (const r of rows) {
|
||||
const verdict = r.down >= threshold ? ' <= BASE OFF' : '';
|
||||
console.log(
|
||||
w(r.down, 6),
|
||||
w(r.on, 4),
|
||||
w(r.wide, 6),
|
||||
w(r.opts, 6),
|
||||
w([...r.layers].sort().join(','), 26),
|
||||
r.id + verdict,
|
||||
);
|
||||
}
|
||||
|
||||
const cut = rows.filter((r) => r.down >= threshold);
|
||||
console.log(`\n${cut.length} rule(s) at or above the threshold:`);
|
||||
for (const r of cut) console.log(` ${r.id} (down ${r.down}, still on in ${r.on})`);
|
||||
console.log(
|
||||
`\n${rows.length - cut.length} rule(s) below it stay on and become local debt where downgraded.`,
|
||||
);
|
||||
|
||||
// rules that ONLY ever appear downgraded, anywhere: candidates for deletion
|
||||
const neverOn = rows.filter((r) => r.on === 0 && r.down >= 2);
|
||||
if (neverOn.length) {
|
||||
console.log(`\nnever enabled in any package (${neverOn.length}):`);
|
||||
for (const r of neverOn) console.log(` ${r.id} (down ${r.down})`);
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"packages/@n8n/agents": [
|
||||
"src/__tests__/agent-configuration.test.ts",
|
||||
"src/__tests__/integration/agent-vector-store-pinecone.test.ts",
|
||||
"src/evals/categorization.ts"
|
||||
],
|
||||
"packages/@n8n/ai-node-sdk": ["src/index.ts"],
|
||||
"packages/@n8n/ai-utilities": [
|
||||
"integration-tests/openai.test.ts",
|
||||
"src/__tests__/adapters/langchain-chat-model.test.ts",
|
||||
"src/adapters/langchain-chat-model.ts"
|
||||
],
|
||||
"packages/@n8n/ai-workflow-builder.ee": [
|
||||
"evaluations/__tests__/argument-parser.test.ts",
|
||||
"src/agents/planner.agent.ts",
|
||||
"src/test/ai-workflow-builder-agent.service.test.ts"
|
||||
],
|
||||
"packages/@n8n/api-types": [
|
||||
"src/__tests__/agent-builder-interactive.test.ts",
|
||||
"src/agent-builder-interactive.ts"
|
||||
],
|
||||
"packages/@n8n/backend-common": ["src/__tests__/cli-parser.test.ts", "src/cli-parser.ts"],
|
||||
"packages/@n8n/backend-network": [
|
||||
"src/dns/__tests__/dns-resolver.test.ts",
|
||||
"src/dns/dns-resolver.ts"
|
||||
],
|
||||
"packages/@n8n/backend-test-utils": ["src/db/projects.ts"],
|
||||
"packages/@n8n/benchmark": ["src/commands/list.ts"],
|
||||
"packages/@n8n/blob-storage": [
|
||||
"src/__tests__/azure-byte-store.test.ts",
|
||||
"src/azure-blob/azure-blob.config.ts"
|
||||
],
|
||||
"packages/@n8n/chat-hub": ["src/artifact.test.ts", "src/artifact.ts"],
|
||||
"packages/@n8n/cli": [
|
||||
"src/__tests__/client.test.ts",
|
||||
"src/base-command.ts",
|
||||
"src/client.ts",
|
||||
"src/commands/login.ts",
|
||||
"src/index.ts"
|
||||
],
|
||||
"packages/@n8n/client-oauth2": ["src/client-oauth2-token.ts", "test/client-oauth2.test.ts"],
|
||||
"packages/@n8n/codemirror-lang-sql": ["src/complete.ts", "test/complete.test.ts"],
|
||||
"packages/@n8n/codemirror-lang": [
|
||||
"src/expressions/grammar.terms.ts",
|
||||
"test/expressions/expressions.test.ts"
|
||||
],
|
||||
"packages/@n8n/computer-use": [
|
||||
"src/__mocks__/@inquirer/prompts.ts",
|
||||
"src/config-templates.test.ts"
|
||||
],
|
||||
"packages/@n8n/config": [
|
||||
"src/configs/__tests__/ai.config.test.ts",
|
||||
"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",
|
||||
"src/migrations/__tests__/migration-helpers.ts"
|
||||
],
|
||||
"packages/@n8n/decorators": [
|
||||
"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"
|
||||
],
|
||||
"packages/@n8n/extension-sdk": ["src/backend/define.ts"],
|
||||
"packages/@n8n/imap": ["src/connection-options.test.ts", "src/connection-options.ts"],
|
||||
"packages/@n8n/instance-ai": [
|
||||
"evaluations/__tests__/agent-report-rendering.test.ts",
|
||||
"evaluations/computer-use/report-html.ts",
|
||||
"src/__tests__/binary-checks.test.ts",
|
||||
"src/agent/aia-model-defaults.ts",
|
||||
"src/tools/__tests__/activity.tool.test.ts"
|
||||
],
|
||||
"packages/@n8n/json-schema-to-zod": ["src/index.ts", "test/json-schema-to-zod.test.ts"],
|
||||
"packages/@n8n/local-gateway": ["src/main/connect-origin.test.ts", "src/main/connect-origin.ts"],
|
||||
"packages/@n8n/mcp-apps": [
|
||||
"src/apps-manifest.ts",
|
||||
"src/apps/workflow-preview/App.vue",
|
||||
"src/apps/workflow-preview/composables/use-workflow-preview.test.ts"
|
||||
],
|
||||
"packages/@n8n/mcp-browser-extension": [
|
||||
"src/approvedHosts.test.ts",
|
||||
"src/approvedHosts.ts",
|
||||
"src/ui/App.vue"
|
||||
],
|
||||
"packages/@n8n/mcp-browser": [
|
||||
"src/__tests__/cdp-relay-attach.test.ts",
|
||||
"src/adapters/agent-browser.ts"
|
||||
],
|
||||
"packages/@n8n/node-cli": [
|
||||
"src/commands/build.test.ts",
|
||||
"src/commands/build.ts",
|
||||
"src/configs/eslint.ts",
|
||||
"src/modules.d.ts",
|
||||
"src/test-utils/command-tester.ts"
|
||||
],
|
||||
"packages/@n8n/node-engine-compatibility": [
|
||||
"src/__tests__/acceptance-fixtures.ts",
|
||||
"src/__tests__/engine-step-data-loader.test.ts",
|
||||
"src/constants.ts"
|
||||
],
|
||||
"packages/@n8n/nodes-langchain": [
|
||||
"credentials/AlibabaCloudApi.credentials.ts",
|
||||
"credentials/test/AnthropicApi.credentials.test.ts",
|
||||
"nodes/Guardrails/Guardrails.node.ts",
|
||||
"nodes/Guardrails/helpers/__tests__/model.test.ts",
|
||||
"nodes/mcp/McpClient/__test__/McpClient.node.test.ts"
|
||||
],
|
||||
"packages/@n8n/permissions": ["src/__tests__/schemas.test.ts", "src/constants.ee.ts"],
|
||||
"packages/@n8n/scheduler": ["src/__tests__/dependency-purity.test.ts", "src/core/clock-skew.ts"],
|
||||
"packages/@n8n/syslog-client": ["src/client.ts", "test/client.test.ts", "vite.config.ts"],
|
||||
"packages/@n8n/task-runner": [
|
||||
"src/__tests__/node-types.test.ts",
|
||||
"src/config/base-runner-config.ts"
|
||||
],
|
||||
"packages/@n8n/telemetry": ["src/__tests__/catalog.test.ts", "src/catalog.ts"],
|
||||
"packages/@n8n/tournament": ["src/Constants.ts", "test/ASTHooks.test.ts"],
|
||||
"packages/@n8n/utils": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/assert.ts",
|
||||
"src/client-assertion.test.ts"
|
||||
],
|
||||
"packages/@n8n/workflow-sdk": [
|
||||
"src/__tests__/fixtures-download.ts",
|
||||
"src/ast-interpreter/errors.ts",
|
||||
"src/ast-interpreter/interpreter.test.ts",
|
||||
"src/prompts/best-practices/guides/chatbot.ts",
|
||||
"src/prompts/node-guidance/parameter-guides/embedding-nodes.ts"
|
||||
],
|
||||
"packages/cli": [
|
||||
"src/__tests__/abstract-server.test.ts",
|
||||
"src/abstract-server.ts",
|
||||
"src/active-workflow-manager.ts",
|
||||
"src/binary-data/database.manager.ts",
|
||||
"src/commands/db/revert.ts",
|
||||
"src/commands/import/credentials.ts",
|
||||
"src/commands/ldap/reset.ts",
|
||||
"src/controllers/project.controller.ts",
|
||||
"src/credentials-helper.ts",
|
||||
"src/credentials/credential-connection-status-provider.interface.ts",
|
||||
"src/credentials/credential-connection-status-proxy.ts",
|
||||
"src/credentials/credential-dependency.service.ts",
|
||||
"src/credentials/credentials-finder.service.ts",
|
||||
"src/credentials/credentials.controller.ts",
|
||||
"src/credentials/credentials.service.ee.ts",
|
||||
"src/credentials/credentials.service.ts",
|
||||
"src/databases/repositories/__tests__/credentials.repository.test.ts",
|
||||
"src/evaluation.ee/evaluation-collection.service.ts",
|
||||
"src/evaluation.ee/test-runner/test-runner.service.ee.ts",
|
||||
"src/eventbus/message-event-bus/message-event-bus.ts",
|
||||
"src/events/relays/telemetry.event-relay.ts",
|
||||
"src/executions/execution-data/db-store.ts",
|
||||
"src/executions/execution-persistence.ts",
|
||||
"src/executions/execution-recovery.service.ts",
|
||||
"src/executions/execution.service.ts",
|
||||
"src/instance-settings-loader/loaders/log-streaming.instance-settings-loader.ts",
|
||||
"src/license.ts",
|
||||
"src/metrics/prometheus/instance-role-metrics.service.ts",
|
||||
"src/modules/agent-evals/agent-evals.module.ts",
|
||||
"src/modules/agents/agent-knowledge.service.ts",
|
||||
"src/modules/agents/agent-publish.service.ts",
|
||||
"src/modules/agents/agent-task.service.ts",
|
||||
"src/modules/agents/agents.service.ts",
|
||||
"src/modules/agents/builder/agents-builder.service.ts",
|
||||
"src/modules/agents/entities/agent-background-job.entity.ts",
|
||||
"src/modules/agents/instance-ai-builder-delegate.adapter.ts",
|
||||
"src/modules/agents/integrations/agent-channel-reconciler.service.ts",
|
||||
"src/modules/agents/integrations/leader-channel-relay.service.ts",
|
||||
"src/modules/agents/integrations/n8n-memory.ts",
|
||||
"src/modules/agents/integrations/platforms/discord-integration.ts",
|
||||
"src/modules/agents/repositories/agent-background-job.repository.ts",
|
||||
"src/modules/agents/tools/workflow-tool-workflow-resolver.ts",
|
||||
"src/modules/breaking-changes/breaking-changes.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-agent.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-credentials.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-title.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-tool.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-workflow.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.attachment.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.models.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.settings.service.ts",
|
||||
"src/modules/data-table/data-table-ddl.service.ts",
|
||||
"src/modules/data-table/data-table.service.ts",
|
||||
"src/modules/data-table/utils/sql-utils.ts",
|
||||
"src/modules/dynamic-credentials.ee/database/entities/credential-resolver.ts",
|
||||
"src/modules/dynamic-credentials.ee/database/repositories/credential-resolver.repository.ts",
|
||||
"src/modules/dynamic-credentials.ee/services/credential-connection-status.service.ts",
|
||||
"src/modules/dynamic-credentials.ee/services/credential-resolver-workflow.service.ts",
|
||||
"src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts",
|
||||
"src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts",
|
||||
"src/modules/favorites/favorites.service.ts",
|
||||
"src/modules/insights/insights-collection.service.ts",
|
||||
"src/modules/instance-ai/__tests__/agent-preview-handoff.test.ts",
|
||||
"src/modules/instance-ai/instance-ai.adapter.service.ts",
|
||||
"src/modules/instance-ai/mcp/instance-ai-mcp-registry.service.ts",
|
||||
"src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts",
|
||||
"src/modules/instance-ai/storage/typeorm-agent-memory.ts",
|
||||
"src/modules/instance-ai/storage/typeorm-observation-log-store.ts",
|
||||
"src/modules/instance-ai/suspended-thread-persistence.service.ts",
|
||||
"src/modules/instance-reporting/instance-reporting-scheduler.service.ts",
|
||||
"src/modules/log-streaming.ee/log-streaming-destination.service.ts",
|
||||
"src/modules/mcp/mcp-api-key.service.ts",
|
||||
"src/modules/mcp/mcp.settings.service.ts",
|
||||
"src/modules/oauth-jwe/oauth-jwe-key.service.ts",
|
||||
"src/modules/provisioning.ee/provisioning.service.ee.ts",
|
||||
"src/modules/provisioning.ee/role-mapping-rule.service.ee.ts",
|
||||
"src/modules/provisioning.ee/role-resolver.service.ee.ts",
|
||||
"src/modules/source-control.ee/source-control-context.factory.ts",
|
||||
"src/modules/source-control.ee/source-control-export.service.ee.ts",
|
||||
"src/modules/source-control.ee/source-control-import.service.ee.ts",
|
||||
"src/modules/source-control.ee/source-control-scoped.service.ts",
|
||||
"src/modules/source-control.ee/source-control-status.service.ee.ts",
|
||||
"src/modules/token-exchange/services/trusted-key.service.ts",
|
||||
"src/modules/workflow-index/workflow-dependency-query.service.ts",
|
||||
"src/permissions.ee/check-access.ts",
|
||||
"src/policy/policy-enforcement.service.ts",
|
||||
"src/public-api/v1/controllers/__tests__/workflows.public.controller.test.ts",
|
||||
"src/public-api/v1/handlers/audit/audit.handler.ts",
|
||||
"src/public-api/v1/handlers/community-packages/community-packages.handler.ts",
|
||||
"src/public-api/v1/handlers/credentials/credentials.handler.ts",
|
||||
"src/public-api/v1/handlers/data-tables/data-tables.columns.handler.ts",
|
||||
"src/public-api/v1/handlers/data-tables/data-tables.handler.ts",
|
||||
"src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts",
|
||||
"src/public-api/v1/handlers/discover/discover.handler.ts",
|
||||
"src/public-api/v1/handlers/evaluations/evaluations.handler.ts",
|
||||
"src/public-api/v1/handlers/folders/folders.handler.ts",
|
||||
"src/public-api/v1/handlers/insights/insights.handler.ts",
|
||||
"src/public-api/v1/handlers/ldap/ldap.handler.ts",
|
||||
"src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts",
|
||||
"src/public-api/v1/handlers/n8n-packages/n8n-packages.handler.ts",
|
||||
"src/public-api/v1/handlers/otel/otel.handler.ts",
|
||||
"src/public-api/v1/handlers/projects/projects.handler.ts",
|
||||
"src/public-api/v1/handlers/security-policy/security-policy.handler.ts",
|
||||
"src/public-api/v1/handlers/sso-oidc/sso-oidc.handler.ts",
|
||||
"src/public-api/v1/handlers/sso-saml/sso-saml.handler.ts",
|
||||
"src/public-api/v1/handlers/tags/tags.handler.ts",
|
||||
"src/public-api/v1/handlers/users/users.handler.ee.ts",
|
||||
"src/public-api/v1/handlers/variables/variables.handler.ts",
|
||||
"src/public-api/v1/handlers/workflows/workflows.handler.ts",
|
||||
"src/scaling/scaling.service.ts",
|
||||
"src/scheduling/durable-job-provisioner.ts",
|
||||
"src/scheduling/durable-scheduler.ts",
|
||||
"src/scheduling/poll-trigger-node/poll-trigger-job-registrar.ts",
|
||||
"src/scheduling/schedule-trigger-node/schedule-trigger-job-registrar.ts",
|
||||
"src/scheduling/system-tasks/system-task-runner.ts",
|
||||
"src/security-audit/risk-reporters/credentials-risk-reporter.ts",
|
||||
"src/security-audit/security-audit.repository.ts",
|
||||
"src/services/export.service.ts",
|
||||
"src/services/folder-finder.service.ts",
|
||||
"src/services/folder.service.ts",
|
||||
"src/services/hooks.service.ts",
|
||||
"src/services/import.service.ts",
|
||||
"src/services/ownership-transfer/ownership-transfer-handler.registry.ts",
|
||||
"src/services/ownership.service.ts",
|
||||
"src/services/project.service.ee.ts",
|
||||
"src/services/pruning/executions-pruning.service.ts",
|
||||
"src/services/pruning/workflow-history-compaction.service.ts",
|
||||
"src/services/public-api-key.service.ts",
|
||||
"src/services/role-cache.service.ts",
|
||||
"src/services/role.service.ts",
|
||||
"src/services/tag.service.ts",
|
||||
"src/services/user.service.ts",
|
||||
"src/services/workflow-statistics-rollup.service.ts",
|
||||
"src/wait-tracker.ts",
|
||||
"src/workflows/publication/workflow-publication-outbox-consumer.ts",
|
||||
"src/workflows/publication/workflow-publication-reconciler.service.ts",
|
||||
"src/workflows/workflow-creation.service.ts",
|
||||
"src/workflows/workflow-finder.service.ts",
|
||||
"src/workflows/workflow-history/workflow-history.service.ts",
|
||||
"src/workflows/workflow-sharing.service.ts",
|
||||
"src/workflows/workflow-validation.service.ts",
|
||||
"src/workflows/workflow.service.ee.ts",
|
||||
"src/workflows/workflow.service.ts",
|
||||
"src/workflows/workflows.controller.ts",
|
||||
"test/extend-expect.ts"
|
||||
],
|
||||
"packages/core": [
|
||||
"src/__tests__/credentials.test.ts",
|
||||
"src/binary-data/binary-data.config.ts",
|
||||
"test/helpers/constants.ts"
|
||||
],
|
||||
"packages/extensions/insights": [
|
||||
"src/backend/index.ts",
|
||||
"src/frontend/InsightsDashboard.vue",
|
||||
"src/frontend/index.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/chat": [
|
||||
"src/App.vue",
|
||||
"src/__stories__/App.stories.ts",
|
||||
"src/__tests__/Chat.spec.ts",
|
||||
"src/__tests__/plugins/chat.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/composables": [
|
||||
"src/__tests__/packageBoundary.test.ts",
|
||||
"src/injectionKeys.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/design-system": [
|
||||
"src/__tests__/render.ts",
|
||||
"src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts",
|
||||
"src/components/AskAssistantAvatar/AssistantAvatar.stories.ts",
|
||||
"src/components/AskAssistantAvatar/AssistantAvatar.vue",
|
||||
"src/components/N8nFormInput/validators.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/eslint-plugin-design-system": [
|
||||
"src/plugin.ts",
|
||||
"src/rules/require-teleported-tooltip-in-dropdown.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/frontend-constants": ["src/urls.ts", "src/views.test.ts", "src/views.ts"],
|
||||
"packages/frontend/@n8n/frontend-module-sdk": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/index.ts",
|
||||
"src/registries/commandRegistry.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/frontend-test-utils": ["src/__tests__/render.test.ts", "src/defaults.ts"],
|
||||
"packages/frontend/@n8n/frontend-utils": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/constants/sanitization.ts",
|
||||
"src/htmlUtils.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/i18n": ["src/__tests__/setup.ts", "src/index.test.ts", "src/index.ts"],
|
||||
"packages/frontend/@n8n/rest-api-client": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/api/__tests__/api-keys.test.ts",
|
||||
"src/api/ai-usage.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/stores": [
|
||||
"src/__tests__/metaTagConfig.test.ts",
|
||||
"src/cloudPlan.store.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/storybook": [".storybook/ThemedDocsContainer.ts"],
|
||||
"packages/frontend/editor-ui": [
|
||||
"src/Interface.ts",
|
||||
"src/__tests__/data/users.ts",
|
||||
"src/app/App.vue",
|
||||
"src/app/__tests__/modal-definition.test.ts",
|
||||
"src/app/constants/modals.ts",
|
||||
"src/app/stores/defaults/modals.ts",
|
||||
"src/app/stores/workflowDocument/types.ts",
|
||||
"src/app/stores/workflows.store.ts",
|
||||
"src/features/agents/__tests__/AgentAdvancedPanel.test.ts",
|
||||
"src/features/agents/channels/AgentChannelStandardEditView.vue",
|
||||
"src/features/ai/chatHub/__test__/data.ts",
|
||||
"src/features/settings/environments.ee/completions/variables.completions.test.ts",
|
||||
"src/features/shared/editors/components/CodeNodeEditor/CodeNodeEditor.vue",
|
||||
"src/features/shared/editors/plugins/codemirror/completions/__tests__/mock.ts",
|
||||
"src/features/shared/toolsConnection/ToolsConnectionModal.stories.ts"
|
||||
],
|
||||
"packages/modules/insights/frontend": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/chartjs.utils.ts",
|
||||
"src/components/InsightsDashboard.test.ts",
|
||||
"src/components/InsightsDashboard.vue"
|
||||
],
|
||||
"packages/modules/instance-registry/frontend": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/index.ts",
|
||||
"src/instance-registry.store.test.ts"
|
||||
],
|
||||
"packages/modules/otel/frontend": [
|
||||
"src/OtelSettingsRow.vue",
|
||||
"src/SettingsOpenTelemetryView.test.ts",
|
||||
"src/__tests__/setup.ts",
|
||||
"src/index.ts"
|
||||
],
|
||||
"packages/node-dev": ["src/Build.ts"],
|
||||
"packages/nodes-base": [
|
||||
"credentials/ActionNetworkApi.credentials.ts",
|
||||
"credentials/common/aws/s3-path-signing.test.ts",
|
||||
"credentials/test/AtlassianOAuth2Api.credentials.test.ts",
|
||||
"nodes/ActionNetwork/ActionNetwork.node.ts",
|
||||
"nodes/Aws/__tests__/AwsLambda.node.test.ts"
|
||||
],
|
||||
"packages/testing/code-health": ["src/cli.ts", "src/cli/arg-parser.test.ts"],
|
||||
"packages/testing/janitor": ["src/cli.test.ts", "src/cli.ts"],
|
||||
"packages/testing/playwright": ["coverage-options.test.ts"],
|
||||
"packages/testing/rules-engine": ["src/ast/ast-rule.ts", "src/ast/imports.test.ts"],
|
||||
"packages/testing/test-impact": ["src/changes.test.ts", "src/changes.ts"],
|
||||
"packages/workflow": [
|
||||
"src/augment-object.ts",
|
||||
"test/ExpressionExtensions/array-extensions.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Picks the files that `snapshot.mjs` asks ESLint about, and writes them to
|
||||
* `samples.json`.
|
||||
*
|
||||
* The sample list is committed and generated ONCE, from the pre-change tree, so
|
||||
* the before/after snapshots are taken over identical inputs. Regenerating it
|
||||
* mid-refactor invalidates the comparison.
|
||||
*
|
||||
* Per package we take one file of each shape that can resolve to a different
|
||||
* config (source, unit test, `__tests__`, `.vue`, story, and for the nodes
|
||||
* packages a credential and a node), plus one real file for every `files:` glob
|
||||
* literal found in the package's own config. That last part is what covers the
|
||||
* ratchet blocks — a scoped `off` in cli or editor-ui is invisible otherwise.
|
||||
*
|
||||
* `samples.json` is formatted by `pnpm format`, so run that after regenerating
|
||||
* it or the next person's format run shows up as an unrelated diff.
|
||||
*
|
||||
* ponytail: globs are read out of the config text with a regex, not by
|
||||
* executing the config, so a glob built at runtime from a variable is missed.
|
||||
* None exist today; `majority.mjs` walks the real objects and would surface one.
|
||||
*/
|
||||
import { globSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, '..', '..');
|
||||
|
||||
const IGNORE = ['**/node_modules/**', '**/dist/**', '**/.turbo/**', '**/coverage/**'];
|
||||
|
||||
const configs = globSync('packages/**/eslint.config.mjs', { cwd: root, exclude: IGNORE })
|
||||
.filter((p) => !p.includes('/template'))
|
||||
.sort();
|
||||
|
||||
const isFile = (pkgDir, p) => {
|
||||
try {
|
||||
return statSync(join(root, pkgDir, p)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/** First lintable file matching a glob inside a package, or undefined. */
|
||||
function pick(pkgDir, pattern) {
|
||||
const opts = { cwd: join(root, pkgDir), exclude: IGNORE };
|
||||
const hits = globSync(pattern, opts).sort();
|
||||
const file = hits.find((p) => LINTABLE.test(p) && isFile(pkgDir, p));
|
||||
if (file) return file;
|
||||
// `src/foo/**` also matches the directory itself, so look inside it
|
||||
for (const hit of hits) {
|
||||
if (isFile(pkgDir, hit)) continue;
|
||||
const nested = globSync(join(hit, '**/*'), opts)
|
||||
.sort()
|
||||
.find((p) => LINTABLE.test(p) && isFile(pkgDir, p));
|
||||
if (nested) return nested;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const LINTABLE = /\.(?:[cm]?tsx?|vue|[cm]?jsx?)$/;
|
||||
|
||||
const TEST_MARKER = /(^|\/)(test|tests|__tests__|__test__)(\/|$)|\.(test|spec|cy)\.[cm]?tsx?$/;
|
||||
|
||||
const SHAPES = [
|
||||
// a plain source file: the one that matters most
|
||||
['src/**/*.ts', (f) => !TEST_MARKER.test(f)],
|
||||
['**/*.test.ts', null],
|
||||
['**/__tests__/**/*.ts', null],
|
||||
['**/*.vue', null],
|
||||
['**/*.stories.ts', null],
|
||||
// the two nodes packages keep their rules in these trees, not in src/
|
||||
['credentials/*.ts', null],
|
||||
['nodes/**/*.node.ts', null],
|
||||
];
|
||||
|
||||
const samples = {};
|
||||
|
||||
for (const config of configs) {
|
||||
const pkgDir = dirname(config);
|
||||
const files = new Set();
|
||||
|
||||
for (const [pattern, extra] of SHAPES) {
|
||||
const hits = globSync(pattern, { cwd: join(root, pkgDir), exclude: IGNORE })
|
||||
.sort()
|
||||
.filter((p) => LINTABLE.test(p) && isFile(pkgDir, p));
|
||||
const hit = extra ? hits.find(extra) : hits[0];
|
||||
if (hit) files.add(hit);
|
||||
}
|
||||
|
||||
// one real file per `files:` glob literal in this package's config
|
||||
const text = readFileSync(join(root, config), 'utf8');
|
||||
for (const block of text.matchAll(/files:\s*\[([^\]]*)\]/g)) {
|
||||
for (const literal of block[1].matchAll(/'([^']+)'|"([^"]+)"/g)) {
|
||||
const glob = (literal[1] ?? literal[2]).replace(/^\.\//, '');
|
||||
const hit = pick(pkgDir, glob);
|
||||
if (hit) files.add(hit);
|
||||
}
|
||||
}
|
||||
|
||||
if (files.size === 0) continue;
|
||||
samples[pkgDir] = [...files].sort();
|
||||
}
|
||||
|
||||
const out = join(here, 'samples.json');
|
||||
writeFileSync(out, JSON.stringify(samples, null, '\t') + '\n');
|
||||
const total = Object.values(samples).reduce((n, f) => n + f.length, 0);
|
||||
console.log(`${relative(root, out)}: ${Object.keys(samples).length} packages, ${total} files`);
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Records what ESLint actually resolves for every file in `samples.json`, so a
|
||||
* config refactor can be proved behaviour-neutral instead of eyeballed.
|
||||
*
|
||||
* Run it once before the change and once after, then compare with `diff.mjs`.
|
||||
* `@n8n/eslint-config` is consumed from its built dist, so build it first:
|
||||
* pnpm turbo run build --filter=@n8n/eslint-config
|
||||
*
|
||||
* Severities are normalised so `warn` reads as `off`: every lint script runs
|
||||
* with `--quiet`, so a warning is not enforced and a promote/demote between the
|
||||
* two is not a behaviour change. Options are compared only where the rule is on.
|
||||
*
|
||||
* `CI`/`NODE_ENV` are pinned because some configs branch on them; without that
|
||||
* the snapshot would describe a developer laptop rather than the CI gate.
|
||||
*
|
||||
* ponytail: `--print-config` is one process per file (~300), which takes a few
|
||||
* minutes even pooled. It is the only output ESLint offers that reflects the
|
||||
* whole cascade, including plugin-injected blocks.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, '..', '..');
|
||||
|
||||
const outIndex = process.argv.indexOf('--out');
|
||||
if (outIndex === -1) {
|
||||
console.error('usage: snapshot.mjs --out <file.json> [--only <pkgDir>]');
|
||||
process.exit(2);
|
||||
}
|
||||
const outFile = process.argv[outIndex + 1];
|
||||
const onlyIndex = process.argv.indexOf('--only');
|
||||
const only = onlyIndex === -1 ? undefined : process.argv[onlyIndex + 1];
|
||||
|
||||
const samples = JSON.parse(readFileSync(join(here, 'samples.json'), 'utf8'));
|
||||
const env = { ...process.env, CI: 'true', NODE_ENV: 'production', FORCE_COLOR: '0' };
|
||||
|
||||
/** `warn` collapses into `off`; options are irrelevant once a rule is off. */
|
||||
function normalise(entry) {
|
||||
const value = Array.isArray(entry) ? entry : [entry];
|
||||
const raw = value[0];
|
||||
const severity = raw === 'error' || raw === 2 ? 2 : raw === 'warn' || raw === 1 ? 0 : 0;
|
||||
return severity === 0 ? [0] : [2, ...value.slice(1)];
|
||||
}
|
||||
|
||||
function printConfig(pkgDir, file) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('pnpm', ['exec', 'eslint', '--print-config', file], {
|
||||
cwd: join(root, pkgDir),
|
||||
env,
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (c) => (stdout += c));
|
||||
child.stderr.on('data', (c) => (stderr += c));
|
||||
child.on('close', (code) => {
|
||||
const start = stdout.indexOf('{');
|
||||
// ESLint prints `undefined` for a file its config ignores. Anything
|
||||
// else without a config is a failure, and must not be recorded as an
|
||||
// ignore: a broken install would otherwise read as "nothing to lint"
|
||||
// and a whole tree could silently drop out of the comparison.
|
||||
if (start === -1) {
|
||||
const ignored = code === 0 && stdout.trim() === 'undefined';
|
||||
if (ignored) return resolve({ ignored: true });
|
||||
return resolve({
|
||||
error: (stderr || stdout).trim().split('\n').slice(0, 3).join(' ').slice(0, 300),
|
||||
});
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(stdout.slice(start));
|
||||
} catch (e) {
|
||||
return resolve({ error: `unparseable --print-config output: ${String(e).slice(0, 120)}` });
|
||||
}
|
||||
const rules = {};
|
||||
for (const [id, entry] of Object.entries(parsed.rules ?? {})) {
|
||||
const n = normalise(entry);
|
||||
if (n[0] !== 0) rules[id] = n;
|
||||
}
|
||||
const lang = parsed.languageOptions ?? {};
|
||||
resolve({
|
||||
rules,
|
||||
plugins: Object.keys(parsed.plugins ?? {}).sort(),
|
||||
settings: parsed.settings ?? {},
|
||||
languageOptions: {
|
||||
ecmaVersion: lang.ecmaVersion,
|
||||
sourceType: lang.sourceType,
|
||||
globals: Object.keys(lang.globals ?? {}).sort(),
|
||||
parserOptions: lang.parserOptions ?? {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const jobs = [];
|
||||
for (const [pkgDir, files] of Object.entries(samples)) {
|
||||
if (only && pkgDir !== only) continue;
|
||||
for (const file of files) jobs.push([pkgDir, file]);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
let done = 0;
|
||||
const POOL = 6;
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: POOL }, async () => {
|
||||
for (;;) {
|
||||
const job = jobs.shift();
|
||||
if (!job) return;
|
||||
const [pkgDir, file] = job;
|
||||
result[`${pkgDir}|${file}`] = await printConfig(pkgDir, file);
|
||||
done++;
|
||||
if (done % 25 === 0) process.stderr.write(` ${done} files\n`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
mkdirSync(dirname(outFile), { recursive: true });
|
||||
writeFileSync(outFile, JSON.stringify(result, null, '\t') + '\n');
|
||||
const ignored = Object.values(result).filter((r) => r.ignored).length;
|
||||
const failed = Object.entries(result).filter(([, r]) => r.error);
|
||||
console.log(`${outFile}: ${Object.keys(result).length} entries, ${ignored} ignored`);
|
||||
if (failed.length) {
|
||||
console.error(`\n${failed.length} file(s) could not be resolved — the snapshot is not usable:`);
|
||||
for (const [key, r] of failed.slice(0, 10)) console.error(` ${key}\n ${r.error}`);
|
||||
if (failed.length > 10) console.error(` ... and ${failed.length - 10} more`);
|
||||
console.error('\nUsually a stale install. Run `pnpm install` on this branch and retry.');
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user