build: Update ESLint to v9 (#16639)

This commit is contained in:
Elias Meire
2025-06-27 10:42:47 +02:00
committed by GitHub
parent a99ccfffe1
commit 0775fd859e
176 changed files with 5417 additions and 4111 deletions

View File

@@ -1,498 +0,0 @@
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
const config = (module.exports = {
ignorePatterns: [
'node_modules/**',
'dist/**',
'tsup.config.ts',
// TODO: remove these
'*.js',
],
plugins: [
/**
* Plugin with lint rules for import/export syntax
* https://github.com/import-js/eslint-plugin-import
*/
'eslint-plugin-import',
/**
* @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript
* See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins
*/
'@typescript-eslint',
/*
* Plugin to allow specifying local ESLint rules.
* https://github.com/ivov/eslint-plugin-n8n-local-rules
*/
'eslint-plugin-n8n-local-rules',
/** https://github.com/sweepline/eslint-plugin-unused-imports */
'unused-imports',
/** https://github.com/sindresorhus/eslint-plugin-unicorn */
'eslint-plugin-unicorn',
/** https://github.com/wix-incubator/eslint-plugin-lodash */
'eslint-plugin-lodash',
],
extends: [
/**
* Config for typescript-eslint recommended ruleset (without type checking)
*
* https://github.com/typescript-eslint/typescript-eslint/blob/1c1b572c3000d72cfe665b7afbada0ec415e7855/packages/eslint-plugin/src/configs/recommended.ts
*/
'plugin:@typescript-eslint/recommended',
/**
* Config for typescript-eslint recommended ruleset (with type checking)
*
* https://github.com/typescript-eslint/typescript-eslint/blob/1c1b572c3000d72cfe665b7afbada0ec415e7855/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts
*/
'plugin:@typescript-eslint/recommended-requiring-type-checking',
/**
* Config for Airbnb style guide for TS, /base to remove React rules
*
* https://github.com/iamturns/eslint-config-airbnb-typescript
* https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base/rules
*/
'eslint-config-airbnb-typescript/base',
/**
* Config to disable ESLint rules covered by Prettier
*
* https://github.com/prettier/eslint-config-prettier
*/
'eslint-config-prettier',
],
rules: {
// ******************************************************************
// additions to base ruleset
// ******************************************************************
// ----------------------------------
// ESLint
// ----------------------------------
/**
* https://eslint.org/docs/rules/id-denylist
*/
'id-denylist': [
'error',
'err',
'cb',
'callback',
'any',
'Number',
'number',
'String',
'string',
'Boolean',
'boolean',
'Undefined',
'undefined',
],
/**
* https://eslint.org/docs/latest/rules/no-void
*/
'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
*/
'no-constant-binary-expression': 'error',
/**
* https://eslint.org/docs/latest/rules/sort-imports
*/
'sort-imports': 'off', // @TECH_DEBT: Enable, prefs to be decided - N8N-5821
// ----------------------------------
// @typescript-eslint
// ----------------------------------
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md
*/
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
/** https://typescript-eslint.io/rules/await-thenable/ */
'@typescript-eslint/await-thenable': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-ts-comment.md
*/
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': true }],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md
*/
'@typescript-eslint/ban-types': [
'error',
{
types: {
Object: {
message: 'Use object instead',
fixWith: 'object',
},
String: {
message: 'Use string instead',
fixWith: 'string',
},
Boolean: {
message: 'Use boolean instead',
fixWith: 'boolean',
},
Number: {
message: 'Use number instead',
fixWith: 'number',
},
Symbol: {
message: 'Use symbol instead',
fixWith: 'symbol',
},
Function: {
message: [
'The `Function` type accepts any function-like value.',
'It provides no type safety when calling the function, which can be a common source of bugs.',
'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.',
'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.',
].join('\n'),
},
},
extendDefaults: false,
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md
*/
'@typescript-eslint/consistent-type-assertions': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-imports.md
*/
'@typescript-eslint/consistent-type-imports': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
*/
'@typescript-eslint/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'semi',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
},
],
/**
* 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: '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/import-js/eslint-plugin-import/blob/HEAD/docs/rules/no-duplicates.md
*/
'import/no-duplicates': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-void-type.md
*/
'@typescript-eslint/no-invalid-void-type': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-promises.md
*/
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/v4.30.0/packages/eslint-plugin/docs/rules/no-floating-promises.md
*/
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/v4.33.0/packages/eslint-plugin/docs/rules/no-namespace.md
*/
'@typescript-eslint/no-namespace': 'off',
/**
* https://eslint.org/docs/1.0.0/rules/no-throw-literal
*/
'@typescript-eslint/no-throw-literal': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.md
*/
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md
*/
'@typescript-eslint/no-unnecessary-qualifier': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-expressions.md
*/
'@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
*/
'@typescript-eslint/prefer-optional-chain': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/promise-function-async.md
*/
'@typescript-eslint/promise-function-async': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/triple-slash-reference.md
*/
'@typescript-eslint/triple-slash-reference': 'off', // @TECH_DEBT: Enable, disallowing in all cases - N8N-5820
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/naming-convention.md
*/
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'import',
format: ['camelCase', 'PascalCase'],
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/return-await.md
*/
'@typescript-eslint/return-await': ['error', 'always'],
/**
* https://typescript-eslint.io/rules/explicit-member-accessibility/
*/
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
// ----------------------------------
// eslint-plugin-import
// ----------------------------------
/**
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md
*/
'import/no-cycle': ['error', { ignoreExternal: false, maxDepth: 3 }],
/**
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
*/
'import/no-default-export': 'error',
/**
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unresolved.md
*/
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
/**
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/order.md
*/
'import/order': [
'error',
{
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
groups: [['builtin', 'external'], 'internal', ['parent', 'index', 'sibling'], 'object'],
'newlines-between': 'always',
},
],
// ----------------------------------
// eslint-plugin-n8n-local-rules
// ----------------------------------
'n8n-local-rules/no-uncaught-json-parse': 'error',
'n8n-local-rules/no-json-parse-json-stringify': 'error',
'n8n-local-rules/no-unneeded-backticks': 'error',
'n8n-local-rules/no-interpolation-in-regular-string': 'error',
'n8n-local-rules/no-unused-param-in-catch-clause': 'error',
'n8n-local-rules/no-useless-catch-throw': 'error',
'n8n-local-rules/no-plain-errors': 'error',
// ******************************************************************
// overrides to base ruleset
// ******************************************************************
// ----------------------------------
// ESLint
// ----------------------------------
/**
* https://eslint.org/docs/rules/class-methods-use-this
*/
'class-methods-use-this': 'off',
/**
* https://eslint.org/docs/rules/eqeqeq
*/
eqeqeq: 'error',
/**
* https://eslint.org/docs/rules/no-plusplus
*/
'no-plusplus': 'off',
/**
* https://eslint.org/docs/rules/object-shorthand
*/
'object-shorthand': 'error',
/**
* https://eslint.org/docs/rules/prefer-const
*/
'prefer-const': 'error',
/**
* https://eslint.org/docs/rules/prefer-spread
*/
'prefer-spread': 'error',
// These are tuned off since we use `noUnusedLocals` and `noUnusedParameters` now
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
/**
* https://www.typescriptlang.org/docs/handbook/enums.html#const-enums
*/
'no-restricted-syntax': [
'error',
{
selector: 'TSEnumDeclaration:not([const=true])',
message:
'Do not declare raw enums as it leads to runtime overhead. Use const enum instead. See https://www.typescriptlang.org/docs/handbook/enums.html#const-enums',
},
],
// ----------------------------------
// import
// ----------------------------------
/**
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md
*/
'import/prefer-default-export': 'off',
// ----------------------------------
// no-unused-imports
// ----------------------------------
/**
* 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',
/** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unnecessary-await.md */
'unicorn/no-unnecessary-await': 'error',
/** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-promise-resolve-reject.md */
'unicorn/no-useless-promise-resolve-reject': 'error',
'lodash/path-style': ['error', 'as-needed'],
'lodash/import-scope': ['error', 'method'],
},
overrides: [
{
files: ['test/**/*.ts', '**/__tests__/*.ts', '**/*.cy.ts'],
rules: {
'n8n-local-rules/no-plain-errors': 'off',
'n8n-local-rules/no-skipped-tests':
process.env.NODE_ENV === 'development' ? 'warn' : 'error',
// TODO: Remove these
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/naming-convention': 'off',
'import/no-duplicates': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-loop-func': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-throw-literal': 'off',
'@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/no-var-requires': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/unbound-method': 'off',
'id-denylist': 'off',
'import/no-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'n8n-local-rules/no-uncaught-json-parse': 'off',
'prefer-const': 'off',
'prefer-spread': 'off',
},
},
],
});

View File

@@ -1,102 +0,0 @@
const isCI = process.env.CI === 'true';
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
plugins: ['vue'],
extends: ['plugin:vue/vue3-recommended', '@vue/typescript', './base'],
env: {
browser: true,
es6: true,
node: true,
},
ignorePatterns: ['**/*.js', '**/*.d.ts', 'vite.config.ts', '**/*.ts.snap'],
overrides: [
{
files: ['**/*.test.ts', '**/test/**/*.ts', '**/__tests__/**/*.ts', '**/*.stories.ts'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['**/*.vue'],
rules: {
'vue/no-deprecated-slot-attribute': 'error',
'vue/no-deprecated-slot-scope-attribute': 'error',
'vue/no-multiple-template-root': 'error',
'vue/v-slot-style': 'error',
'vue/no-unused-components': 'error',
'vue/multi-word-component-names': 'off',
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
},
],
'vue/no-reserved-component-names': [
'error',
{
disallowVueBuiltInComponents: true,
disallowVue3BuiltInComponents: false,
},
],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/attribute-hyphenation': ['error', 'always'],
'vue/define-emits-declaration': ['error', 'type-literal'],
'vue/require-macro-variable-name': [
'error',
{
defineProps: 'props',
defineEmits: 'emit',
defineSlots: 'slots',
useSlots: 'slots',
useAttrs: 'attrs',
},
],
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
'vue/no-v-html': 'error',
// TODO: remove these
'vue/no-mutating-props': 'warn',
'vue/no-side-effects-in-computed-properties': 'warn',
'vue/no-v-text-v-html-on-component': 'warn',
'vue/return-in-computed-property': 'warn',
},
},
],
rules: {
'no-console': 'warn',
'no-debugger': isCI ? 'error' : 'off',
semi: [2, 'always'],
'comma-dangle': ['error', 'always-multiline'],
'no-tabs': 0,
'no-labels': 0,
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'import/no-extraneous-dependencies': 'warn',
// TODO: fix these
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
// TODO: remove these
'n8n-local-rules/no-plain-errors': 'off',
},
};

