first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
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';
|
||||
|
||||
export const baseConfig = tseslint.config(
|
||||
globalIgnores([
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
'eslint.config.mjs',
|
||||
'tsup.config.ts',
|
||||
'jest.config.js',
|
||||
'vite.config.ts',
|
||||
'vitest.config.ts',
|
||||
]),
|
||||
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': '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://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',
|
||||
|
||||
'@typescript-eslint/consistent-type-exports': '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': ['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://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': '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/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': ['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
|
||||
*/
|
||||
'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': 'off',
|
||||
|
||||
// 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',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'n8n-local-rules/no-skipped-tests': process.env.NODE_ENV === 'development' ? 'warn' : 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,122 @@
|
||||
import { globalIgnores } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import VuePlugin from 'eslint-plugin-vue';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
||||
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',
|
||||
'vue/one-component-per-file': 'off',
|
||||
|
||||
// TODO: remove these
|
||||
'n8n-local-rules/no-internal-package-import': '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/no-undef-components': [
|
||||
'error',
|
||||
{
|
||||
ignorePatterns: [
|
||||
'RouterLink', // Vue Router global component
|
||||
'RouterView', // Vue Router global component
|
||||
'Teleport', // Vue 3 built-in
|
||||
'Transition', // Vue 3 built-in
|
||||
'TransitionGroup', // Vue 3 built-in
|
||||
'KeepAlive', // Vue 3 built-in
|
||||
'Suspense', // Vue 3 built-in
|
||||
],
|
||||
},
|
||||
],
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/component-name-in-template-casing': [
|
||||
'error',
|
||||
'PascalCase',
|
||||
{
|
||||
registeredComponentsOnly: false,
|
||||
},
|
||||
],
|
||||
'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',
|
||||
'n8n-local-rules/no-internal-package-import': 'warn',
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
);
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
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',
|
||||
'n8n-local-rules/no-internal-package-import': 'error',
|
||||
'n8n-local-rules/no-type-only-import-in-di': 'error',
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ESLint.Plugin;
|
||||
@@ -0,0 +1 @@
|
||||
declare module 'eslint-plugin-lodash';
|
||||
@@ -0,0 +1,40 @@
|
||||
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 { NoTopLevelRelativeImportsInBackendModuleRule } from './no-top-level-relative-imports-in-backend-module.js';
|
||||
import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-module.js';
|
||||
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';
|
||||
|
||||
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,
|
||||
'no-top-level-relative-imports-in-backend-module': NoTopLevelRelativeImportsInBackendModuleRule,
|
||||
'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,
|
||||
} satisfies Record<string, AnyRuleModule>;
|
||||
@@ -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' });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoArgumentSpreadRule } from './no-argument-spread.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-unbounded-argument-spread', NoArgumentSpreadRule, {
|
||||
valid: [
|
||||
{ code: 'fn(1, 2, 3)' },
|
||||
{ code: 'fn(...[1, 2, 3])' },
|
||||
{ code: 'new Foo(...[1, 2])' },
|
||||
{ code: 'fn.apply(null, deps)' },
|
||||
{ code: 'Reflect.construct(Foo, deps)' },
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
code: 'fn(...deps)',
|
||||
output: 'fn.apply(undefined, deps)',
|
||||
errors: [{ messageId: 'replaceWithApply' }],
|
||||
},
|
||||
{
|
||||
code: 'obj.fn(...deps)',
|
||||
output: 'obj.fn.apply(obj, deps)',
|
||||
errors: [{ messageId: 'replaceWithApply' }],
|
||||
},
|
||||
{
|
||||
code: 'instance = metadata.factory(...dependencies);',
|
||||
output: 'instance = metadata.factory.apply(metadata, dependencies);',
|
||||
errors: [{ messageId: 'replaceWithApply' }],
|
||||
},
|
||||
{
|
||||
code: 'new Foo(...deps)',
|
||||
output: 'Reflect.construct(Foo, deps)',
|
||||
errors: [{ messageId: 'replaceWithReflect' }],
|
||||
},
|
||||
{
|
||||
code: 'someFunction(a, ...deps)',
|
||||
output: null, // multiple args — no fix
|
||||
errors: [{ messageId: 'replaceWithApply' }],
|
||||
},
|
||||
{
|
||||
code: 'new Bar(a, ...deps)',
|
||||
output: null,
|
||||
errors: [{ messageId: 'replaceWithReflect' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoArgumentSpreadRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Avoid spreading potentially large arrays in function or constructor calls — can cause stack overflows. Use `.apply` or `Reflect.construct` instead.',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
noUnboundedSpread:
|
||||
'Avoid spreading an array in function or constructor calls unless known to be small.',
|
||||
replaceWithApply:
|
||||
'Replace `array.push(...largeArray)` with `array.push.apply(array, largeArray)` to avoid potential stack overflows.',
|
||||
replaceWithReflect:
|
||||
'Replace `new Constructor(...args)` with `Reflect.construct(Constructor, args)` to avoid potential stack overflows.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
for (const arg of node.arguments) {
|
||||
if (arg.type !== 'SpreadElement') continue;
|
||||
|
||||
const spreadArg = arg.argument;
|
||||
|
||||
// Allow spread of inline arrays
|
||||
if (spreadArg.type === 'ArrayExpression') return;
|
||||
|
||||
// Only autofix if it's the sole argument
|
||||
const canFix = node.arguments.length === 1;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'replaceWithApply',
|
||||
fix: canFix
|
||||
? (fixer) => {
|
||||
const source = context.sourceCode;
|
||||
|
||||
if (node.callee.type === 'MemberExpression') {
|
||||
// Preserve `this`
|
||||
const thisText = source.getText(node.callee.object);
|
||||
const calleeText = source.getText(node.callee);
|
||||
const argText = source.getText(spreadArg);
|
||||
return fixer.replaceText(node, `${calleeText}.apply(${thisText}, ${argText})`);
|
||||
} else {
|
||||
// Not a memberexpression, use undefined as thisArg
|
||||
const calleeText = source.getText(node.callee);
|
||||
const argText = source.getText(spreadArg);
|
||||
return fixer.replaceText(node, `${calleeText}.apply(undefined, ${argText})`);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
NewExpression(node) {
|
||||
for (const arg of node.arguments || []) {
|
||||
if (arg.type !== 'SpreadElement') continue;
|
||||
|
||||
const spreadArg = arg.argument;
|
||||
|
||||
if (spreadArg.type === 'ArrayExpression') return;
|
||||
|
||||
const canFix = node.arguments.length === 1;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'replaceWithReflect',
|
||||
fix: canFix
|
||||
? (fixer) => {
|
||||
const source = context.sourceCode;
|
||||
const ctorText = source.getText(node.callee);
|
||||
const argText = source.getText(spreadArg);
|
||||
return fixer.replaceText(node, `Reflect.construct(${ctorText}, ${argText})`);
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-module.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-constructor-in-backend-module', NoConstructorInBackendModuleRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
// initialization code
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
export class RegularClass {
|
||||
constructor() {
|
||||
// this is fine in regular classes
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
@SomeOtherDecorator()
|
||||
export class OtherModule {
|
||||
constructor() {
|
||||
// this is fine with other decorators
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
constructor() {
|
||||
// this should be removed
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'noConstructorInBackendModule' }],
|
||||
output: `
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
@BackendModule({ name: 'insights' })
|
||||
export class InsightsModule {
|
||||
constructor(private service: SomeService) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// other code
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'noConstructorInBackendModule' }],
|
||||
output: `
|
||||
@BackendModule({ name: 'insights' })
|
||||
export class InsightsModule {
|
||||
|
||||
|
||||
async init() {
|
||||
// other code
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoConstructorInBackendModuleRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'A class decorated with `@BackendModule` must not have a constructor. This ensures that module dependencies are loaded only when the module is used.',
|
||||
},
|
||||
messages: {
|
||||
noConstructorInBackendModule:
|
||||
'Remove the constructor from the class decorated with `@BackendModule`.',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
'ClassDeclaration MethodDefinition[kind="constructor"]'(node: TSESTree.MethodDefinition) {
|
||||
const classDeclaration = node.parent?.parent as TSESTree.ClassDeclaration;
|
||||
|
||||
const isBackendModule =
|
||||
classDeclaration.decorators?.some(
|
||||
(d) =>
|
||||
d.expression.type === 'CallExpression' &&
|
||||
d.expression.callee.type === 'Identifier' &&
|
||||
d.expression.callee.name === 'BackendModule',
|
||||
) ?? false;
|
||||
|
||||
if (isBackendModule) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noConstructorInBackendModule',
|
||||
fix: (fixer) => fixer.remove(node),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
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',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoInternalPackageImportRule } from './no-internal-package-import.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-internal-package-import', NoInternalPackageImportRule, {
|
||||
valid: [
|
||||
{ code: 'import { SomeDto } from "@n8n/api-types"' },
|
||||
{ code: 'import { Logger } from "@n8n/backend-common"' },
|
||||
{ code: 'import { NodeHelpers } from "@n8n/workflow"' },
|
||||
{ code: 'import lodash from "lodash"' },
|
||||
{ code: 'import { helper } from "./local-file"' },
|
||||
{ code: 'import { utils } from "../utils"' },
|
||||
{ code: 'import express from "express"' },
|
||||
{ code: 'import { something } from "@other-org/package/src/file"' },
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
code: 'import { UpdateDataTableDto } from "@n8n/api-types/src/dto/data-table/update-data-table.dto"',
|
||||
output: 'import { UpdateDataTableDto } from "@n8n/api-types"',
|
||||
errors: [{ messageId: 'noInternalPackageImport' }],
|
||||
},
|
||||
{
|
||||
code: 'import { helper } from "@n8n/backend-common/src/utils/helper"',
|
||||
output: 'import { helper } from "@n8n/backend-common"',
|
||||
errors: [{ messageId: 'noInternalPackageImport' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoInternalPackageImportRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow imports from internal package paths (e.g. `@n8n/pkg/src/...`).',
|
||||
},
|
||||
messages: {
|
||||
noInternalPackageImport:
|
||||
'Import from "{{ packageRoot }}", not from the internal `/src/` path.',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
const INTERNAL_IMPORT_REGEX = /^(?<packageRoot>@n8n\/[^/]+)\/src\//;
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (typeof node.source.type !== 'string') return;
|
||||
|
||||
const match = node.source.value.match(INTERNAL_IMPORT_REGEX);
|
||||
|
||||
if (!match?.groups) return;
|
||||
|
||||
const { packageRoot } = match.groups;
|
||||
|
||||
context.report({
|
||||
node: node.source,
|
||||
messageId: 'noInternalPackageImport',
|
||||
fix: (fixer) => fixer.replaceText(node.source, `"${packageRoot}"`),
|
||||
data: { packageRoot },
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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}\``),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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])',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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})`),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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(', ')})`,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoTopLevelRelativeImportsInBackendModuleRule } from './no-top-level-relative-imports-in-backend-module.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run(
|
||||
'no-top-level-relative-imports-in-backend-module',
|
||||
NoTopLevelRelativeImportsInBackendModuleRule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
import { Container } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
const { LocalService } = await import('./local.service');
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
import { Container } from '@n8n/di';
|
||||
import { LocalService } from './local.service';
|
||||
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
// code
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'placeInsideInit' }],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
import { helper } from './helper';
|
||||
import { config } from './config';
|
||||
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
// code
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'placeInsideInit' }, { messageId: 'placeInsideInit' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoTopLevelRelativeImportsInBackendModuleRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Relative imports in `.module.ts` files must be placed inside the `init` method. This ensures that module imports are loaded only when the module is used.',
|
||||
},
|
||||
messages: {
|
||||
placeInsideInit:
|
||||
"Place this relative import inside the `init` method, using `await import('./path')` syntax.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
'Program > ImportDeclaration'(node: TSESTree.ImportDeclaration) {
|
||||
if (node.source.value.startsWith('.')) {
|
||||
context.report({ node, messageId: 'placeInsideInit' });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoTypeOnlyImportInDiRule } from './no-type-only-import-in-di.js';
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
// Required to parse decorators and TS syntax
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('no-type-only-import-in-di', NoTypeOnlyImportInDiRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/provisioning.service.ts',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { type Publisher } from './publisher.service';
|
||||
|
||||
class RegularClass {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/regular-class.ts',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
|
||||
@Service()
|
||||
class SimpleService {}
|
||||
`,
|
||||
filename: '/test/simple.service.ts',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
|
||||
@Service()
|
||||
class ConfigService {
|
||||
constructor(private readonly port: number) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/config.service.ts',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
// Test individual specifier fix: import { type Publisher } -> import { Publisher }
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { type Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/provisioning.service.ts',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'publisher',
|
||||
typeName: 'Publisher',
|
||||
importStyle: '{ type Publisher }',
|
||||
suggestedImportStyle: '{ Publisher }',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Test declaration-level fix: import type { EventService } -> import { EventService }
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import type { EventService } from './event.service';
|
||||
|
||||
@Service()
|
||||
class NotificationService {
|
||||
constructor(private readonly eventService: EventService) {}
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { EventService } from './event.service';
|
||||
|
||||
@Service()
|
||||
class NotificationService {
|
||||
constructor(private readonly eventService: EventService) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/notification.service.ts',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'eventService',
|
||||
typeName: 'EventService',
|
||||
importStyle: '{ type EventService }',
|
||||
suggestedImportStyle: '{ EventService }',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Multiple invalid imports handled in one pass
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { type Publisher } from './publisher.service';
|
||||
import { type Logger } from './logger';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(
|
||||
private readonly publisher: Publisher,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { Publisher } from './publisher.service';
|
||||
import { Logger } from './logger';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(
|
||||
private readonly publisher: Publisher,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/provisioning.service.ts',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'publisher',
|
||||
typeName: 'Publisher',
|
||||
importStyle: '{ type Publisher }',
|
||||
suggestedImportStyle: '{ Publisher }',
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'logger',
|
||||
typeName: 'Logger',
|
||||
importStyle: '{ type Logger }',
|
||||
suggestedImportStyle: '{ Logger }',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Test with multiple spaces after 'type' keyword
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { type Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/provisioning.service.ts',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'publisher',
|
||||
typeName: 'Publisher',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Test multi-specifier import where only one is used in DI
|
||||
// Should convert to inline type syntax for other specifiers
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import type { IPublisher, Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { type IPublisher, Publisher } from './publisher.service';
|
||||
|
||||
@Service()
|
||||
class ProvisioningService {
|
||||
constructor(private readonly publisher: Publisher) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/provisioning.service.ts',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'publisher',
|
||||
typeName: 'Publisher',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Test multi-specifier import with multiple type-only specifiers
|
||||
code: `
|
||||
import { Service } from '@n8n/di';
|
||||
import type { ILogger, Logger, IConfig } from './types';
|
||||
|
||||
@Service()
|
||||
class MyService {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { Service } from '@n8n/di';
|
||||
import { type ILogger, Logger, type IConfig } from './types';
|
||||
|
||||
@Service()
|
||||
class MyService {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
}
|
||||
`,
|
||||
filename: '/test/my.service.ts',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: {
|
||||
paramName: 'logger',
|
||||
typeName: 'Logger',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoTypeOnlyImportInDiRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow type-only imports for constructor parameters in @Service() classes.',
|
||||
},
|
||||
fixable: 'code', // 1. Enable fixability
|
||||
messages: {
|
||||
noTypeOnlyImportInDi:
|
||||
'Constructor parameter "{{ paramName }}" uses type-only imported "{{ typeName }}" which is erased at runtime. Remove the `type` keyword to fix dependency injection.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
// Track the specific node that needs fixing
|
||||
const typeOnlyImports = new Map<
|
||||
string,
|
||||
{
|
||||
isTypeOnly: boolean;
|
||||
node: TSESTree.ImportDeclaration | TSESTree.ImportSpecifier;
|
||||
}
|
||||
>();
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
// Handle `import type { Foo } from 'bar'`
|
||||
if (node.importKind === 'type') {
|
||||
for (const specifier of node.specifiers) {
|
||||
if (
|
||||
specifier.type === 'ImportSpecifier' ||
|
||||
specifier.type === 'ImportDefaultSpecifier'
|
||||
) {
|
||||
typeOnlyImports.set(specifier.local.name, { isTypeOnly: true, node });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle `import { type Foo } from 'bar'`
|
||||
for (const specifier of node.specifiers) {
|
||||
if (specifier.type === 'ImportSpecifier') {
|
||||
const isSpecifierTypeOnly = specifier.importKind === 'type';
|
||||
typeOnlyImports.set(specifier.local.name, {
|
||||
isTypeOnly: isSpecifierTypeOnly,
|
||||
node: specifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ClassDeclaration(node) {
|
||||
const hasServiceDecorator = node.decorators?.some(
|
||||
(d) =>
|
||||
d.expression.type === 'CallExpression' &&
|
||||
d.expression.callee.type === 'Identifier' &&
|
||||
d.expression.callee.name === 'Service',
|
||||
);
|
||||
|
||||
if (!hasServiceDecorator) return;
|
||||
|
||||
const constructor = node.body.body.find(
|
||||
(m): m is TSESTree.MethodDefinition =>
|
||||
m.type === 'MethodDefinition' && m.kind === 'constructor',
|
||||
);
|
||||
|
||||
if (!constructor || constructor.value.type !== 'FunctionExpression') return;
|
||||
|
||||
for (const param of constructor.value.params) {
|
||||
const actualParam = param.type === 'TSParameterProperty' ? param.parameter : param;
|
||||
if (actualParam.type !== 'Identifier' || !actualParam.typeAnnotation) continue;
|
||||
|
||||
const typeNode = actualParam.typeAnnotation.typeAnnotation;
|
||||
if (typeNode.type === 'TSTypeReference' && typeNode.typeName.type === 'Identifier') {
|
||||
const typeName = typeNode.typeName.name;
|
||||
const importInfo = typeOnlyImports.get(typeName);
|
||||
|
||||
if (importInfo?.isTypeOnly) {
|
||||
context.report({
|
||||
node: actualParam,
|
||||
messageId: 'noTypeOnlyImportInDi',
|
||||
data: { paramName: actualParam.name, typeName },
|
||||
fix(fixer) {
|
||||
const targetNode = importInfo.node;
|
||||
|
||||
// Scenario A: import type { Foo, Bar } from 'bar'
|
||||
if (targetNode.type === 'ImportDeclaration') {
|
||||
const fixes = [];
|
||||
|
||||
// Find and remove the declaration-level 'type' keyword
|
||||
const typeToken = sourceCode.getFirstToken(
|
||||
targetNode,
|
||||
(t) => t.value === 'type',
|
||||
);
|
||||
if (!typeToken) return null;
|
||||
|
||||
const nextToken = sourceCode.getTokenAfter(typeToken);
|
||||
if (!nextToken) return null;
|
||||
|
||||
// Remove 'type' and any whitespace after it up to the next token
|
||||
fixes.push(fixer.removeRange([typeToken.range[0], nextToken.range[0]]));
|
||||
|
||||
// Add 'type' inline for all specifiers except the one being used in DI
|
||||
for (const specifier of targetNode.specifiers) {
|
||||
if (
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
specifier.local.name !== typeName
|
||||
) {
|
||||
// Add 'type ' before this specifier
|
||||
fixes.push(fixer.insertTextBefore(specifier, 'type '));
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
// Scenario B: import { type Foo } from 'bar'
|
||||
if (targetNode.type === 'ImportSpecifier') {
|
||||
const typeToken = sourceCode.getFirstToken(
|
||||
targetNode,
|
||||
(t) => t.value === 'type',
|
||||
);
|
||||
if (!typeToken) return null;
|
||||
|
||||
const nextToken = sourceCode.getTokenAfter(typeToken);
|
||||
if (!nextToken) return null;
|
||||
|
||||
// Remove 'type' and any whitespace after it up to the next token
|
||||
return fixer.removeRange([typeToken.range[0], nextToken.range[0]]);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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}'`),
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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' });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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]),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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'
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user