chore: Migrate phase-one backend packages to Oxlint (no-changelog) (#39360)

This commit is contained in:
Matsu
2026-09-23 10:00:59 +00:00
committed by GitHub
parent 1c48187ca1
commit 27627bc793
51 changed files with 560 additions and 152 deletions
+1 -5
View File
@@ -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/**']));
@@ -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/**'],
});
+5 -2
View File
@@ -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:"
},
+9 -17
View File
@@ -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',
},
},
);
});
@@ -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',
},
},
],
});
+5 -2
View File
@@ -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:",
+1 -1
View File
@@ -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:
@@ -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,
);
@@ -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',
{
+2 -5
View File
@@ -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",
@@ -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',
},
},
],
});
+5 -2
View File
@@ -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:",
@@ -0,0 +1,7 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [backendConfig],
options: { typeAware: true },
});
+5 -2
View File
@@ -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:"
+16
View File
@@ -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',
},
},
],
});
+5 -2
View File
@@ -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:"
+6 -1
View File
@@ -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: {
@@ -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',
},
},
],
});
+5 -2
View File
@@ -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:"
@@ -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
// ----------------------------------
@@ -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',
@@ -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' },
});
@@ -0,0 +1,7 @@
export enum NumericStatus {
Ready,
}
export enum StringStatus {
Ready = 'ready',
}
@@ -0,0 +1,5 @@
export default {
categories: { correctness: 'off' },
jsPlugins: ['@n8n/eslint-config/plugin'],
rules: { 'n8n-local-rules/no-raw-enum': 'error' },
};
@@ -0,0 +1,5 @@
const enum Status {
Ready,
}
export const status = Status.Ready;
@@ -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<string, AnyRuleModule>;
@@ -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 }],
},
],
});
@@ -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' });
},
};
},
});
-4
View File
@@ -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',
},
});
+7
View File
@@ -0,0 +1,7 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [backendConfig],
options: { typeAware: true },
});
+5 -2
View File
@@ -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:"
@@ -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/**'],
});
+4 -1
View File
@@ -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:"
@@ -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',
@@ -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',
+21
View File
@@ -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'],
},
],
},
},
],
});
+5 -2
View File
@@ -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:",
@@ -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',
},
},
],
});
+5 -2
View File
@@ -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:"
+2 -3
View File
@@ -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,
@@ -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
@@ -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',
},
},
@@ -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`.
*
@@ -34,7 +34,7 @@ interface SessionOption {
updatedAt?: string;
}
enum PreviewLayout {
const enum PreviewLayout {
Docked = 'docked',
Fullpage = 'fullpage',
}
+90 -3
View File
@@ -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([email protected])
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/[email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
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/[email protected])([email protected])
eslint:
specifier: 'catalog:'
version: 9.29.0([email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
@@ -1974,6 +1989,9 @@ importers:
specifier: 1.18.0
version: 1.18.0(patch_hash=149e256a2a7b632497650b32816716ced972ab02a5ab00fbd8a5158a51722c49)([email protected]([email protected]))([email protected])
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([email protected])
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([email protected])
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/[email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
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/[email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
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([email protected])
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([email protected])
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([email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
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([email protected])
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/[email protected])([email protected])
oxlint:
specifier: 'catalog:'
version: 1.78.0([email protected])
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
typescript:
specifier: catalog:typescript
version: 7.0.2
@@ -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. |
+7 -7
View File
@@ -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,
+35 -20
View File
@@ -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}`);
+1
View File
@@ -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": [
@@ -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.');
+39 -3
View File
@@ -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