View File

@@ -1,2 +0,0 @@
/** @type {import('jest').Config} */
module.exports = require('../../../jest.config');

View File

@@ -1,465 +0,0 @@
'use strict';
const path = require('path');
/**
* This file contains any locally defined ESLint rules. They are picked up by
* eslint-plugin-n8n-local-rules and exposed as 'n8n-local-rules/<rule-name>'.
*/
module.exports = {
/**
* A rule to detect calls to JSON.parse() that are not wrapped inside try/catch blocks.
*
* Valid:
* ```js
* try { JSON.parse(foo) } catch(err) { baz() }
* ```
*
* Invalid:
* ```js
* JSON.parse(foo)
* ```
*
* The pattern where an object is cloned with JSON.parse(JSON.stringify()) is allowed
* (abundant in the n8n codebase):
*
* Valid:
* ```js
* JSON.parse(JSON.stringify(foo))
* ```
*/
'no-uncaught-json-parse': {
meta: {
type: 'problem',
docs: {
description:
'Calls to `JSON.parse()` must be replaced with `jsonParse()` from `n8n-workflow` or surrounded with a try/catch block.',
recommended: 'error',
},
schema: [],
messages: {
noUncaughtJsonParse:
'Use `jsonParse()` from `n8n-workflow` or surround the `JSON.parse()` call with a try/catch block.',
},
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!isJsonParseCall(node)) {
return;
}
if (isJsonStringifyCall(node)) {
return;
}
if (context.getAncestors().find((node) => node.type === 'TryStatement') !== undefined) {
return;
}
// Found a JSON.parse() call not wrapped into a try/catch, so report it
context.report({
messageId: 'noUncaughtJsonParse',
node,
});
},
};
},
},
'no-json-parse-json-stringify': {
meta: {
type: 'problem',
docs: {
description:
'Calls to `JSON.parse(JSON.stringify(arg))` must be replaced with `deepCopy(arg)` from `n8n-workflow`.',
recommended: 'error',
},
messages: {
noJsonParseJsonStringify: 'Replace with `deepCopy({{ argText }})`',
},
fixable: 'code',
},
create(context) {
return {
CallExpression(node) {
if (isJsonParseCall(node) && isJsonStringifyCall(node)) {
const [callExpression] = node.arguments;
const { arguments: args } = callExpression;
if (!Array.isArray(args) || args.length !== 1) return;
const [arg] = args;
if (!arg) return;
const argText = context.getSourceCode().getText(arg);
context.report({
messageId: 'noJsonParseJsonStringify',
node,
data: { argText },
fix: (fixer) => fixer.replaceText(node, `deepCopy(${argText})`),
});
}
},
};
},
},
'no-unneeded-backticks': {
meta: {
type: 'problem',
docs: {
description:
'Template literal backticks may only be used for string interpolation or multiline strings.',
recommended: 'error',
},
messages: {
noUneededBackticks: 'Use single or double quotes, not backticks',
},
fixable: 'code',
},
create(context) {
return {
TemplateLiteral(node) {
if (node.expressions.length > 0) return;
if (node.quasis.every((q) => q.loc.start.line !== q.loc.end.line)) return;
node.quasis.forEach((q) => {
const escaped = q.value.raw.replace(/(?<!\\)'/g, "\\'");
context.report({
messageId: 'noUneededBackticks',
node,
fix: (fixer) => fixer.replaceText(q, `'${escaped}'`),
});
});
},
};
},
},
'no-unused-param-in-catch-clause': {
meta: {
type: 'problem',
docs: {
description: 'Unused param in catch clause must be omitted.',
recommended: 'error',
},
messages: {
removeUnusedParam: 'Remove unused param in catch clause',
},
fixable: 'code',
},
create(context) {
return {
CatchClause(node) {
if (node.param?.name?.startsWith('_')) {
const start = node.range[0] + 'catch '.length;
const end = node.param.range[1] + '()'.length;
context.report({
messageId: 'removeUnusedParam',
node,
fix: (fixer) => fixer.removeRange([start, end]),
});
}
},
};
},
},
'no-useless-catch-throw': {
meta: {
type: 'problem',
docs: {
description: 'Disallow `try-catch` blocks where the `catch` only contains a `throw error`.',
recommended: 'error',
},
messages: {
noUselessCatchThrow: 'Remove useless `catch` block.',
},
fixable: 'code',
},
create(context) {
return {
CatchClause(node) {
if (
node.body.body.length === 1 &&
node.body.body[0].type === 'ThrowStatement' &&
node.body.body[0].argument.type === 'Identifier' &&
node.body.body[0].argument.name === node.param.name
) {
context.report({
node,
messageId: 'noUselessCatchThrow',
fix(fixer) {
const tryStatement = node.parent;
const tryBlock = tryStatement.block;
const sourceCode = context.getSourceCode();
const tryBlockText = sourceCode.getText(tryBlock);
const tryBlockTextWithoutBraces = tryBlockText.slice(1, -1).trim();
const indentedTryBlockText = tryBlockTextWithoutBraces
.split('\n')
.map((line) => line.replace(/\t/, ''))
.join('\n');
return fixer.replaceText(tryStatement, indentedTryBlockText);
},
});
}
},
};
},
},
'no-skipped-tests': {
meta: {
type: 'problem',
docs: {
description: 'Tests must not be skipped.',
recommended: 'error',
},
messages: {
removeSkip: 'Remove `.skip()` call',
removeOnly: 'Remove `.only()` call',
removeXPrefix: 'Remove `x` prefix',
},
fixable: 'code',
},
create(context) {
const TESTING_FUNCTIONS = new Set(['test', 'it', 'describe']);
const SKIPPING_METHODS = new Set(['skip', 'only']);
const PREFIXED_TESTING_FUNCTIONS = new Set(['xtest', 'xit', 'xdescribe']);
const toMessageId = (s) => 'remove' + s.charAt(0).toUpperCase() + s.slice(1);
return {
MemberExpression(node) {
if (
node.object.type === 'Identifier' &&
TESTING_FUNCTIONS.has(node.object.name) &&
node.property.type === 'Identifier' &&
SKIPPING_METHODS.has(node.property.name)
) {
context.report({
messageId: toMessageId(node.property.name),
node,
fix: (fixer) => {
const [start, end] = node.property.range;
return fixer.removeRange([start - '.'.length, end]);
},
});
}
},
CallExpression(node) {
if (
node.callee.type === 'Identifier' &&
PREFIXED_TESTING_FUNCTIONS.has(node.callee.name)
) {
context.report({
messageId: 'removeXPrefix',
node,
fix: (fixer) => fixer.replaceText(node.callee, 'test'),
});
}
},
};
},
},
'no-interpolation-in-regular-string': {
meta: {
type: 'problem',
docs: {
description:
'String interpolation `${...}` requires backticks, not single or double quotes.',
recommended: 'error',
},
messages: {
useBackticks: 'Use backticks to interpolate',
},
fixable: 'code',
},
create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string') return;
if (/\$\{/.test(node.value)) {
context.report({
messageId: 'useBackticks',
node,
fix: (fixer) => fixer.replaceText(node, `\`${node.value}\``),
});
}
},
};
},
},
'no-plain-errors': {
meta: {
type: 'problem',
docs: {
description:
'Only `ApplicationError` (from the `workflow` package) or its child classes must be thrown. This ensures the error will be normalized when reported to Sentry, if applicable.',
recommended: 'error',
},
messages: {
useApplicationError:
'Throw an `ApplicationError` (from the `workflow` package) or its child classes.',
},
fixable: 'code',
},
create(context) {
return {
ThrowStatement(node) {
if (!node.argument) return;
const isNewError =
node.argument.type === 'NewExpression' && node.argument.callee.name === 'Error';
const isNewlessError =
node.argument.type === 'CallExpression' && node.argument.callee.name === 'Error';
if (isNewError || isNewlessError) {
return context.report({
messageId: 'useApplicationError',
node,
fix: (fixer) =>
fixer.replaceText(
node,
`throw new ApplicationError(${node.argument.arguments
.map((arg) => arg.raw)
.join(', ')})`,
),
});
}
},
};
},
},
'no-dynamic-import-template': {
meta: {
type: 'error',
docs: {
description:
'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.',
recommended: true,
},
},
create: function (context) {
return {
'AwaitExpression > ImportExpression TemplateLiteral'(node) {
const templateValue = node.quasis[0].value.cooked;
if (!templateValue?.startsWith('@/')) return;
context.report({
node,
message:
'Use relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.',
});
},
};
},
},
'misplaced-n8n-typeorm-import': {
meta: {
type: 'error',
docs: {
description: 'Ensure `@n8n/typeorm` is imported only from within the `@n8n/db` package.',
recommended: 'error',
},
messages: {
moveImport: 'Please move this import to `@n8n/db`.',
},
},
create(context) {
return {
ImportDeclaration(node) {
if (node.source.value === '@n8n/typeorm' && !context.getFilename().includes('@n8n/db')) {
context.report({ node, messageId: 'moveImport' });
}
},
};
},
},
'no-type-unsafe-event-emitter': {
meta: {
type: 'problem',
docs: {
description: 'Disallow extending from `EventEmitter`, which is not type-safe.',
recommended: 'error',
},
messages: {
noExtendsEventEmitter: 'Extend from the type-safe `TypedEmitter` class instead.',
},
},
create(context) {
return {
ClassDeclaration(node) {
if (
node.superClass &&
node.superClass.type === 'Identifier' &&
node.superClass.name === 'EventEmitter' &&
node.id.name !== 'TypedEmitter'
) {
context.report({
node: node.superClass,
messageId: 'noExtendsEventEmitter',
});
}
},
};
},
},
'no-untyped-config-class-field': {
meta: {
type: 'problem',
docs: {
description: 'Enforce explicit typing of config class fields',
recommended: 'error',
},
messages: {
noUntypedConfigClassField:
'Class field must have an explicit type annotation, e.g. `field: type = value`. See: https://github.com/n8n-io/n8n/pull/10433',
},
},
create(context) {
return {
PropertyDefinition(node) {
if (!node.typeAnnotation) {
context.report({ node: node.key, messageId: 'noUntypedConfigClassField' });
}
},
};
},
},
};
const isJsonParseCall = (node) =>
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'JSON' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'parse';
const isJsonStringifyCall = (node) => {
const parseArg = node.arguments?.[0];
return (
parseArg !== undefined &&
parseArg.type === 'CallExpression' &&
parseArg.callee.type === 'MemberExpression' &&
parseArg.callee.object.type === 'Identifier' &&
parseArg.callee.object.name === 'JSON' &&
parseArg.callee.property.type === 'Identifier' &&
parseArg.callee.property.name === 'stringify'
);
};

