diff --git a/packages/@n8n/ai-node-sdk/eslint.config.mjs b/packages/@n8n/ai-node-sdk/eslint.config.mjs index 6e12998da83..e87981f10f9 100644 --- a/packages/@n8n/ai-node-sdk/eslint.config.mjs +++ b/packages/@n8n/ai-node-sdk/eslint.config.mjs @@ -1,8 +1,4 @@ import { defineConfig, globalIgnores } from 'eslint/config'; import { backendConfig } from '@n8n/eslint-config/backend'; -export default defineConfig(backendConfig, globalIgnores(['dist/**']), { - rules: { - '@typescript-eslint/no-explicit-any': 'warn', - }, -}); +export default defineConfig(backendConfig, globalIgnores(['dist/**'])); diff --git a/packages/@n8n/ai-node-sdk/oxlint.config.mts b/packages/@n8n/ai-node-sdk/oxlint.config.mts new file mode 100644 index 00000000000..07747b67c55 --- /dev/null +++ b/packages/@n8n/ai-node-sdk/oxlint.config.mts @@ -0,0 +1,8 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + ignorePatterns: ['dist/**'], +}); diff --git a/packages/@n8n/ai-node-sdk/package.json b/packages/@n8n/ai-node-sdk/package.json index 35448876a5b..aa34e9477f5 100644 --- a/packages/@n8n/ai-node-sdk/package.json +++ b/packages/@n8n/ai-node-sdk/package.json @@ -21,8 +21,8 @@ "build:unchecked": "tsc --build tsconfig.build.esm.json tsconfig.build.cjs.json --noCheck", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc --build tsconfig.build.esm.json tsconfig.build.cjs.json --watch", "test": "vitest run --passWithNoTests", "test:unit": "vitest run --passWithNoTests", @@ -35,8 +35,11 @@ "@n8n/ai-utilities": "workspace:*" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:" }, diff --git a/packages/@n8n/backend-common/eslint.config.mjs b/packages/@n8n/backend-common/eslint.config.mjs index 29bad4b454d..f542ab4b56b 100644 --- a/packages/@n8n/backend-common/eslint.config.mjs +++ b/packages/@n8n/backend-common/eslint.config.mjs @@ -1,21 +1,13 @@ import { defineConfig } from 'eslint/config'; import { backendConfig } from '@n8n/eslint-config/backend'; -export default defineConfig( - backendConfig, - { - rules: { - '@typescript-eslint/no-wrapper-object-types': 'warn', - }, +export default defineConfig(backendConfig, { + files: ['**/*.test.ts'], + rules: { + 'n8n-local-rules/no-uncaught-json-parse': 'warn', + '@typescript-eslint/no-unsafe-return': 'warn', + '@typescript-eslint/no-unsafe-assignment': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + '@typescript-eslint/unbound-method': 'warn', }, - { - files: ['**/*.test.ts'], - rules: { - 'n8n-local-rules/no-uncaught-json-parse': 'warn', - '@typescript-eslint/no-unsafe-return': 'warn', - '@typescript-eslint/no-unsafe-assignment': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - '@typescript-eslint/unbound-method': 'warn', - }, - }, -); +}); diff --git a/packages/@n8n/backend-common/oxlint.config.mts b/packages/@n8n/backend-common/oxlint.config.mts new file mode 100644 index 00000000000..ab247a45d33 --- /dev/null +++ b/packages/@n8n/backend-common/oxlint.config.mts @@ -0,0 +1,20 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + overrides: [ + { + files: ['**/*.test.ts'], + jsPlugins: ['@n8n/eslint-config/plugin'], + rules: { + 'n8n-local-rules/no-uncaught-json-parse': 'warn', + 'typescript/no-unsafe-return': 'warn', + 'typescript/no-unsafe-assignment': 'warn', + 'typescript/no-unsafe-argument': 'warn', + 'typescript/unbound-method': 'warn', + }, + }, + ], +}); diff --git a/packages/@n8n/backend-common/package.json b/packages/@n8n/backend-common/package.json index ee2914fda8a..8bc779724f6 100644 --- a/packages/@n8n/backend-common/package.json +++ b/packages/@n8n/backend-common/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -38,11 +38,14 @@ "yargs-parser": "21.1.1" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@types/stream-json": "1.7.8", "@types/yargs-parser": "21.0.0", "@n8n/vitest-config": "workspace:*", "@vitest/coverage-v8": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:", "vitest-mock-extended": "catalog:", diff --git a/packages/@n8n/backend-services/README.md b/packages/@n8n/backend-services/README.md index b1e1c1ceb7a..e621eb47cbd 100644 --- a/packages/@n8n/backend-services/README.md +++ b/packages/@n8n/backend-services/README.md @@ -39,7 +39,7 @@ lints and tests here without edits: | Concern | Setup | Mirrors | | --- | --- | --- | | TypeScript | `common.go` + `backend.go`, `lib` es2023, `strictFunctionTypes`, `strictPropertyInitialization` and `useUnknownInCatchVariables` off | `packages/cli/tsconfig.json` | -| Lint | `oxlint --type-aware`, then the ESLint guardrails pass. `eslint.config.mjs` is the policy twin that code-health and the guardrails read | `packages/cli/oxlint.config.mts`, `eslint.guardrails.config.mjs` | +| Lint | `oxlint --type-aware`. `eslint.config.mjs` is the policy twin that code-health reads | `packages/cli/oxlint.config.mts`, `eslint.config.mjs` | | Tests | Vitest with the decorators config, one fork per file, the same `N8N_USER_FOLDER` and `N8N_ENCRYPTION_KEY` setup | `packages/cli/vitest.config.base.ts`, `test/setup-test-folder.ts` | Two things do not carry over on purpose: diff --git a/packages/@n8n/backend-services/eslint.guardrails.config.mjs b/packages/@n8n/backend-services/eslint.guardrails.config.mjs deleted file mode 100644 index fcd0d8a5f90..00000000000 --- a/packages/@n8n/backend-services/eslint.guardrails.config.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { defineConfig, globalIgnores } from 'eslint/config'; -import { functionalGuardrailsConfig } from '@n8n/eslint-config/functional-guardrails'; -import { localRulesPlugin } from '@n8n/eslint-config/plugin'; -import packageConfig from './eslint.config.mjs'; - -const directivePluginConfigs = packageConfig.flatMap(({ plugins }) => { - const externalPlugins = Object.fromEntries( - Object.entries(plugins ?? {}).filter(([name]) => name !== 'n8n-local-rules'), - ); - - return Object.keys(externalPlugins).length === 0 ? [] : [{ plugins: externalPlugins }]; -}); - -const dependencyRuleConfigs = packageConfig.flatMap(({ files, ignores, settings, rules }) => { - const rule = rules?.['import-x/no-extraneous-dependencies']; - if (!rule) return []; - - return [ - { - ...(files ? { files } : {}), - ...(ignores ? { ignores } : {}), - ...(settings ? { settings } : {}), - rules: { 'import-x/no-extraneous-dependencies': rule }, - }, - ]; -}); - -export default defineConfig( - globalIgnores([ - 'dist/**', - 'coverage/**', - 'eslint.config.mjs', - 'eslint.guardrails.config.mjs', - 'oxlint.config.mts', - 'vitest.config.ts', - ]), - { - plugins: { 'n8n-local-rules': localRulesPlugin }, - }, - functionalGuardrailsConfig, - ...directivePluginConfigs, - ...dependencyRuleConfigs, -); diff --git a/packages/@n8n/backend-services/oxlint.config.mts b/packages/@n8n/backend-services/oxlint.config.mts index a6060889413..e3175003eba 100644 --- a/packages/@n8n/backend-services/oxlint.config.mts +++ b/packages/@n8n/backend-services/oxlint.config.mts @@ -4,10 +4,10 @@ import { defineConfig } from 'oxlint'; // Same rule set as packages/cli/oxlint.config.mts, so a file moved from cli lints the same way. export default defineConfig({ extends: [backendConfig], - ignorePatterns: ['coverage/**', 'eslint.guardrails.config.mjs'], + options: { typeAware: true }, + ignorePatterns: ['coverage/**'], rules: { 'n8n-local-rules/misplaced-n8n-typeorm-import': 'error', - // The narrow ESLint guardrail config owns no-unsealed-workflow-entity-write. 'n8n-local-rules/no-guardrail-disable': [ 'error', { diff --git a/packages/@n8n/backend-services/package.json b/packages/@n8n/backend-services/package.json index 29d33650f0d..3f8b7053ead 100644 --- a/packages/@n8n/backend-services/package.json +++ b/packages/@n8n/backend-services/package.json @@ -9,10 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "pnpm run lint:base && pnpm run lint:guardrails", - "lint:base": "oxlint --type-aware . --quiet", - "lint:fix": "oxlint --type-aware . --fix && pnpm run lint:guardrails", - "lint:guardrails": "eslint --config eslint.guardrails.config.mjs . --quiet", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run --passWithNoTests", "test:unit": "vitest run --passWithNoTests", @@ -30,7 +28,6 @@ "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@vitest/coverage-v8": "catalog:", - "eslint": "catalog:", "oxlint": "catalog:", "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", diff --git a/packages/@n8n/client-oauth2/oxlint.config.mts b/packages/@n8n/client-oauth2/oxlint.config.mts new file mode 100644 index 00000000000..1512d424642 --- /dev/null +++ b/packages/@n8n/client-oauth2/oxlint.config.mts @@ -0,0 +1,26 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + overrides: [ + { + files: ['**/*.test.ts'], + rules: { + 'id-denylist': 'warn', + 'typescript/no-unsafe-return': 'warn', + 'typescript/no-unsafe-call': 'warn', + 'typescript/no-unsafe-member-access': 'warn', + 'typescript/no-unsafe-assignment': 'warn', + }, + }, + { + files: ['src/client-oauth2.ts'], + jsPlugins: ['@n8n/eslint-config/plugin'], + rules: { + 'n8n-local-rules/no-uncentralized-http': 'off', + }, + }, + ], +}); diff --git a/packages/@n8n/client-oauth2/package.json b/packages/@n8n/client-oauth2/package.json index d12999b7ec7..77125e2f19f 100644 --- a/packages/@n8n/client-oauth2/package.json +++ b/packages/@n8n/client-oauth2/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write src test", "format:check": "biome ci src test", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -28,10 +28,13 @@ "axios": "catalog:" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@vitest/coverage-v8": "catalog:", "https-proxy-agent": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vite": "catalog:", "vitest": "catalog:", diff --git a/packages/@n8n/computer-use/oxlint.config.mts b/packages/@n8n/computer-use/oxlint.config.mts new file mode 100644 index 00000000000..b50c92512db --- /dev/null +++ b/packages/@n8n/computer-use/oxlint.config.mts @@ -0,0 +1,7 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, +}); diff --git a/packages/@n8n/computer-use/package.json b/packages/@n8n/computer-use/package.json index abfc9aca211..b0aa3211bcc 100644 --- a/packages/@n8n/computer-use/package.json +++ b/packages/@n8n/computer-use/package.json @@ -16,8 +16,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write src", "format:check": "biome ci src", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -55,12 +55,15 @@ "zod-to-json-schema": "catalog:" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@types/mime-types": "catalog:", "@types/node": "catalog:", "@types/yargs-parser": "21.0.0", "@vitest/coverage-v8": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vite": "catalog:", "vitest": "catalog:" diff --git a/packages/@n8n/config/oxlint.config.mts b/packages/@n8n/config/oxlint.config.mts new file mode 100644 index 00000000000..b1a6fc869e0 --- /dev/null +++ b/packages/@n8n/config/oxlint.config.mts @@ -0,0 +1,16 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + overrides: [ + { + files: ['**/*.config.ts'], + jsPlugins: ['@n8n/eslint-config/plugin'], + rules: { + 'n8n-local-rules/no-untyped-config-class-field': 'error', + }, + }, + ], +}); diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index 06aab8acd55..bf9b0bce237 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write src test", "format:check": "biome ci src test", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -29,9 +29,12 @@ "zod": "catalog:" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@vitest/coverage-v8": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vite": "catalog:", "vitest": "catalog:" diff --git a/packages/@n8n/decorators/eslint.config.mjs b/packages/@n8n/decorators/eslint.config.mjs index b6bbfc0600b..dc55341d028 100644 --- a/packages/@n8n/decorators/eslint.config.mjs +++ b/packages/@n8n/decorators/eslint.config.mjs @@ -5,10 +5,15 @@ export default defineConfig( backendConfig, { rules: { - '@typescript-eslint/no-base-to-string': 'warn', 'import-x/export': 'warn', }, }, + { + files: ['src/redactable.ts'], + rules: { + '@typescript-eslint/no-base-to-string': 'warn', + }, + }, { files: ['**/*.test.ts'], rules: { diff --git a/packages/@n8n/decorators/oxlint.config.mts b/packages/@n8n/decorators/oxlint.config.mts new file mode 100644 index 00000000000..c8f0da68419 --- /dev/null +++ b/packages/@n8n/decorators/oxlint.config.mts @@ -0,0 +1,24 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + overrides: [ + { + files: ['src/redactable.ts'], + rules: { + 'typescript/no-base-to-string': 'warn', + }, + }, + { + files: ['**/*.test.ts'], + rules: { + 'no-unused-expressions': 'warn', + 'typescript/no-unsafe-assignment': 'warn', + 'typescript/unbound-method': 'warn', + 'import/no-duplicates': 'warn', + }, + }, + ], +}); diff --git a/packages/@n8n/decorators/package.json b/packages/@n8n/decorators/package.json index e02baa698ff..88b48fe0246 100644 --- a/packages/@n8n/decorators/package.json +++ b/packages/@n8n/decorators/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -35,10 +35,13 @@ "dist/**/*" ], "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@types/express": "catalog:", "@types/lodash": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:", "zod": "catalog:" diff --git a/packages/@n8n/eslint-config/src/configs/base.ts b/packages/@n8n/eslint-config/src/configs/base.ts index fcfb89384dd..8a6fb241ee0 100644 --- a/packages/@n8n/eslint-config/src/configs/base.ts +++ b/packages/@n8n/eslint-config/src/configs/base.ts @@ -334,18 +334,6 @@ export const baseConfig = tseslint.config( '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 // ---------------------------------- diff --git a/packages/@n8n/eslint-config/src/plugin.ts b/packages/@n8n/eslint-config/src/plugin.ts index 785572340b8..13573040160 100644 --- a/packages/@n8n/eslint-config/src/plugin.ts +++ b/packages/@n8n/eslint-config/src/plugin.ts @@ -31,6 +31,7 @@ export const localRulesPlugin = { 'n8n-local-rules/no-type-only-import-in-di': 'error', 'n8n-local-rules/no-aws-credential-discovery-imports': 'error', 'n8n-local-rules/no-application-error': 'error', + 'n8n-local-rules/no-raw-enum': 'error', 'n8n-local-rules/no-dynamic-regexp': 'warn', 'n8n-local-rules/no-restricted-sleep-definition': 'error', 'n8n-local-rules/no-restricted-sleep-import': 'error', diff --git a/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/eslint.config.mjs b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/eslint.config.mjs new file mode 100644 index 00000000000..c92ef61ff41 --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/eslint.config.mjs @@ -0,0 +1,9 @@ +import { localRulesPlugin } from '@n8n/eslint-config/plugin'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + files: ['**/*.ts'], + languageOptions: { parser: tseslint.parser }, + plugins: { 'n8n-local-rules': localRulesPlugin }, + rules: { 'n8n-local-rules/no-raw-enum': 'error' }, +}); diff --git a/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/invalid.ts b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/invalid.ts new file mode 100644 index 00000000000..a4172db570b --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/invalid.ts @@ -0,0 +1,7 @@ +export enum NumericStatus { + Ready, +} + +export enum StringStatus { + Ready = 'ready', +} diff --git a/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/oxlint.config.mts b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/oxlint.config.mts new file mode 100644 index 00000000000..4b110474059 --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/oxlint.config.mts @@ -0,0 +1,5 @@ +export default { + categories: { correctness: 'off' }, + jsPlugins: ['@n8n/eslint-config/plugin'], + rules: { 'n8n-local-rules/no-raw-enum': 'error' }, +}; diff --git a/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/valid.ts b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/valid.ts new file mode 100644 index 00000000000..dceccd15271 --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/valid.ts @@ -0,0 +1,5 @@ +const enum Status { + Ready, +} + +export const status = Status.Ready; diff --git a/packages/@n8n/eslint-config/src/rules/index.ts b/packages/@n8n/eslint-config/src/rules/index.ts index 923b82624ed..506d4894685 100644 --- a/packages/@n8n/eslint-config/src/rules/index.ts +++ b/packages/@n8n/eslint-config/src/rules/index.ts @@ -32,6 +32,7 @@ import { NoUnsealedCredentialsEntityWriteRule } from './no-unsealed-credentials- import { RequireEscapedQueryValuesRule } from './require-escaped-query-values.js'; import { NoOnLeaderTakeoverRule } from './no-on-leader-takeover.js'; import { NoEncryptionGuardrailDisableRule } from './no-encryption-guardrail-disable.js'; +import { NoRawEnumRule } from './no-raw-enum.js'; export const rules = { 'no-uncaught-json-parse': NoUncaughtJsonParseRule, @@ -67,4 +68,5 @@ export const rules = { 'require-escaped-query-values': RequireEscapedQueryValuesRule, 'no-on-leader-takeover': NoOnLeaderTakeoverRule, 'no-encryption-guardrail-disable': NoEncryptionGuardrailDisableRule, + 'no-raw-enum': NoRawEnumRule, } satisfies Record; diff --git a/packages/@n8n/eslint-config/src/rules/no-raw-enum.test.ts b/packages/@n8n/eslint-config/src/rules/no-raw-enum.test.ts new file mode 100644 index 00000000000..4a6d89f4373 --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/no-raw-enum.test.ts @@ -0,0 +1,43 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { NoRawEnumRule } from './no-raw-enum.js'; + +const ruleTester = new RuleTester(); + +ruleTester.run('no-raw-enum', NoRawEnumRule, { + valid: [ + 'type Status = "ready" | "running";', + 'const enum Status { Ready, Running }', + { + code: 'declare const enum Status { Ready, Running }', + filename: 'types.d.ts', + }, + ], + invalid: [ + { + code: 'enum Status { Ready, Running }', + errors: [{ messageId: 'noRawEnum', line: 1, column: 1 }], + }, + { + code: 'enum Status { Ready = "ready", Running = "running" }', + errors: [{ messageId: 'noRawEnum', line: 1, column: 1 }], + }, + { + code: 'enum Status {}', + errors: [{ messageId: 'noRawEnum', line: 1, column: 1 }], + }, + { + code: 'export enum Status { Ready }', + errors: [{ messageId: 'noRawEnum', line: 1, column: 8 }], + }, + { + code: 'declare enum Status { Ready }', + filename: 'types.d.ts', + errors: [{ messageId: 'noRawEnum', line: 1, column: 1 }], + }, + { + code: 'declare namespace Api { enum Status { Ready } }', + filename: 'types.d.ts', + errors: [{ messageId: 'noRawEnum', line: 1, column: 25 }], + }, + ], +}); diff --git a/packages/@n8n/eslint-config/src/rules/no-raw-enum.ts b/packages/@n8n/eslint-config/src/rules/no-raw-enum.ts new file mode 100644 index 00000000000..ef5d7b7040a --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/no-raw-enum.ts @@ -0,0 +1,23 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; + +export const NoRawEnumRule = ESLintUtils.RuleCreator.withoutDocs({ + meta: { + type: 'problem', + docs: { + description: 'Require const enums to avoid runtime enum objects.', + }, + messages: { + noRawEnum: + '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', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + TSEnumDeclaration(node) { + if (!node.const) context.report({ node, messageId: 'noRawEnum' }); + }, + }; + }, +}); diff --git a/packages/@n8n/imap/eslint.config.mjs b/packages/@n8n/imap/eslint.config.mjs index 101ff0f543a..f2543a07d77 100644 --- a/packages/@n8n/imap/eslint.config.mjs +++ b/packages/@n8n/imap/eslint.config.mjs @@ -4,9 +4,5 @@ import { backendConfig } from '@n8n/eslint-config/backend'; export default defineConfig(backendConfig, { rules: { '@typescript-eslint/consistent-type-imports': 'error', - - // TODO: Remove this - '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn', - '@typescript-eslint/no-floating-promises': 'warn', }, }); diff --git a/packages/@n8n/imap/oxlint.config.mts b/packages/@n8n/imap/oxlint.config.mts new file mode 100644 index 00000000000..b50c92512db --- /dev/null +++ b/packages/@n8n/imap/oxlint.config.mts @@ -0,0 +1,7 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, +}); diff --git a/packages/@n8n/imap/package.json b/packages/@n8n/imap/package.json index c3835aa2e49..25085573044 100644 --- a/packages/@n8n/imap/package.json +++ b/packages/@n8n/imap/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write src test", "format:check": "biome ci src test", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -28,9 +28,12 @@ "libmime": "5.4.2" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@types/libmime": "^5.3.0", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vite": "catalog:", "vitest": "catalog:" diff --git a/packages/@n8n/local-gateway/oxlint.config.mts b/packages/@n8n/local-gateway/oxlint.config.mts new file mode 100644 index 00000000000..4b3053914c3 --- /dev/null +++ b/packages/@n8n/local-gateway/oxlint.config.mts @@ -0,0 +1,8 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + ignorePatterns: ['electron-builder.config.js', 'scripts/**'], +}); diff --git a/packages/@n8n/local-gateway/package.json b/packages/@n8n/local-gateway/package.json index 0d4b21c0ae2..eba9f386087 100644 --- a/packages/@n8n/local-gateway/package.json +++ b/packages/@n8n/local-gateway/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc -p tsconfig.renderer.json && tsc -p tsconfig.build.json && pnpm copy:renderer && pnpm copy:assets", "build:unchecked": "tsc -p tsconfig.renderer.json --noCheck && tsc -p tsconfig.build.json --noCheck && pnpm copy:renderer && pnpm copy:assets", - "lint": "eslint . --quiet", + "lint": "oxlint --quiet", "typecheck": "tsc --noEmit", "copy:assets": "node scripts/copy-assets.js", "copy:renderer": "node scripts/copy-renderer.js", @@ -29,12 +29,15 @@ }, "devDependencies": { "@n8n/eslint-config": "workspace:*", + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@types/node": "catalog:", "@vitest/coverage-v8": "catalog:", "concurrently": "^8.2.0", "electron-builder": "^26.9.0", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "rimraf": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:" diff --git a/packages/@n8n/nodes-langchain/nodes/Guardrails/actions/checks/pii.ts b/packages/@n8n/nodes-langchain/nodes/Guardrails/actions/checks/pii.ts index f728426f551..9d5c6be649a 100644 --- a/packages/@n8n/nodes-langchain/nodes/Guardrails/actions/checks/pii.ts +++ b/packages/@n8n/nodes-langchain/nodes/Guardrails/actions/checks/pii.ts @@ -18,7 +18,7 @@ import type { CreateCheckFn, CustomRegex } from '../types'; * Includes global and region-specific types (US, UK, Spain, Italy, etc.). * These map to regex patterns for detection. */ -// eslint-disable-next-line no-restricted-syntax +// eslint-disable-next-line n8n-local-rules/no-raw-enum export enum PIIEntity { // Global CREDIT_CARD = 'CREDIT_CARD', diff --git a/packages/@n8n/oxlint-config/src/configs/base.ts b/packages/@n8n/oxlint-config/src/configs/base.ts index 87d45f17ae3..5342a1aa137 100644 --- a/packages/@n8n/oxlint-config/src/configs/base.ts +++ b/packages/@n8n/oxlint-config/src/configs/base.ts @@ -223,6 +223,7 @@ export const baseConfig = defineConfig({ 'n8n-local-rules/no-json-parse-json-stringify': 'error', 'n8n-local-rules/no-restricted-sleep-definition': 'error', 'n8n-local-rules/no-restricted-sleep-import': 'error', + 'n8n-local-rules/no-raw-enum': 'error', 'n8n-local-rules/no-type-only-import-in-di': 'error', 'n8n-local-rules/no-unsealed-credentials-entity-write': 'error', 'n8n-local-rules/no-unsealed-workflow-entity-write': 'error', diff --git a/packages/@n8n/scheduler/oxlint.config.mts b/packages/@n8n/scheduler/oxlint.config.mts new file mode 100644 index 00000000000..dfd0c025a89 --- /dev/null +++ b/packages/@n8n/scheduler/oxlint.config.mts @@ -0,0 +1,21 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + ignorePatterns: ['stryker.config.mjs', 'vitest.stryker.config.ts', '.stryker-tmp/**'], + overrides: [ + { + files: ['./src/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: ['@n8n/db', '@n8n/di', '@n8n/typeorm'], + }, + ], + }, + }, + ], +}); diff --git a/packages/@n8n/scheduler/package.json b/packages/@n8n/scheduler/package.json index a1b4d03b96b..9398e866bab 100644 --- a/packages/@n8n/scheduler/package.json +++ b/packages/@n8n/scheduler/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "mutate": "node ../../../scripts/mutation-health/mutate.mjs --package-dir packages/@n8n/scheduler", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run --passWithNoTests", @@ -37,6 +37,7 @@ "n8n-workflow": "workspace:*" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@stryker-mutator/core": "catalog:", @@ -44,6 +45,8 @@ "@types/luxon": "catalog:", "@vitest/coverage-v8": "catalog:", "fast-check": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vite": "catalog:", "vitest": "catalog:", diff --git a/packages/@n8n/syslog-client/oxlint.config.mts b/packages/@n8n/syslog-client/oxlint.config.mts new file mode 100644 index 00000000000..b1a6fc869e0 --- /dev/null +++ b/packages/@n8n/syslog-client/oxlint.config.mts @@ -0,0 +1,16 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + overrides: [ + { + files: ['**/*.config.ts'], + jsPlugins: ['@n8n/eslint-config/plugin'], + rules: { + 'n8n-local-rules/no-untyped-config-class-field': 'error', + }, + }, + ], +}); diff --git a/packages/@n8n/syslog-client/package.json b/packages/@n8n/syslog-client/package.json index 881ad43bc99..6d6a215a658 100644 --- a/packages/@n8n/syslog-client/package.json +++ b/packages/@n8n/syslog-client/package.json @@ -9,8 +9,8 @@ "build:unchecked": "tsc -p tsconfig.build.json --noCheck", "format": "biome format --write src test", "format:check": "biome ci src test", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "watch": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "test:unit": "vitest run", @@ -26,9 +26,12 @@ "zod": "catalog:" }, "devDependencies": { + "@n8n/oxlint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@vitest/coverage-v8": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vite": "catalog:", "vitest": "catalog:" diff --git a/packages/@n8n/syslog-client/src/constants.ts b/packages/@n8n/syslog-client/src/constants.ts index 20c722adcf3..b8cdf9e4087 100644 --- a/packages/@n8n/syslog-client/src/constants.ts +++ b/packages/@n8n/syslog-client/src/constants.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-restricted-syntax */ +/* eslint-disable n8n-local-rules/no-raw-enum */ /* We want the runtime overhead here */ /** @@ -19,8 +19,7 @@ export enum Facility { User = 1, Mail = 2, System = 3, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - Daemon = 3, + Daemon = System, Auth = 4, Syslog = 5, Lpr = 6, diff --git a/packages/frontend/@n8n/frontend-constants/README.md b/packages/frontend/@n8n/frontend-constants/README.md index 9c5b065f9f2..b684b4dbf36 100644 --- a/packages/frontend/@n8n/frontend-constants/README.md +++ b/packages/frontend/@n8n/frontend-constants/README.md @@ -30,7 +30,7 @@ cannot read (TypeScript error TS2748). A plain `enum` emits a regular `dist` across the package boundary, and it preserves the nominal enum-member types the rest of the front end relies on (so relocating a value such as `VIEWS` here is behavior-preserving). The repo lint default prefers `const enum`, so the -`no-restricted-syntax` rule is relaxed for these files in `eslint.config.mjs`. +`no-raw-enum` rule is relaxed for these files in `eslint.config.mjs`. ## Contributing diff --git a/packages/frontend/@n8n/frontend-constants/eslint.config.mjs b/packages/frontend/@n8n/frontend-constants/eslint.config.mjs index 1bc715fe7f0..f0d5dfa0285 100644 --- a/packages/frontend/@n8n/frontend-constants/eslint.config.mjs +++ b/packages/frontend/@n8n/frontend-constants/eslint.config.mjs @@ -7,11 +7,11 @@ export default defineConfig( // `VIEWS` is declared as a plain `enum` (not a `const enum`) so this package's // `dist` stays consumable by `isolatedModules` downstream packages (a `const // enum` emits an ambient const enum → TS2748). See the doc comment in views.ts. - // - `no-restricted-syntax` otherwise bans raw enums in favor of `const enum`. + // - `no-raw-enum` otherwise bans raw enums in favor of `const enum`. // - view identifiers are UPPER_CASE by long-standing app convention. files: ['src/views.ts'], rules: { - 'no-restricted-syntax': 'off', + 'n8n-local-rules/no-raw-enum': 'off', '@typescript-eslint/naming-convention': 'off', }, }, diff --git a/packages/frontend/@n8n/frontend-constants/src/views.ts b/packages/frontend/@n8n/frontend-constants/src/views.ts index df1110217b1..d9547b6a2e5 100644 --- a/packages/frontend/@n8n/frontend-constants/src/views.ts +++ b/packages/frontend/@n8n/frontend-constants/src/views.ts @@ -11,7 +11,7 @@ * * The repo's default lint bans raw enums in favor of `const enum` for runtime * overhead; that guidance is inverted here because `const enum` is precisely what - * breaks dist consumption. The `no-restricted-syntax` rule (and the camelCase + * breaks dist consumption. The `no-raw-enum` rule (and the camelCase * naming-convention rule, since view identifiers are UPPER_CASE by convention) * are relaxed for this file in `eslint.config.mjs`. * diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentPreviewDock.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentPreviewDock.vue index 66134be469e..1d8d9e29a44 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentPreviewDock.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentPreviewDock.vue @@ -34,7 +34,7 @@ interface SessionOption { updatedAt?: string; } -enum PreviewLayout { +const enum PreviewLayout { Docked = 'docked', Fullpage = 'fullpage', } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b06b853b29..fd4ed56f833 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1315,12 +1315,21 @@ importers: specifier: workspace:* version: link:../ai-utilities devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config '@n8n/vitest-config': specifier: workspace:* version: link:../vitest-config + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -1640,6 +1649,9 @@ importers: specifier: 21.1.1 version: 21.1.1 devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -1655,6 +1667,12 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9) + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -1764,9 +1782,6 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9) - eslint: - specifier: 'catalog:' - version: 9.29.0(jiti@2.6.1)(supports-color@8.1.1) oxlint: specifier: 'catalog:' version: 1.78.0(oxlint-tsgolint@0.21.1) @@ -1974,6 +1989,9 @@ importers: specifier: 1.18.0 version: 1.18.0(patch_hash=149e256a2a7b632497650b32816716ced972ab02a5ab00fbd8a5158a51722c49)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -1989,6 +2007,12 @@ importers: nock: specifier: 'catalog:' version: 14.0.14 + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -2176,6 +2200,9 @@ importers: specifier: 'catalog:' version: 3.23.3(zod@3.25.76) devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -2194,6 +2221,12 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9) + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -2219,6 +2252,9 @@ importers: specifier: 3.25.76 version: 3.25.76 devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -2228,6 +2264,12 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9) + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -2418,6 +2460,9 @@ importers: specifier: workspace:* version: link:../../workflow devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -2430,6 +2475,12 @@ importers: '@types/lodash': specifier: 'catalog:' version: 4.17.17 + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -2822,6 +2873,9 @@ importers: specifier: 5.4.2 version: 5.4.2 devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -2831,6 +2885,12 @@ importers: '@types/libmime': specifier: ^5.3.0 version: 5.3.0 + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -3007,6 +3067,9 @@ importers: '@n8n/eslint-config': specifier: workspace:* version: link:../eslint-config + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -3025,6 +3088,12 @@ importers: electron-builder: specifier: ^26.9.0 version: 26.9.0(electron-builder-squirrel-windows@26.9.0)(supports-color@8.1.1) + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 rimraf: specifier: 'catalog:' version: 6.0.1 @@ -3804,6 +3873,9 @@ importers: specifier: workspace:* version: link:../../workflow devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -3825,6 +3897,12 @@ importers: fast-check: specifier: 'catalog:' version: 3.23.2 + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 @@ -3878,6 +3956,9 @@ importers: specifier: 3.25.76 version: 3.25.76 devDependencies: + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../oxlint-config '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -3887,6 +3968,12 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9) + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 typescript: specifier: catalog:typescript version: 7.0.2 diff --git a/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md b/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md index e920cb139a9..58e04e715a7 100644 --- a/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md +++ b/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md @@ -21,7 +21,6 @@ These rules come from the shared base or backend layers. Resolve these first bec | Priority | Rule | Blocker | Suggested next action | | --- | --- | --- | --- | -| P1 | `no-restricted-syntax` | Oxlint has no AST-selector rule. The base layer uses it for the raw-enum restriction. | Add a purpose-built syntax rule for each active selector. Start with the raw-enum rule. | | P1 | `typescript/consistent-type-imports` | Oxlint does not honor the `emitDecoratorMetadata` exception. Its fixer can remove imports that DI needs at runtime. | Wait for upstream support or add a tested shim that skips decorated files. Do not enable the current fixer. | | P1 | `typescript/prefer-optional-chain` | The native nursery rule changes guarded undeclared globals incorrectly. | Re-test after the rule leaves nursery or after the upstream fixer issue is resolved. | | P2 | `import/export` | A native rule exists, but it is still in the nursery category. | Run parity samples and enable it after it becomes stable. | diff --git a/scripts/lint-parity/oxlint-gap.json b/scripts/lint-parity/oxlint-gap.json index 75656598d12..8f8d890b9b5 100644 --- a/scripts/lint-parity/oxlint-gap.json +++ b/scripts/lint-parity/oxlint-gap.json @@ -7,13 +7,6 @@ "rule id when one exists but we still do not enable it. `replacement` records", "the intended migration path for each gap." ], - "no-restricted-syntax": { - "layer": "base", - "native": null, - "reason": "oxlint 1.78 has no AST-selector rule and no equivalent. It is an ESLint core rule, so a jsPlugin cannot host it either: `Rule 'no-restricted-syntax' not found in plugin 'eslint'`. base uses it for the raw-enum ban; editor-ui uses it for store-migration ratchets, and editor-ui keeps ESLint.", - "impact": "The const-enum ban is unenforced in migrated packages.", - "replacement": "Add a native selector rule, or replace each selector use with a purpose-built syntactic rule." - }, "no-octal": { "layer": "base", "native": null, @@ -43,6 +36,13 @@ "replacement": "Wait for the upstream emitDecoratorMetadata option, or use an approved shim that suppresses decorated files.", "upstream": "https://github.com/oxc-project/oxc/issues/13609" }, + "frontend/no-restricted-syntax": { + "layer": "frontend", + "native": null, + "reason": "editor-ui uses package-specific AST selectors for workflow-store and modal-migration ratchets. The frontend keeps ESLint for these selectors.", + "impact": "None for the backend migration.", + "replacement": "Replace each selector with a focused rule before an Oxlint-only frontend migration." + }, "frontend/vue-template-rules": { "layer": "frontend", "native": null, diff --git a/scripts/lint-parity/oxlint-parity.mjs b/scripts/lint-parity/oxlint-parity.mjs index 380b2a94f02..a107e6e4e6c 100644 --- a/scripts/lint-parity/oxlint-parity.mjs +++ b/scripts/lint-parity/oxlint-parity.mjs @@ -17,13 +17,12 @@ * local rules would read as missing. Those are read from the config objects * instead, which is weaker: it describes what we declared, not what oxlint * resolved. A fixture probe is the only way to prove they fire. - * - `--print-config` resolves the config, not a file, so `overrides` do not - * appear. Both sides are therefore compared as a union across the package's - * sample files. + * - `--print-config` does not apply overrides to its top-level rule table. Both + * sides are therefore compared as a union across the package's sample files. */ import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; +import { dirname, join, matchesGlob, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); @@ -114,6 +113,16 @@ function isEnabled(severity) { return value === 'deny' || value === 'error' || value === 2; } +const matchesPattern = (file, pattern) => matchesGlob(file, pattern.replace(/^\.\//, '')); + +function overrideMatchesSample({ files = [], excludeFiles = [], ignores = [] }) { + return sampledFiles.some( + (file) => + (files.length === 0 || files.some((pattern) => matchesPattern(file, pattern))) && + ![...excludeFiles, ...ignores].some((pattern) => matchesPattern(file, pattern)), + ); +} + /** Native rules oxlint actually resolved for this package. */ function resolvedNativeRules() { const out = spawnSync('pnpm', ['exec', 'oxlint', '--print-config'], { @@ -126,7 +135,12 @@ function resolvedNativeRules() { throw new Error(`oxlint --print-config failed: ${(out.stderr || out.stdout).trim()}`); } const parsed = JSON.parse(out.stdout.slice(start)); - return Object.entries(parsed.rules ?? {}) + return [ + ...Object.entries(parsed.rules ?? {}), + ...(parsed.overrides ?? []) + .filter(overrideMatchesSample) + .flatMap(({ rules }) => Object.entries(rules ?? {})), + ] .filter(([, severity]) => isEnabled(severity)) .map(([id]) => id); } @@ -138,7 +152,7 @@ function resolvedNativeRules() { async function declaredJsPluginRules() { const mod = await import(pathToFileURL(join(root, pkgDir, 'oxlint.config.mts')).href); const declared = new Map(); - const overrideOnly = new Set(); + const overrideOnly = new Map(); const plugins = new Set(); const visit = (config) => { @@ -151,7 +165,11 @@ async function declaredJsPluginRules() { // An override that turns a rule off still leaves it enforced elsewhere // in the package, and this comparison is a union. if (!isEnabled(severity)) continue; - if (!declared.has(id)) overrideOnly.add(id); + if (!isEnabled(declared.get(id)) || overrideOnly.has(id)) { + const overrides = overrideOnly.get(id) ?? []; + overrides.push(override); + overrideOnly.set(id, overrides); + } declared.set(id, severity); } } @@ -164,12 +182,6 @@ async function declaredJsPluginRules() { return { ids, overrideOnly, plugins: [...plugins] }; } -// The test-file override is only comparable when a test file was sampled: -// ESLint resolves its config per file, oxlint's --print-config does not resolve -// overrides at all, so without a test sample there is nothing to compare and an -// override-only rule would read as unrequested. -const isTestSample = (key) => /(\.test\.ts|\/__tests__\/|^test\/)/.test(key.split('|')[1] ?? ''); - const native = oxlintRuleIds(); const gap = JSON.parse(readFileSync(gapFile, 'utf8')); @@ -177,11 +189,11 @@ const gap = JSON.parse(readFileSync(gapFile, 'utf8')); const snapshot = JSON.parse(readFileSync(resolve(eslintFile), 'utf8')); const eslintRules = new Set(); let sampled = 0; -let sampledTestFile = false; +const sampledFiles = []; for (const [key, entry] of Object.entries(snapshot)) { if (!key.startsWith(`${pkgDir}|`) || !entry.rules) continue; sampled++; - if (isTestSample(key)) sampledTestFile = true; + sampledFiles.push(key.split('|')[1]); for (const id of Object.keys(entry.rules)) eslintRules.add(id); } if (sampled === 0) { @@ -191,12 +203,13 @@ if (sampled === 0) { const { ids: jsPluginIds, overrideOnly, plugins } = await declaredJsPluginRules(); -const comparable = (id) => !overrideOnly.has(id) || sampledTestFile; +const comparable = (id) => { + const overrides = overrideOnly.get(id); + return !overrides || overrides.some(overrideMatchesSample); +}; const oxlintRules = new Set( - [...resolvedNativeRules(), ...jsPluginIds] - .filter(comparable) - .map((id) => canonical(id, native)), + [...resolvedNativeRules(), ...jsPluginIds].filter(comparable).map((id) => canonical(id, native)), ); const matched = []; @@ -225,7 +238,9 @@ const undocumented = missing.filter((entry) => !isDocumented(entry)); console.log(`${pkgDir}: ${sampled} sample files, ${eslintRules.size} ESLint rules at error`); console.log(` jsPlugins declared: ${plugins.join(', ') || 'none'}`); console.log(` matched: ${matched.length}`); -console.log(` missing in oxlint: ${missing.length} (${missing.length - undocumented.length} documented)`); +console.log( + ` missing in oxlint: ${missing.length} (${missing.length - undocumented.length} documented)`, +); console.log(` extra in oxlint: ${extra.length}`); for (const id of extra) console.log(` EXTRA ${id}`); diff --git a/scripts/lint-parity/samples.json b/scripts/lint-parity/samples.json index 004b8c9d40f..abb04ac4ffd 100644 --- a/scripts/lint-parity/samples.json +++ b/scripts/lint-parity/samples.json @@ -24,6 +24,7 @@ "src/dns/__tests__/dns-resolver.test.ts", "src/dns/dns-resolver.ts" ], + "packages/@n8n/backend-services": ["src/index.ts", "test/setup-test-folder.ts"], "packages/@n8n/backend-test-utils": ["src/db/projects.ts"], "packages/@n8n/benchmark": ["src/commands/list.ts"], "packages/@n8n/blob-storage": [ diff --git a/scripts/lint-parity/verify-raw-enum-bridge.mjs b/scripts/lint-parity/verify-raw-enum-bridge.mjs new file mode 100644 index 00000000000..4f4782c7dcd --- /dev/null +++ b/scripts/lint-parity/verify-raw-enum-bridge.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const fixtureDirectory = fileURLToPath( + new URL('../../packages/@n8n/eslint-config/src/rules/fixtures/raw-enum/', import.meta.url), +); +const oxlintPackageDirectory = fileURLToPath( + new URL('../../packages/@n8n/oxlint-config/', import.meta.url), +); + +const run = (command, args, cwd) => { + try { + return execFileSync(command, args, { cwd, encoding: 'utf8' }); + } catch (error) { + if (error.status === 1 && typeof error.stdout === 'string') return error.stdout; + throw error; + } +}; + +const eslint = JSON.parse( + run( + 'pnpm', + ['exec', 'eslint', '--config', 'eslint.config.mjs', '--format', 'json', '.'], + fixtureDirectory, + ), +); +const oxlint = JSON.parse( + run( + 'pnpm', + [ + 'exec', + 'oxlint', + '--config', + `${fixtureDirectory}/oxlint.config.mts`, + '--format', + 'json', + fixtureDirectory, + ], + oxlintPackageDirectory, + ), +); + +const eslintDiagnostics = eslint + .flatMap(({ messages }) => messages) + .filter(({ ruleId }) => ruleId === 'n8n-local-rules/no-raw-enum') + .map(({ ruleId, message, line, column }) => ({ ruleId, message, line, column })); + +const oxlintDiagnostics = oxlint.diagnostics + .filter(({ code }) => code === 'n8n-local-rules(no-raw-enum)') + .map(({ code, message, labels }) => ({ + ruleId: code.replace('(', '/').replace(')', ''), + message, + line: labels[0].span.line, + column: labels[0].span.column, + })); + +assert.deepEqual(oxlintDiagnostics, eslintDiagnostics); +assert.equal(eslintDiagnostics.length, 2); + +console.log('Raw-enum ESLint/Oxlint bridge parity passed.'); diff --git a/turbo.json b/turbo.json index 6c3b36311f5..6f54bde1585 100644 --- a/turbo.json +++ b/turbo.json @@ -57,15 +57,51 @@ "lint": { "dependsOn": ["^build", "@n8n/eslint-config#build"] }, - // oxlint spike packages (DEVP-584). They run oxlint instead of ESLint, so + // Oxlint packages run Oxlint instead of ESLint, so // they need the oxlint layer built, not the ESLint one. - "@n8n/errors#lint": { + "@n8n/ai-node-sdk#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/backend-common#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/backend-services#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/client-oauth2#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/computer-use#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/config#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/crdt#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/decorators#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, "@n8n/di#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, - "@n8n/crdt#lint": { + "@n8n/errors#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/imap#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/local-gateway#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/scheduler#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/syslog-client#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "n8n#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, // The n8n-nodes-base lint script also validates load-options methods