View File

@@ -1,83 +0,0 @@
'use strict';
const rules = require('./local-rules'),
RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester();
ruleTester.run('no-uncaught-json-parse', rules['no-uncaught-json-parse'], {
valid: [
{
code: 'try { JSON.parse(foo) } catch (e) {}',
},
{
code: 'JSON.parse(JSON.stringify(foo))',
},
],
invalid: [
{
code: 'JSON.parse(foo)',
errors: [{ messageId: 'noUncaughtJsonParse' }],
},
],
});
ruleTester.run('no-json-parse-json-stringify', rules['no-json-parse-json-stringify'], {
valid: [
{
code: 'deepCopy(foo)',
},
],
invalid: [
{
code: 'JSON.parse(JSON.stringify(foo))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo)',
},
{
code: 'JSON.parse(JSON.stringify(foo.bar))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo.bar)',
},
{
code: 'JSON.parse(JSON.stringify(foo.bar.baz))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo.bar.baz)',
},
{
code: 'JSON.parse(JSON.stringify(foo.bar[baz]))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo.bar[baz])',
},
],
});
ruleTester.run('no-useless-catch-throw', rules['no-useless-catch-throw'], {
valid: [
{
code: 'try { foo(); } catch (e) { console.error(e); }',
},
{
code: 'try { foo(); } catch (e) { throw new Error("Custom error"); }',
},
],
invalid: [
{
code: `
try {
// Some comment
if (foo) {
bar();
}
} catch (e) {
throw e;
}`,
errors: [{ messageId: 'noUselessCatchThrow' }],
output: `
// Some comment
if (foo) {
bar();
}`,
},
],
});

View File

@@ -1,11 +0,0 @@
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['./base'],
env: {
es6: true,
node: true,
},
};

View File

@@ -1,33 +1,57 @@
{
"name": "@n8n/eslint-config",
"private": true,
"name": "@n8n/eslint-config",
"type": "module",
"version": "0.0.1",
"exports": {
"./base": "./base.js",
"./frontend": "./frontend.js",
"./local-rules": "./local-rules.js",
"./node": "./node.js",
"./shared": "./shared.js"
},
"devDependencies": {
"@types/eslint": "^8.56.5",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vue/eslint-config-typescript": "^13.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-n8n-local-rules": "^1.0.0",
"eslint-plugin-unicorn": "^51.0.1",
"eslint-plugin-unused-imports": "^3.1.0",
"eslint-plugin-vue": "^9.23.0",
"vue-eslint-parser": "^9.4.2"
"./base": {
"default": "./dist/configs/base.js",
"types": "./dist/configs/base.d.js"
},
"./frontend": {
"default": "./dist/configs/frontend.js",
"types": "./dist/configs/frontend.d.js"
},
"./node": {
"default": "./dist/configs/node.js",
"types": "./dist/configs/node.d.js"
}
},
"scripts": {
"clean": "rimraf .turbo",
"test": "jest"
"build": "tsc",
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"format": "biome format --write .",
"format:check": "biome ci .",
"test": "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",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/rule-tester": "^8.35.0",
"@typescript-eslint/utils": "^8.35.0",
"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",
"tsup": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "^8.35.0",
"vitest": "catalog:"
},
"peerDependencies": {
"eslint": ">= 9"
}
}

View File

@@ -1,41 +0,0 @@
/**
* @type {(dir: string, mode: 'frontend' | undefined) => import('@types/eslint').ESLint.ConfigData}
*/
module.exports = (tsconfigRootDir, mode) => {
const isFrontend = mode === 'frontend';
const parser = isFrontend ? 'vue-eslint-parser' : '@typescript-eslint/parser';
const extraParserOptions = isFrontend
? {
extraFileExtensions: ['.vue'],
parser: {
ts: '@typescript-eslint/parser',
js: '@typescript-eslint/parser',
vue: 'vue-eslint-parser',
template: 'vue-eslint-parser',
},
}
: {};
const settings = {
'import/parsers': {
'@typescript-eslint/parser': ['.ts'],
},
'import/resolver': {
typescript: {
tsconfigRootDir,
project: './tsconfig.json',
},
},
};
return {
parser,
parserOptions: {
tsconfigRootDir,
project: ['./tsconfig.json'],
...extraParserOptions,
},
settings,
};
};

View File

@@ -0,0 +1,423 @@
import { globalIgnores } from 'eslint/config';
import eslint from '@eslint/js';
import importPlugin from 'eslint-plugin-import-x';
import typescriptPlugin from '@typescript-eslint/eslint-plugin';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import stylisticPlugin from '@stylistic/eslint-plugin';
import unicornPlugin from 'eslint-plugin-unicorn';
import lodashPlugin from 'eslint-plugin-lodash';
import { localRulesPlugin } from '../plugin.js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
// Slowest rules are disabled locally to improve performance in development
// They are enabled in CI to ensure code quality
const runAllRules = process.env.CI === 'true' || process.env.INCLUDE_SLOW_RULES === 'true';
export const baseConfig = tseslint.config(
globalIgnores([
'node_modules/**',
'dist/**',
'eslint.config.mjs',
'tsup.config.ts',
'jest.config.js',
'cypress.config.js',
]),
eslint.configs.recommended,
tseslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.typescript,
eslintConfigPrettier,
localRulesPlugin.configs.recommended,
{
plugins: {
'unused-imports': unusedImportsPlugin,
'@stylistic': stylisticPlugin,
lodash: lodashPlugin,
unicorn: unicornPlugin,
'@typescript-eslint': typescriptPlugin,
},
languageOptions: {
parserOptions: {
projectService: true,
},
},
settings: {
'import-x/resolver-next': [createTypeScriptImportResolver()],
},
rules: {
// ******************************************************************
// additions to base ruleset
// ******************************************************************
// ----------------------------------
// ESLint
// ----------------------------------
/**
* https://eslint.org/docs/rules/id-denylist
*/
'id-denylist': [
'error',
'err',
'cb',
'callback',
'any',
'Number',
'number',
'String',
'string',
'Boolean',
'boolean',
'Undefined',
'undefined',
],
/**
* https://eslint.org/docs/latest/rules/no-void
*/
'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
*/
'no-constant-binary-expression': 'error',
/**
* https://eslint.org/docs/latest/rules/sort-imports
*/
'sort-imports': 'off', // @TECH_DEBT: Enable, prefs to be decided - N8N-5821
// ----------------------------------
// @typescript-eslint
// ----------------------------------
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md
*/
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
/** https://typescript-eslint.io/rules/await-thenable/ */
'@typescript-eslint/await-thenable': runAllRules ? 'error' : 'off',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-ts-comment.md
*/
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': true }],
/**
* https://typescript-eslint.io/rules/no-restricted-types
*/
'@typescript-eslint/no-restricted-types': [
'error',
{
types: {
Object: {
message: 'Use object instead',
fixWith: 'object',
},
String: {
message: 'Use string instead',
fixWith: 'string',
},
Boolean: {
message: 'Use boolean instead',
fixWith: 'boolean',
},
Number: {
message: 'Use number instead',
fixWith: 'number',
},
Symbol: {
message: 'Use symbol instead',
fixWith: 'symbol',
},
Function: {
message: [
'The `Function` type accepts any function-like value.',
'It provides no type safety when calling the function, which can be a common source of bugs.',
'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.',
'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.',
].join('\n'),
},
},
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md
*/
'@typescript-eslint/consistent-type-assertions': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-imports.md
*/
'@typescript-eslint/consistent-type-imports': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
*/
'@stylistic/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'semi',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
},
],
// 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
*/
'@typescript-eslint/no-invalid-void-type': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-promises.md
*/
'@typescript-eslint/no-misused-promises': runAllRules
? ['error', { checksVoidReturn: false }]
: 'off',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/v4.30.0/packages/eslint-plugin/docs/rules/no-floating-promises.md
*/
'@typescript-eslint/no-floating-promises': runAllRules
? ['error', { ignoreVoid: true }]
: 'off',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/v4.33.0/packages/eslint-plugin/docs/rules/no-namespace.md
*/
'@typescript-eslint/no-namespace': 'off',
/**
* https://typescript-eslint.io/rules/only-throw-error/
*/
'@typescript-eslint/only-throw-error': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.md
*/
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md
*/
'@typescript-eslint/no-unnecessary-qualifier': runAllRules ? 'error' : 'off',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-expressions.md
*/
'@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
*/
'@typescript-eslint/prefer-optional-chain': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/promise-function-async.md
*/
'@typescript-eslint/promise-function-async': runAllRules ? 'error' : 'off',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/triple-slash-reference.md
*/
'@typescript-eslint/triple-slash-reference': 'off', // @TECH_DEBT: Enable, disallowing in all cases - N8N-5820
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/return-await.md
*/
'@typescript-eslint/return-await': ['error', 'always'],
/**
* https://typescript-eslint.io/rules/explicit-member-accessibility/
*/
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
// ----------------------------------
// eslint-plugin-import
// ----------------------------------
/**
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md
*/
'import-x/no-cycle': runAllRules ? ['error', { ignoreExternal: false, maxDepth: 3 }] : 'off',
/**
* 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
*/
'import-x/no-duplicates': 'error',
/**
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md
*/
'import-x/prefer-default-export': 'off',
// These rules are not needed as TypeScript handles them
'import-x/named': 'off',
'import-x/namespace': 'off',
'import-x/default': 'off',
'import-x/no-named-as-default-member': 'off',
'import-x/no-unresolved': 'off',
// ******************************************************************
// overrides to base ruleset
// ******************************************************************
// ----------------------------------
// ESLint
// ----------------------------------
/**
* https://eslint.org/docs/rules/class-methods-use-this
*/
'class-methods-use-this': 'off',
/**
* https://eslint.org/docs/rules/eqeqeq
*/
eqeqeq: 'error',
/**
* https://eslint.org/docs/rules/no-plusplus
*/
'no-plusplus': 'off',
/**
* https://eslint.org/docs/rules/object-shorthand
*/
'object-shorthand': 'error',
/**
* https://eslint.org/docs/rules/prefer-const
*/
'prefer-const': 'error',
/**
* https://eslint.org/docs/rules/prefer-spread
*/
'prefer-spread': 'error',
// These are tuned off since we use `noUnusedLocals` and `noUnusedParameters` now
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
/**
* https://www.typescriptlang.org/docs/handbook/enums.html#const-enums
*/
'no-restricted-syntax': [
'error',
{
selector: 'TSEnumDeclaration:not([const=true])',
message:
'Do not declare raw enums as it leads to runtime overhead. Use const enum instead. See https://www.typescriptlang.org/docs/handbook/enums.html#const-enums',
},
],
// ----------------------------------
// no-unused-imports
// ----------------------------------
/**
* 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',
/** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unnecessary-await.md */
'unicorn/no-unnecessary-await': 'error',
/** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-promise-resolve-reject.md */
'unicorn/no-useless-promise-resolve-reject': 'error',
'lodash/path-style': ['error', 'as-needed'],
'lodash/import-scope': ['error', 'method'],
},
},
{
// Rules for unit tests
files: ['test/**/*.ts', '**/__tests__/*.ts', '**/*.test.ts', '**/*.cy.ts'],
rules: {
'n8n-local-rules/no-plain-errors': 'off',
'n8n-local-rules/no-skipped-tests': process.env.NODE_ENV === 'development' ? 'warn' : 'error',
},
},
);

View File

@@ -0,0 +1,101 @@
import { globalIgnores } from 'eslint/config';
import tseslint from 'typescript-eslint';
import VuePlugin from 'eslint-plugin-vue';
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 };
export const frontendConfig = tseslint.config(
globalIgnores(['**/*.js', '**/*.d.ts', 'vite.config.ts', '**/*.ts.snap']),
baseConfig,
VuePlugin.configs['flat/recommended'],
{
rules: {
'no-console': 'warn',
'no-debugger': isCI ? 'error' : 'off',
semi: [2, 'always'],
'comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
},
},
{
files: ['**/*.ts'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: allGlobals,
parser: tseslint.parser,
parserOptions: { projectService: true, extraFileExtensions },
},
},
{
files: ['**/*.test.ts', '**/test/**/*.ts', '**/__tests__/**/*.ts', '**/*.stories.ts'],
rules: {
'import-x/no-extraneous-dependencies': 'warn',
},
},
{
files: ['**/*.vue'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: allGlobals,
parserOptions: {
parser: tseslint.parser,
extraFileExtensions,
},
},
rules: {
'vue/no-deprecated-slot-attribute': 'error',
'vue/no-deprecated-slot-scope-attribute': 'error',
'vue/no-multiple-template-root': 'error',
'vue/v-slot-style': 'error',
'vue/no-unused-components': 'error',
'vue/multi-word-component-names': 'off',
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
},
],
'vue/no-reserved-component-names': [
'error',
{
disallowVueBuiltInComponents: true,
disallowVue3BuiltInComponents: false,
},
],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/attribute-hyphenation': ['error', 'always'],
'vue/define-emits-declaration': ['error', 'type-literal'],
'vue/require-macro-variable-name': [
'error',
{
defineProps: 'props',
defineEmits: 'emit',
defineSlots: 'slots',
useSlots: 'slots',
useAttrs: 'attrs',
},
],
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
'vue/no-v-html': 'error',
// TODO: remove these
'vue/no-mutating-props': 'warn',
'vue/no-side-effects-in-computed-properties': 'warn',
'vue/no-v-text-v-html-on-component': 'warn',
'vue/return-in-computed-property': 'warn',
},
},
);

View File

@@ -0,0 +1,10 @@
import tseslint from 'typescript-eslint';
import globals from 'globals';
import { baseConfig } from './base.js';
export const nodeConfig = tseslint.config(baseConfig, {
languageOptions: {
ecmaVersion: 2024,
globals: globals.node,
},
});

View File

@@ -0,0 +1,30 @@
import type { ESLint } from 'eslint';
import { rules } from './rules/index.js';
const plugin = {
meta: {
name: 'n8n-local-rules',
},
configs: {},
// @ts-expect-error Rules type does not match for typescript-eslint and eslint
rules: rules as ESLint.Plugin['rules'],
} satisfies ESLint.Plugin;
export const localRulesPlugin = {
...plugin,
configs: {
recommended: {
plugins: {
'n8n-local-rules': plugin,
},
rules: {
'n8n-local-rules/no-uncaught-json-parse': 'error',
'n8n-local-rules/no-json-parse-json-stringify': 'error',
'n8n-local-rules/no-unneeded-backticks': 'error',
'n8n-local-rules/no-interpolation-in-regular-string': 'error',
'n8n-local-rules/no-unused-param-in-catch-clause': 'error',
'n8n-local-rules/no-useless-catch-throw': 'error',
},
},
},
} satisfies ESLint.Plugin;

View File

@@ -0,0 +1 @@
declare module 'eslint-plugin-lodash';

View File

@@ -0,0 +1,28 @@
import { NoJsonParseJsonStringifyRule } from './no-json-parse-json-stringify.js';
import { NoUncaughtJsonParseRule } from './no-uncaught-json-parse.js';
import { NoUnneededBackticksRule } from './no-unneeded-backticks.js';
import { NoUnusedParamInCatchClauseRule } from './no-unused-param-catch-clause.js';
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 { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.js';
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
export const rules = {
'no-uncaught-json-parse': NoUncaughtJsonParseRule,
'no-json-parse-json-stringify': NoJsonParseJsonStringifyRule,
'no-unneeded-backticks': NoUnneededBackticksRule,
'no-unused-param-in-catch-clause': NoUnusedParamInCatchClauseRule,
'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-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
} satisfies Record<string, AnyRuleModule>;

View File

@@ -0,0 +1,24 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const MisplacedN8nTypeormImportRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'Ensure `@n8n/typeorm` is imported only from within the `@n8n/db` package.',
},
messages: {
moveImport: 'Please move this import to `@n8n/db`.',
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
ImportDeclaration(node) {
if (node.source.value === '@n8n/typeorm' && !context.filename.includes('@n8n/db')) {
context.report({ node, messageId: 'moveImport' });
}
},
};
},
});

View File

@@ -0,0 +1,31 @@
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
export const NoDynamicImportTemplateRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description:
'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.',
},
schema: [],
messages: {
noDynamicImportTemplate:
'Use relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.',
},
},
defaultOptions: [],
create(context) {
return {
'AwaitExpression > ImportExpression TemplateLiteral'(node: TSESTree.TemplateLiteral) {
const templateValue = node.quasis[0].value.cooked;
if (!templateValue?.startsWith('@/')) return;
context.report({
node,
messageId: 'noDynamicImportTemplate',
});
},
};
},
});

View File

@@ -0,0 +1,31 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoInterpolationInRegularStringRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'String interpolation `${...}` requires backticks, not single or double quotes.',
},
messages: {
useBackticks: 'Use backticks to interpolate',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string') return;
if (/\$\{/.test(node.value)) {
context.report({
messageId: 'useBackticks',
node,
fix: (fixer) => fixer.replaceText(node, `\`${node.value}\``),
});
}
},
};
},
});

View File

@@ -0,0 +1,34 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { NoJsonParseJsonStringifyRule } from './no-json-parse-json-stringify.js';
const ruleTester = new RuleTester();
ruleTester.run('no-json-parse-json-stringify', NoJsonParseJsonStringifyRule, {
valid: [
{
code: 'deepCopy(foo)',
},
],
invalid: [
{
code: 'JSON.parse(JSON.stringify(foo))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo)',
},
{
code: 'JSON.parse(JSON.stringify(foo.bar))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo.bar)',
},
{
code: 'JSON.parse(JSON.stringify(foo.bar.baz))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo.bar.baz)',
},
{
code: 'JSON.parse(JSON.stringify(foo.bar[baz]))',
errors: [{ messageId: 'noJsonParseJsonStringify' }],
output: 'deepCopy(foo.bar[baz])',
},
],
});

View File

@@ -0,0 +1,48 @@
import { isJsonParseCall, isJsonStringifyCall } from '../utils/json.js';
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
export const NoJsonParseJsonStringifyRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description:
'Calls to `JSON.parse(JSON.stringify(arg))` must be replaced with `deepCopy(arg)` from `n8n-workflow`.',
},
schema: [],
messages: {
noJsonParseJsonStringify: 'Replace with `deepCopy({{ argText }})`',
},
fixable: 'code',
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (isJsonParseCall(node) && isJsonStringifyCall(node)) {
const [callExpression] = node.arguments;
if (callExpression.type !== TSESTree.AST_NODE_TYPES.CallExpression) {
return;
}
const { arguments: args } = callExpression;
if (!Array.isArray(args) || args.length !== 1) return;
const [arg] = args;
if (!arg) return;
const argText = context.sourceCode.getText(arg);
context.report({
messageId: 'noJsonParseJsonStringify',
node,
data: { argText },
fix: (fixer) => fixer.replaceText(node, `deepCopy(${argText})`),
});
}
},
};
},
});

View File

@@ -0,0 +1,49 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
export const NoPlainErrorsRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description:
'Only `ApplicationError` (from the `workflow` package) or its child classes must be thrown. This ensures the error will be normalized when reported to Sentry, if applicable.',
},
messages: {
useApplicationError:
'Throw an `ApplicationError` (from the `workflow` package) or its 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: 'useApplicationError',
node,
fix: (fixer) =>
fixer.replaceText(
node,
`throw new ApplicationError(${(node.argument as TSESTree.CallExpression).arguments
.map((arg) => context.sourceCode.getText(arg))
.join(', ')})`,
),
});
}
},
};
},
});

View File

@@ -0,0 +1,57 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoSkippedTestsRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'Tests must not be skipped.',
},
messages: {
removeSkip: 'Remove `.skip()` call',
removeOnly: 'Remove `.only()` call',
removeXPrefix: 'Remove `x` prefix',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
const TESTING_FUNCTIONS = new Set(['test', 'it', 'describe']);
const SKIPPING_METHODS = new Set(['skip', 'only']);
const PREFIXED_TESTING_FUNCTIONS = new Set(['xtest', 'xit', 'xdescribe']);
const toMessageId = (s: string) =>
('remove' + s.charAt(0).toUpperCase() + s.slice(1)) as
| 'removeSkip'
| 'removeOnly'
| 'removeXPrefix';
return {
MemberExpression(node) {
if (
node.object.type === 'Identifier' &&
TESTING_FUNCTIONS.has(node.object.name) &&
node.property.type === 'Identifier' &&
SKIPPING_METHODS.has(node.property.name)
) {
context.report({
messageId: toMessageId(node.property.name),
node,
fix: (fixer) => {
const [start, end] = node.property.range;
return fixer.removeRange([start - '.'.length, end]);
},
});
}
},
CallExpression(node) {
if (node.callee.type === 'Identifier' && PREFIXED_TESTING_FUNCTIONS.has(node.callee.name)) {
context.report({
messageId: 'removeXPrefix',
node,
fix: (fixer) => fixer.replaceText(node.callee, 'test'),
});
}
},
};
},
});

View File

@@ -0,0 +1,32 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoTypeUnsafeEventEmitterRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'Disallow extending from `EventEmitter`, which is not type-safe.',
},
messages: {
noExtendsEventEmitter: 'Extend from the type-safe `TypedEmitter` class instead.',
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
ClassDeclaration(node) {
if (
node.superClass &&
node.superClass.type === 'Identifier' &&
node.superClass.name === 'EventEmitter' &&
node.id?.name !== 'TypedEmitter'
) {
context.report({
node: node.superClass,
messageId: 'noExtendsEventEmitter',
});
}
},
};
},
});

View File

@@ -0,0 +1,21 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { NoUncaughtJsonParseRule } from './no-uncaught-json-parse.js';
const ruleTester = new RuleTester();
ruleTester.run('no-uncaught-json-parse', NoUncaughtJsonParseRule, {
valid: [
{
code: 'try { JSON.parse(foo) } catch (e) {}',
},
{
code: 'JSON.parse(JSON.stringify(foo))',
},
],
invalid: [
{
code: 'JSON.parse(foo)',
errors: [{ messageId: 'noUncaughtJsonParse' }],
},
],
});

View File

@@ -0,0 +1,44 @@
import { ESLintUtils } from '@typescript-eslint/utils';
import { isJsonParseCall, isJsonStringifyCall } from '../utils/json.js';
export const NoUncaughtJsonParseRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
hasSuggestions: true,
docs: {
description:
'Calls to `JSON.parse()` must be replaced with `jsonParse()` from `n8n-workflow` or surrounded with a try/catch block.',
},
schema: [],
messages: {
noUncaughtJsonParse:
'Use `jsonParse()` from `n8n-workflow` or surround the `JSON.parse()` call with a try/catch block.',
},
},
defaultOptions: [],
create({ report, sourceCode }) {
return {
CallExpression(node) {
if (!isJsonParseCall(node)) {
return;
}
if (isJsonStringifyCall(node)) {
return;
}
if (
sourceCode.getAncestors(node).find((node) => node.type === 'TryStatement') !== undefined
) {
return;
}
// Found a JSON.parse() call not wrapped into a try/catch, so report it
report({
messageId: 'noUncaughtJsonParse',
node,
});
},
};
},
});

View File

@@ -0,0 +1,35 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoUnneededBackticksRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description:
'Template literal backticks may only be used for string interpolation or multiline strings.',
},
messages: {
noUnneededBackticks: 'Use single or double quotes, not backticks',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
return {
TemplateLiteral(node) {
if (node.expressions.length > 0) return;
if (node.quasis.every((q) => q.loc.start.line !== q.loc.end.line)) return;
node.quasis.forEach((q) => {
const escaped = q.value.raw.replace(/(?<!\\)'/g, "\\'");
context.report({
messageId: 'noUnneededBackticks',
node,
fix: (fixer) => fixer.replaceText(q, `'${escaped}'`),
});
});
},
};
},
});

View File

@@ -0,0 +1,25 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoUntypedConfigClassFieldRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'Enforce explicit typing of config class fields',
},
messages: {
noUntypedConfigClassField:
'Class field must have an explicit type annotation, e.g. `field: type = value`. See: https://github.com/n8n-io/n8n/pull/10433',
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
PropertyDefinition(node) {
if (!node.typeAnnotation) {
context.report({ node: node.key, messageId: 'noUntypedConfigClassField' });
}
},
};
},
});

View File

@@ -0,0 +1,32 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoUnusedParamInCatchClauseRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'Unused param in catch clause must be omitted.',
},
messages: {
removeUnusedParam: 'Remove unused param in catch clause',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
return {
CatchClause(node) {
if (node.param?.type === 'Identifier' && node.param.name.startsWith('_')) {
const start = node.range[0] + 'catch '.length;
const end = node.param.range[1] + '()'.length;
context.report({
messageId: 'removeUnusedParam',
node,
fix: (fixer) => fixer.removeRange([start, end]),
});
}
},
};
},
});

View File

@@ -0,0 +1,34 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { NoUselessCatchThrowRule } from './no-useless-catch-throw.js';
const ruleTester = new RuleTester();
ruleTester.run('no-useless-catch-throw', NoUselessCatchThrowRule, {
valid: [
{
code: 'try { foo(); } catch (e) { console.error(e); }',
},
{
code: 'try { foo(); } catch (e) { throw new Error("Custom error"); }',
},
],
invalid: [
{
code: `
try {
// Some comment
if (foo) {
bar();
}
} catch (e) {
throw e;
}`,
errors: [{ messageId: 'noUselessCatchThrow' }],
output: `
// Some comment
if (foo) {
bar();
}`,
},
],
});

View File

@@ -0,0 +1,46 @@
import { ESLintUtils } from '@typescript-eslint/utils';
export const NoUselessCatchThrowRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description: 'Disallow `try-catch` blocks where the `catch` only contains a `throw error`.',
},
messages: {
noUselessCatchThrow: 'Remove useless `catch` block.',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
return {
CatchClause(node) {
if (
node.body.body.length === 1 &&
node.body.body[0].type === 'ThrowStatement' &&
node.body.body[0].argument.type === 'Identifier' &&
node.param?.type === 'Identifier' &&
node.body.body[0].argument.name === node.param.name
) {
context.report({
node,
messageId: 'noUselessCatchThrow',
fix(fixer) {
const tryStatement = node.parent;
const tryBlock = tryStatement.block;
const sourceCode = context.sourceCode;
const tryBlockText = sourceCode.getText(tryBlock);
const tryBlockTextWithoutBraces = tryBlockText.slice(1, -1).trim();
const indentedTryBlockText = tryBlockTextWithoutBraces
.split('\n')
.map((line) => line.replace(/\t/, ''))
.join('\n');
return fixer.replaceText(tryStatement, indentedTryBlockText);
},
});
}
},
};
},
});

View File

@@ -0,0 +1,21 @@
import type { TSESTree } from '@typescript-eslint/utils';
export const isJsonParseCall = (node: TSESTree.CallExpression) =>
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'JSON' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'parse';
export const isJsonStringifyCall = (node: TSESTree.CallExpression) => {
const parseArg = node.arguments?.[0];
return (
parseArg !== undefined &&
parseArg.type === 'CallExpression' &&
parseArg.callee.type === 'MemberExpression' &&
parseArg.callee.object.type === 'Identifier' &&
parseArg.callee.object.name === 'JSON' &&
parseArg.callee.property.type === 'Identifier' &&
parseArg.callee.property.name === 'stringify'
);
};

View File

@@ -0,0 +1,13 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"types": ["vitest/globals"],
"esModuleInterop": true,
"module": "NodeNext",
"moduleResolution": "nodenext"
},
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,4 @@
import { defineConfig, mergeConfig } from 'vite';
import { vitestConfig } from '@n8n/vitest-config/node';
export default mergeConfig(defineConfig({}), vitestConfig);