diff --git a/packages/testing/code-health/eslint.config.mjs b/packages/testing/code-health/eslint.config.mjs index ca86c38e133..5ba0f921fd5 100644 --- a/packages/testing/code-health/eslint.config.mjs +++ b/packages/testing/code-health/eslint.config.mjs @@ -1,24 +1,6 @@ import { defineConfig } from 'eslint/config'; import { backendConfig } from '@n8n/eslint-config/backend'; -export default defineConfig( - backendConfig, - { - ignores: ['coverage/**', 'dist/**'], - }, - { - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: 'objectLiteralProperty', - format: null, - filter: { - regex: '^[a-z]+-[a-z-]+$', - match: true, - }, - }, - ], - }, - }, -); +export default defineConfig(backendConfig, { + ignores: ['coverage/**', 'dist/**'], +}); diff --git a/packages/testing/code-health/oxlint.config.mts b/packages/testing/code-health/oxlint.config.mts new file mode 100644 index 00000000000..002b0ae4000 --- /dev/null +++ b/packages/testing/code-health/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: ['coverage/**'], +}); diff --git a/packages/testing/code-health/package.json b/packages/testing/code-health/package.json index 1b2ab9df022..6e9d615213f 100644 --- a/packages/testing/code-health/package.json +++ b/packages/testing/code-health/package.json @@ -23,8 +23,8 @@ "test:coverage": "vitest run --coverage", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "typecheck": "tsc --noEmit" }, "license": "MIT", @@ -40,9 +40,13 @@ "yaml": "catalog:" }, "devDependencies": { + "@n8n/eslint-config": "workspace:*", + "@n8n/oxlint-config": "workspace:*", "@types/markdown-it": "catalog:", "@vitest/coverage-v8": "catalog:", "fast-check": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "vitest": "catalog:" } } diff --git a/packages/testing/containers/eslint.config.mjs b/packages/testing/containers/eslint.config.mjs index ee76ed2056a..2b219565bfc 100644 --- a/packages/testing/containers/eslint.config.mjs +++ b/packages/testing/containers/eslint.config.mjs @@ -1,30 +1,11 @@ import { defineConfig } from 'eslint/config'; import { backendConfig } from '@n8n/eslint-config/backend'; -export default defineConfig( - backendConfig, - { - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - // Add exception for Docker Compose labels - { - selector: 'objectLiteralProperty', - format: null, // Allow any format - filter: { - regex: '^com\\.docker\\.', - match: true, - }, - }, - ], - }, +export default defineConfig(backendConfig, { + files: ['services/keycloak.ts'], + // Test infrastructure that talks to a container it started itself, + // so the guarded client buys nothing here. + rules: { + 'n8n-local-rules/no-uncentralized-http': 'off', }, - { - files: ['services/keycloak.ts'], - // Test infrastructure that talks to a container it started itself, - // so the guarded client buys nothing here. - rules: { - 'n8n-local-rules/no-uncentralized-http': 'off', - }, - }, -); +}); diff --git a/packages/testing/containers/oxlint.config.mts b/packages/testing/containers/oxlint.config.mts new file mode 100644 index 00000000000..d61bb36d7d0 --- /dev/null +++ b/packages/testing/containers/oxlint.config.mts @@ -0,0 +1,14 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + overrides: [ + { + files: ['services/keycloak.ts'], + jsPlugins: ['@n8n/eslint-config/plugin'], + rules: { 'n8n-local-rules/no-uncentralized-http': 'off' }, + }, + ], +}); diff --git a/packages/testing/containers/package.json b/packages/testing/containers/package.json index 260af4e2bdd..0c593e8d396 100644 --- a/packages/testing/containers/package.json +++ b/packages/testing/containers/package.json @@ -27,8 +27,8 @@ "typecheck": "tsc --noEmit", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix" + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix" }, "keywords": [], "author": "", @@ -47,6 +47,10 @@ "undici": "catalog:undici-v7" }, "devDependencies": { + "@n8n/eslint-config": "workspace:*", + "@n8n/oxlint-config": "workspace:*", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "tsx": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:" diff --git a/packages/testing/janitor/eslint.config.mjs b/packages/testing/janitor/eslint.config.mjs index d47150b1d37..90345e74796 100644 --- a/packages/testing/janitor/eslint.config.mjs +++ b/packages/testing/janitor/eslint.config.mjs @@ -1,25 +1,6 @@ import { defineConfig } from 'eslint/config'; import { backendConfig } from '@n8n/eslint-config/backend'; -export default defineConfig( - backendConfig, - { - ignores: ['coverage/**', 'bin/**'], - }, - { - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - // Allow kebab-case for rule IDs in config objects - { - selector: 'objectLiteralProperty', - format: null, - filter: { - regex: '^[a-z]+-[a-z-]+$', // kebab-case pattern - match: true, - }, - }, - ], - }, - }, -); +export default defineConfig(backendConfig, { + ignores: ['coverage/**', 'bin/**'], +}); diff --git a/packages/testing/janitor/oxlint.config.mts b/packages/testing/janitor/oxlint.config.mts new file mode 100644 index 00000000000..7a348a21213 --- /dev/null +++ b/packages/testing/janitor/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: ['coverage/**', 'bin/**'], +}); diff --git a/packages/testing/janitor/package.json b/packages/testing/janitor/package.json index 7a656a64b09..9cf0bc5c25e 100644 --- a/packages/testing/janitor/package.json +++ b/packages/testing/janitor/package.json @@ -23,8 +23,8 @@ "test:coverage": "vitest run --coverage", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "typecheck": "tsc --noEmit" }, "keywords": [ @@ -46,9 +46,13 @@ "peerDependencies": { "ts-morph": "catalog:" }, - "devDependencies": { + "devDependencies": { + "@n8n/eslint-config": "workspace:*", + "@n8n/oxlint-config": "workspace:*", "@types/node": "catalog:", "@vitest/coverage-v8": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:" } diff --git a/packages/testing/playwright/eslint.config.mjs b/packages/testing/playwright/eslint.config.mjs index 3f1333de94f..3bd38b1049f 100644 --- a/packages/testing/playwright/eslint.config.mjs +++ b/packages/testing/playwright/eslint.config.mjs @@ -1,6 +1,8 @@ import { backendConfig } from '@n8n/eslint-config/backend'; import playwrightPlugin from 'eslint-plugin-playwright'; +import { legacyFilenameCaseFiles } from './lint-filename-debt.mjs'; + export default [ ...backendConfig, playwrightPlugin.configs['flat/recommended'], @@ -18,50 +20,11 @@ export default [ }, { rules: { - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/promise-function-async': 'off', + 'no-empty-pattern': 'error', 'playwright/expect-expect': 'warn', 'playwright/max-nested-describe': 'warn', 'playwright/no-conditional-in-test': 'error', 'playwright/no-skipped-test': 'warn', - // Allow any naming convention for TestRequirements object properties - // This is specifically for workflow filenames and intercept keys that may not follow camelCase - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: 'default', - format: ['camelCase'], - leadingUnderscore: 'allow', - trailingUnderscore: 'allow', - }, - { - selector: 'variable', - format: ['camelCase', 'UPPER_CASE'], - }, - { - selector: 'typeLike', - format: ['PascalCase'], - }, - { - selector: 'property', - format: ['camelCase', 'snake_case', 'UPPER_CASE'], - filter: { - // Allow any format for properties in TestRequirements objects (workflow files, intercept keys, etc.) - regex: '^(workflow|intercepts|storage|config)$', - match: false, - }, - }, - { - selector: 'objectLiteralProperty', - format: null, // Allow any format for object literal properties in TestRequirements - filter: { - // This allows workflow filenames and intercept keys to use any naming convention - regex: '\\.(json|spec\\.ts)$|[a-zA-Z0-9_-]+', - match: true, - }, - }, - ], 'import-x/no-extraneous-dependencies': [ 'error', { @@ -72,10 +35,42 @@ export default [ }, }, { - // Debt: the base layer enforces kebab-case filenames and this package has - // 124 files that predate it. Rename them, then delete this block. - rules: { - 'unicorn/filename-case': 'off', - }, + files: legacyFilenameCaseFiles, + rules: { 'unicorn/filename-case': 'off' }, + }, + { + files: [ + 'fixtures/langsmith.ts', + 'fixtures/quarantine.ts', + 'tests/cli-workflows/workflow-tests.spec.ts', + 'tests/e2e/chat-hub/fixtures.ts', + 'tests/e2e/instance-ai/fixtures.ts', + 'tests/e2e/instance-ai/instance-ai-workflow-setup.spec.ts', + 'tests/e2e/workflows/editor/execution/fixtures.ts', + 'tests/framework/consumers.ts', + ], + rules: { 'no-empty-pattern': 'off' }, + }, + { + files: [ + 'composables/BuilderWizardComposer.ts', + 'helpers/ClipboardHelper.ts', + 'pages/PublicFormPage.ts', + 'pages/SettingsUsersPage.ts', + 'pages/SourceControlPushModal.ts', + 'reporters/ci-metrics.test.ts', + 'services/tag-api-helper.ts', + 'services/variables-api-helper.ts', + 'tests/cli-workflows/setup-workflow-tests.ts', + 'tests/e2e/projects/projects.spec.ts', + 'tests/e2e/workflows/editor/expressions/quickjs-engine.spec.ts', + 'tests/e2e/workflows/executions/list.spec.ts', + 'tests/e2e/workflows/templates/templates.spec.ts', + 'tests/evals/_smoke/langsmith-fixture.spec.ts', + 'tests/evals/instance-ai/weather-alert.spec.ts', + 'tests/infrastructure/benchmarks/ui/executions-list-customer-scale.spec.ts', + 'utils/benchmark/kafka-driver.ts', + ], + rules: { '@typescript-eslint/promise-function-async': 'off' }, }, ]; diff --git a/packages/testing/playwright/lint-filename-debt.mjs b/packages/testing/playwright/lint-filename-debt.mjs new file mode 100644 index 00000000000..75f64f02f46 --- /dev/null +++ b/packages/testing/playwright/lint-filename-debt.mjs @@ -0,0 +1,130 @@ +export const legacyFilenameCaseFiles = [ + 'Types.ts', + 'composables/BuilderWizardComposer.ts', + 'composables/CanvasComposer.ts', + 'composables/CredentialsComposer.ts', + 'composables/DataTablesComposer.ts', + 'composables/ExecutionsComposer.ts', + 'composables/MfaComposer.ts', + 'composables/NodeDetailsViewComposer.ts', + 'composables/OidcComposer.ts', + 'composables/PartialExecutionComposer.ts', + 'composables/ProjectComposer.ts', + 'composables/TemplatesComposer.ts', + 'composables/TestEntryComposer.ts', + 'composables/WorkflowComposer.ts', + 'helpers/ClipboardHelper.ts', + 'helpers/NavigationHelper.ts', + 'helpers/NodeParameterHelper.ts', + 'pages/AIAssistantPage.ts', + 'pages/AgentBuilderPage.ts', + 'pages/AgentSessionsPage.ts', + 'pages/BasePage.ts', + 'pages/CanvasPage.ts', + 'pages/ChatHubChatPage.ts', + 'pages/ChatHubPersonalAgentsPage.ts', + 'pages/ChatHubSettingsPage.ts', + 'pages/ChatHubWorkflowAgentsPage.ts', + 'pages/CommunityNodesPage.ts', + 'pages/CredentialsPage.ts', + 'pages/DataTableDetails.ts', + 'pages/DataTableView.ts', + 'pages/DemoPage.ts', + 'pages/EvaluationComparePage.ts', + 'pages/ExecutionsPage.ts', + 'pages/InstanceAiPage.ts', + 'pages/KeycloakLoginPage.ts', + 'pages/MfaLoginPage.ts', + 'pages/MfaSetupModal.ts', + 'pages/NodeDetailsViewPage.ts', + 'pages/NpsSurveyPage.ts', + 'pages/OAuthConsentPage.ts', + 'pages/ProjectSettingsPage.ts', + 'pages/PublicFormPage.ts', + 'pages/SecretsProviderSettingsPage.ts', + 'pages/SecuritySettingsPage.ts', + 'pages/SettingsEnvironmentPage.ts', + 'pages/SettingsLogStreamingPage.ts', + 'pages/SettingsMcpPage.ts', + 'pages/SettingsPersonalPage.ts', + 'pages/SettingsSsoPage.ts', + 'pages/SettingsUsersPage.ts', + 'pages/SidebarPage.ts', + 'pages/SignInPage.ts', + 'pages/SourceControlPullModal.ts', + 'pages/SourceControlPushModal.ts', + 'pages/TemplateCredentialSetupPage.ts', + 'pages/TemplatesPage.ts', + 'pages/VariablesPage.ts', + 'pages/VersionsPage.ts', + 'pages/WorkerViewPage.ts', + 'pages/WorkflowActivationModal.ts', + 'pages/WorkflowCredentialSetupModal.ts', + 'pages/WorkflowReviewsPage.ts', + 'pages/WorkflowSettingsModal.ts', + 'pages/WorkflowSharingModal.ts', + 'pages/WorkflowsPage.ts', + 'pages/n8nPage.ts', + 'pages/components/AIBuilderPage.ts', + 'pages/components/ActionToggle.ts', + 'pages/components/AddResource.ts', + 'pages/components/BaseModal.ts', + 'pages/components/Breadcrumbs.ts', + 'pages/components/BuilderSetupWizard.ts', + 'pages/components/ChatAgentCard.ts', + 'pages/components/ChatHubCredentialModal.ts', + 'pages/components/ChatHubPersonalAgentModal.ts', + 'pages/components/ChatHubProviderSettingsModal.ts', + 'pages/components/ChatHubSidebar.ts', + 'pages/components/ChatHubToolsModal.ts', + 'pages/components/CodeNodeEditor.ts', + 'pages/components/CommandBar.ts', + 'pages/components/ConvertToSubworkflowModal.ts', + 'pages/components/CredentialModal.ts', + 'pages/components/DeleteSecretsProviderModal.ts', + 'pages/components/FloatingUiHelper.ts', + 'pages/components/FocusPanel.ts', + 'pages/components/InlineExpressionEditor.ts', + 'pages/components/InstanceAiSidebar.ts', + 'pages/components/InstanceAiWorkflowSetup.ts', + 'pages/components/InteractionsPage.ts', + 'pages/components/LogsPanel.ts', + 'pages/components/ManualChatModal.ts', + 'pages/components/NodeCreator.ts', + 'pages/components/NodeCredentials.ts', + 'pages/components/NotificationsPage.ts', + 'pages/components/ProjectHeader.ts', + 'pages/components/ProjectTabsComponent.ts', + 'pages/components/ResourceCards.ts', + 'pages/components/ResourceLocator.ts', + 'pages/components/ResourceMoveModal.ts', + 'pages/components/RunDataPanel.ts', + 'pages/components/SaveChangesModal.ts', + 'pages/components/SecretsProviderConnectionModal.ts', + 'pages/components/SettingsSidebar.ts', + 'pages/components/SetupWorkflowCredentialsModal.ts', + 'pages/components/StickyComponent.ts', + 'pages/components/TagsManagerModal.ts', + 'pages/components/VariableModal.ts', + 'pages/components/WorkflowMenu.ts', + 'pages/components/WorkflowReviewControls.ts', + 'pages/components/dialogLocators.ts', + 'pages/components/messageBoxLocators.ts', + 'pages/components/nodes/EditFieldsNode.ts', + 'tests/e2e/regression/ADO-1338-ndv-missing-input-panel.spec.ts', + 'tests/e2e/regression/ADO-2230-ndv-reset-data-pagination.spec.ts', + 'tests/e2e/regression/ADO-2372-prevent-clipping-params.spec.ts', + 'tests/e2e/regression/ADO-2929-can-load-old-switch-node-workflows.spec.ts', + 'tests/e2e/regression/ADO-4462-template-setup-experiment.spec.ts', + 'tests/e2e/regression/ADO-5630-execute-button-layout.spec.ts', + 'tests/e2e/regression/ADO-5808-stale-node-execution-data.spec.ts', + 'tests/e2e/regression/AI-1401-sub-nodes-input-panel.spec.ts', + 'tests/e2e/regression/AI-2795-chat-panel-header-height.spec.ts', + 'tests/e2e/regression/AI-716-correctly-set-up-agent-model-shows-error.spec.ts', + 'tests/e2e/regression/AI-812-partial-execs-broken-when-using-chat-trigger.spec.ts', + 'tests/e2e/regression/CAT-2395-node-spacing.spec.ts', + 'tests/e2e/regression/CAT-726-canvas-node-connectors-not-rendered-when-nodes-inserted.spec.ts', + 'tests/e2e/regression/PAY-4367-node-shifting-cyclic.spec.ts', + 'tests/e2e/regression/SUG-121-fields-reset-after-closing-ndv.spec.ts', + 'tests/e2e/regression/SUG-38-inline-expression-preview.spec.ts', +]; diff --git a/packages/testing/playwright/oxlint.config.mts b/packages/testing/playwright/oxlint.config.mts new file mode 100644 index 00000000000..f532dc2fc87 --- /dev/null +++ b/packages/testing/playwright/oxlint.config.mts @@ -0,0 +1,208 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + jsPlugins: ['eslint-plugin-playwright'], + ignorePatterns: [ + 'playwright-report/**/*', + 'ms-playwright-cache/**/*', + '.playwright-browsers/**/*', + 'coverage/**/*', + 'scripts/**/*', + 'janitor.config.mjs', + ], + rules: { + 'playwright/missing-playwright-await': 'error', + 'playwright/no-conditional-in-test': 'error', + 'playwright/no-focused-test': 'error', + 'playwright/no-networkidle': 'error', + 'playwright/no-standalone-expect': 'error', + 'playwright/no-unsafe-references': 'error', + 'playwright/no-wait-for-navigation': 'error', + 'playwright/prefer-web-first-assertions': 'error', + 'playwright/valid-describe-callback': 'error', + 'playwright/valid-expect': 'error', + 'playwright/valid-expect-in-promise': 'error', + 'playwright/valid-test-tags': 'error', + 'playwright/valid-title': 'error', + 'import-x-alias/no-extraneous-dependencies': [ + 'error', + { + devDependencies: ['**/tests/**', '**/e2e/**', '**/playwright/**'], + optionalDependencies: false, + }, + ], + }, + overrides: [ + { + files: [ + 'Types.ts', + 'composables/BuilderWizardComposer.ts', + 'composables/CanvasComposer.ts', + 'composables/CredentialsComposer.ts', + 'composables/DataTablesComposer.ts', + 'composables/ExecutionsComposer.ts', + 'composables/MfaComposer.ts', + 'composables/NodeDetailsViewComposer.ts', + 'composables/OidcComposer.ts', + 'composables/PartialExecutionComposer.ts', + 'composables/ProjectComposer.ts', + 'composables/TemplatesComposer.ts', + 'composables/TestEntryComposer.ts', + 'composables/WorkflowComposer.ts', + 'helpers/ClipboardHelper.ts', + 'helpers/NavigationHelper.ts', + 'helpers/NodeParameterHelper.ts', + 'pages/AIAssistantPage.ts', + 'pages/AgentBuilderPage.ts', + 'pages/AgentSessionsPage.ts', + 'pages/BasePage.ts', + 'pages/CanvasPage.ts', + 'pages/ChatHubChatPage.ts', + 'pages/ChatHubPersonalAgentsPage.ts', + 'pages/ChatHubSettingsPage.ts', + 'pages/ChatHubWorkflowAgentsPage.ts', + 'pages/CommunityNodesPage.ts', + 'pages/CredentialsPage.ts', + 'pages/DataTableDetails.ts', + 'pages/DataTableView.ts', + 'pages/DemoPage.ts', + 'pages/EvaluationComparePage.ts', + 'pages/ExecutionsPage.ts', + 'pages/InstanceAiPage.ts', + 'pages/KeycloakLoginPage.ts', + 'pages/MfaLoginPage.ts', + 'pages/MfaSetupModal.ts', + 'pages/NodeDetailsViewPage.ts', + 'pages/NpsSurveyPage.ts', + 'pages/OAuthConsentPage.ts', + 'pages/ProjectSettingsPage.ts', + 'pages/PublicFormPage.ts', + 'pages/SecretsProviderSettingsPage.ts', + 'pages/SecuritySettingsPage.ts', + 'pages/SettingsEnvironmentPage.ts', + 'pages/SettingsLogStreamingPage.ts', + 'pages/SettingsMcpPage.ts', + 'pages/SettingsPersonalPage.ts', + 'pages/SettingsSsoPage.ts', + 'pages/SettingsUsersPage.ts', + 'pages/SidebarPage.ts', + 'pages/SignInPage.ts', + 'pages/SourceControlPullModal.ts', + 'pages/SourceControlPushModal.ts', + 'pages/TemplateCredentialSetupPage.ts', + 'pages/TemplatesPage.ts', + 'pages/VariablesPage.ts', + 'pages/VersionsPage.ts', + 'pages/WorkerViewPage.ts', + 'pages/WorkflowActivationModal.ts', + 'pages/WorkflowCredentialSetupModal.ts', + 'pages/WorkflowReviewsPage.ts', + 'pages/WorkflowSettingsModal.ts', + 'pages/WorkflowSharingModal.ts', + 'pages/WorkflowsPage.ts', + 'pages/n8nPage.ts', + 'pages/components/AIBuilderPage.ts', + 'pages/components/ActionToggle.ts', + 'pages/components/AddResource.ts', + 'pages/components/BaseModal.ts', + 'pages/components/Breadcrumbs.ts', + 'pages/components/BuilderSetupWizard.ts', + 'pages/components/ChatAgentCard.ts', + 'pages/components/ChatHubCredentialModal.ts', + 'pages/components/ChatHubPersonalAgentModal.ts', + 'pages/components/ChatHubProviderSettingsModal.ts', + 'pages/components/ChatHubSidebar.ts', + 'pages/components/ChatHubToolsModal.ts', + 'pages/components/CodeNodeEditor.ts', + 'pages/components/CommandBar.ts', + 'pages/components/ConvertToSubworkflowModal.ts', + 'pages/components/CredentialModal.ts', + 'pages/components/DeleteSecretsProviderModal.ts', + 'pages/components/FloatingUiHelper.ts', + 'pages/components/FocusPanel.ts', + 'pages/components/InlineExpressionEditor.ts', + 'pages/components/InstanceAiSidebar.ts', + 'pages/components/InstanceAiWorkflowSetup.ts', + 'pages/components/InteractionsPage.ts', + 'pages/components/LogsPanel.ts', + 'pages/components/ManualChatModal.ts', + 'pages/components/NodeCreator.ts', + 'pages/components/NodeCredentials.ts', + 'pages/components/NotificationsPage.ts', + 'pages/components/ProjectHeader.ts', + 'pages/components/ProjectTabsComponent.ts', + 'pages/components/ResourceCards.ts', + 'pages/components/ResourceLocator.ts', + 'pages/components/ResourceMoveModal.ts', + 'pages/components/RunDataPanel.ts', + 'pages/components/SaveChangesModal.ts', + 'pages/components/SecretsProviderConnectionModal.ts', + 'pages/components/SettingsSidebar.ts', + 'pages/components/SetupWorkflowCredentialsModal.ts', + 'pages/components/StickyComponent.ts', + 'pages/components/TagsManagerModal.ts', + 'pages/components/VariableModal.ts', + 'pages/components/WorkflowMenu.ts', + 'pages/components/WorkflowReviewControls.ts', + 'pages/components/dialogLocators.ts', + 'pages/components/messageBoxLocators.ts', + 'pages/components/nodes/EditFieldsNode.ts', + 'tests/e2e/regression/ADO-1338-ndv-missing-input-panel.spec.ts', + 'tests/e2e/regression/ADO-2230-ndv-reset-data-pagination.spec.ts', + 'tests/e2e/regression/ADO-2372-prevent-clipping-params.spec.ts', + 'tests/e2e/regression/ADO-2929-can-load-old-switch-node-workflows.spec.ts', + 'tests/e2e/regression/ADO-4462-template-setup-experiment.spec.ts', + 'tests/e2e/regression/ADO-5630-execute-button-layout.spec.ts', + 'tests/e2e/regression/ADO-5808-stale-node-execution-data.spec.ts', + 'tests/e2e/regression/AI-1401-sub-nodes-input-panel.spec.ts', + 'tests/e2e/regression/AI-2795-chat-panel-header-height.spec.ts', + 'tests/e2e/regression/AI-716-correctly-set-up-agent-model-shows-error.spec.ts', + 'tests/e2e/regression/AI-812-partial-execs-broken-when-using-chat-trigger.spec.ts', + 'tests/e2e/regression/CAT-2395-node-spacing.spec.ts', + 'tests/e2e/regression/CAT-726-canvas-node-connectors-not-rendered-when-nodes-inserted.spec.ts', + 'tests/e2e/regression/PAY-4367-node-shifting-cyclic.spec.ts', + 'tests/e2e/regression/SUG-121-fields-reset-after-closing-ndv.spec.ts', + 'tests/e2e/regression/SUG-38-inline-expression-preview.spec.ts', + ], + rules: { 'unicorn/filename-case': 'off' }, + }, + { + files: [ + 'fixtures/langsmith.ts', + 'fixtures/quarantine.ts', + 'tests/cli-workflows/workflow-tests.spec.ts', + 'tests/e2e/chat-hub/fixtures.ts', + 'tests/e2e/instance-ai/fixtures.ts', + 'tests/e2e/instance-ai/instance-ai-workflow-setup.spec.ts', + 'tests/e2e/workflows/editor/execution/fixtures.ts', + 'tests/framework/consumers.ts', + ], + rules: { 'no-empty-pattern': 'off' }, + }, + { + files: [ + 'composables/BuilderWizardComposer.ts', + 'helpers/ClipboardHelper.ts', + 'pages/PublicFormPage.ts', + 'pages/SettingsUsersPage.ts', + 'pages/SourceControlPushModal.ts', + 'reporters/ci-metrics.test.ts', + 'services/tag-api-helper.ts', + 'services/variables-api-helper.ts', + 'tests/cli-workflows/setup-workflow-tests.ts', + 'tests/e2e/projects/projects.spec.ts', + 'tests/e2e/workflows/editor/expressions/quickjs-engine.spec.ts', + 'tests/e2e/workflows/executions/list.spec.ts', + 'tests/e2e/workflows/templates/templates.spec.ts', + 'tests/evals/_smoke/langsmith-fixture.spec.ts', + 'tests/evals/instance-ai/weather-alert.spec.ts', + 'tests/infrastructure/benchmarks/ui/executions-list-customer-scale.spec.ts', + 'utils/benchmark/kafka-driver.ts', + ], + rules: { 'typescript/promise-function-async': 'off' }, + }, + ], +}); diff --git a/packages/testing/playwright/package.json b/packages/testing/playwright/package.json index 6ec8fcfba3b..9183db073be 100644 --- a/packages/testing/playwright/package.json +++ b/packages/testing/playwright/package.json @@ -48,8 +48,9 @@ "prepare-test-image": "node scripts/build-test-image.mjs", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", + "lint:verify-config": "node scripts/verify-lint-config.mjs", "typecheck": "tsc --noEmit", "janitor": "tsx ../janitor/src/cli.ts", "janitor:fix": "tsx ../janitor/src/cli.ts --fix --write" @@ -59,6 +60,7 @@ "@axe-core/playwright": "^4.13.0", "@currents/playwright": "catalog:e2e", "@n8n/eslint-config": "workspace:*", + "@n8n/oxlint-config": "workspace:*", "@types/debug": "^4.1.12", "@memlab/api": "2.0.0", "@memlab/heap-analysis": "2.0.0", @@ -93,6 +95,8 @@ "nanoid": "catalog:", "monocart-coverage-reports": "^2.12.0", "otplib": "^12.0.1", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "ts-morph": "catalog:", "tsx": "catalog:", "typescript": "catalog:typescript", diff --git a/packages/testing/playwright/scripts/verify-lint-config.mjs b/packages/testing/playwright/scripts/verify-lint-config.mjs new file mode 100644 index 00000000000..c58119f69d7 --- /dev/null +++ b/packages/testing/playwright/scripts/verify-lint-config.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const packageDirectory = join(dirname(fileURLToPath(import.meta.url)), '..'); +const require = createRequire(join(packageDirectory, 'package.json')); +const playwrightPlugin = require('eslint-plugin-playwright'); +const oxlintConfig = await import(pathToFileURL(join(packageDirectory, 'oxlint.config.mts')).href); +const { legacyFilenameCaseFiles } = await import( + pathToFileURL(join(packageDirectory, 'lint-filename-debt.mjs')).href +); + +const eslintRules = { + ...playwrightPlugin.configs['flat/recommended'].rules, + 'playwright/no-conditional-in-test': 'error', +}; + +const errorRuleIds = (rules) => + Object.entries(rules) + .filter(([id, severity]) => { + const value = Array.isArray(severity) ? severity[0] : severity; + return id.startsWith('playwright/') && (value === 'error' || value === 2); + }) + .map(([id]) => id) + .sort(); + +const expected = errorRuleIds(eslintRules); +const actual = errorRuleIds(oxlintConfig.default.rules ?? {}); + +if (JSON.stringify(actual) !== JSON.stringify(expected)) { + console.error('Playwright error-rule parity failed.'); + console.error(`ESLint: ${expected.join(', ')}`); + console.error(`Oxlint: ${actual.join(', ')}`); + process.exit(1); +} + +const filenameCaseOverride = oxlintConfig.default.overrides?.find( + ({ rules }) => rules?.['unicorn/filename-case'] === 'off', +); +const oxlintFilenameCaseFiles = [...(filenameCaseOverride?.files ?? [])].sort(); +const eslintFilenameCaseFiles = [...legacyFilenameCaseFiles].sort(); + +if (JSON.stringify(oxlintFilenameCaseFiles) !== JSON.stringify(eslintFilenameCaseFiles)) { + console.error('Playwright filename-case debt parity failed.'); + process.exit(1); +} + +console.log(`Playwright error-rule parity passed for ${actual.length} rules.`); +console.log( + `Playwright filename-case debt parity passed for ${oxlintFilenameCaseFiles.length} files.`, +); diff --git a/packages/testing/rules-engine/eslint.config.mjs b/packages/testing/rules-engine/eslint.config.mjs index ca86c38e133..5ba0f921fd5 100644 --- a/packages/testing/rules-engine/eslint.config.mjs +++ b/packages/testing/rules-engine/eslint.config.mjs @@ -1,24 +1,6 @@ import { defineConfig } from 'eslint/config'; import { backendConfig } from '@n8n/eslint-config/backend'; -export default defineConfig( - backendConfig, - { - ignores: ['coverage/**', 'dist/**'], - }, - { - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: 'objectLiteralProperty', - format: null, - filter: { - regex: '^[a-z]+-[a-z-]+$', - match: true, - }, - }, - ], - }, - }, -); +export default defineConfig(backendConfig, { + ignores: ['coverage/**', 'dist/**'], +}); diff --git a/packages/testing/rules-engine/oxlint.config.mts b/packages/testing/rules-engine/oxlint.config.mts new file mode 100644 index 00000000000..002b0ae4000 --- /dev/null +++ b/packages/testing/rules-engine/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: ['coverage/**'], +}); diff --git a/packages/testing/rules-engine/package.json b/packages/testing/rules-engine/package.json index 1a0e65f4275..2344e779e89 100644 --- a/packages/testing/rules-engine/package.json +++ b/packages/testing/rules-engine/package.json @@ -28,8 +28,8 @@ "test:watch": "vitest", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", + "lint": "oxlint --quiet", + "lint:fix": "oxlint --fix", "typecheck": "tsc --noEmit" }, "license": "MIT", @@ -37,8 +37,12 @@ "ts-morph": "catalog:" }, "devDependencies": { + "@n8n/eslint-config": "workspace:*", + "@n8n/oxlint-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "@vitest/coverage-v8": "catalog:", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", "typescript": "catalog:typescript", "vitest": "catalog:" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3a78e89ba3..3ed719072b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7769,6 +7769,12 @@ importers: specifier: 'catalog:' version: 2.8.3 devDependencies: + '@n8n/eslint-config': + specifier: workspace:* + version: link:../../@n8n/eslint-config + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../../@n8n/oxlint-config '@types/markdown-it': specifier: 'catalog:' version: 13.0.9 @@ -7778,6 +7784,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 vitest: specifier: 'catalog:' version: 5.0.1(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/browser-playwright@5.0.1)(@vitest/coverage-istanbul@5.0.1)(@vitest/coverage-v8@5.0.1)(@vitest/ui@5.0.1)(jsdom@23.0.1(bufferutil@4.0.9)(supports-color@5.5.0)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) @@ -7818,6 +7830,18 @@ importers: specifier: catalog:undici-v7 version: 7.29.1 devDependencies: + '@n8n/eslint-config': + specifier: workspace:* + version: link:../../@n8n/eslint-config + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../../@n8n/oxlint-config + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 tsx: specifier: 'catalog:' version: 4.19.3 @@ -7849,12 +7873,24 @@ importers: specifier: 'catalog:' version: 2.8.3 devDependencies: + '@n8n/eslint-config': + specifier: workspace:* + version: link:../../@n8n/eslint-config + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../../@n8n/oxlint-config '@types/node': specifier: ^20.17.50 version: 20.19.41 '@vitest/coverage-v8': specifier: 'catalog:' version: 5.0.1(@vitest/browser@5.0.1)(vitest@5.0.1) + 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 @@ -7909,6 +7945,9 @@ importers: '@n8n/eslint-config': specifier: workspace:* version: link:../../@n8n/eslint-config + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../../@n8n/oxlint-config '@n8n/permissions': specifier: workspace:* version: link:../../@n8n/permissions @@ -7996,6 +8035,12 @@ importers: otplib: specifier: ^12.0.1 version: 12.0.1 + oxlint: + specifier: 'catalog:' + version: 1.78.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: + specifier: 'catalog:' + version: 0.21.1 ts-morph: specifier: 'catalog:' version: 27.0.2 @@ -8018,12 +8063,24 @@ importers: specifier: 'catalog:' version: 27.0.2 devDependencies: + '@n8n/eslint-config': + specifier: workspace:* + version: link:../../@n8n/eslint-config + '@n8n/oxlint-config': + specifier: workspace:* + version: link:../../@n8n/oxlint-config '@n8n/vitest-config': specifier: workspace:* version: link:../../@n8n/vitest-config '@vitest/coverage-v8': specifier: 'catalog:' version: 5.0.1(@vitest/browser@5.0.1)(vitest@5.0.1) + 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 93e01efe22a..66f08651406 100644 --- a/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md +++ b/scripts/lint-parity/OXLINT_MIGRATION_BLOCKERS.md @@ -41,6 +41,12 @@ Possible approaches: Audit every package configuration before conversion. Do not copy the full generic naming rule into a new custom implementation. +### Testing infrastructure decisions + +- `@n8n/rules-engine`, `@n8n/code-health`, and `@n8n/playwright-janitor`: retire the exemption-only rule-ID selectors. They enforce no positive naming contract. +- `n8n-containers`: retire the Docker label exemption. Object literal keys are data, not identifier contracts. +- `n8n-playwright`: retire the broad style selectors. Workflow names, fixture keys, and spec paths are data, while identifier casing has no runtime contract. + ## Retirement exceptions ### `n8n-node-dev` diff --git a/scripts/lint-parity/oxlint-parity.mjs b/scripts/lint-parity/oxlint-parity.mjs index a107e6e4e6c..031b268cefd 100644 --- a/scripts/lint-parity/oxlint-parity.mjs +++ b/scripts/lint-parity/oxlint-parity.mjs @@ -64,6 +64,7 @@ const JS_PLUGIN_NAMESPACES = new Set([ 'lodash', 'unused-imports', 'n8n-local-rules', + 'playwright', ]); function splitId(id) { @@ -232,7 +233,12 @@ for (const id of eslintRules) { } const extra = [...oxlintRules].filter((id) => !expected.has(id)).sort(); -const isDocumented = ({ id, native: target }) => id in gap || (target !== null && target in gap); +const hasApplicableGap = (id) => { + const entry = gap[id]; + return entry !== undefined && (!entry.packages || entry.packages.includes(pkgDir)); +}; +const isDocumented = ({ id, native: target }) => + hasApplicableGap(id) || (target !== null && hasApplicableGap(target)); const undocumented = missing.filter((entry) => !isDocumented(entry)); console.log(`${pkgDir}: ${sampled} sample files, ${eslintRules.size} ESLint rules at error`); diff --git a/scripts/lint-parity/samples.json b/scripts/lint-parity/samples.json index e30699a67a4..c1fb919bfcc 100644 --- a/scripts/lint-parity/samples.json +++ b/scripts/lint-parity/samples.json @@ -378,8 +378,9 @@ "nodes/Aws/__tests__/AwsLambda.node.test.ts" ], "packages/testing/code-health": ["src/cli.ts", "src/cli/arg-parser.test.ts"], + "packages/testing/containers": ["services/keycloak.ts", "tests/resource-tracker.test.ts"], "packages/testing/janitor": ["src/cli.test.ts", "src/cli.ts"], - "packages/testing/playwright": ["coverage-options.test.ts"], + "packages/testing/playwright": ["coverage-options.test.ts", "fixtures/base.ts"], "packages/testing/rules-engine": ["src/ast/ast-rule.ts", "src/ast/imports.test.ts"], "packages/testing/test-impact": ["src/changes.test.ts", "src/changes.ts"], "packages/workflow": [ diff --git a/turbo.json b/turbo.json index e278c292277..4c2e8b8a289 100644 --- a/turbo.json +++ b/turbo.json @@ -80,6 +80,9 @@ "@n8n/cli#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, + "@n8n/code-health#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, "@n8n/client-oauth2#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, @@ -119,6 +122,12 @@ "@n8n/n8n-benchmark#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, + "@n8n/playwright-janitor#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "@n8n/rules-engine#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, "@n8n/scheduler#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, @@ -131,6 +140,12 @@ "n8n-core#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] }, + "n8n-containers#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, + "n8n-playwright#lint": { + "dependsOn": ["^build", "@n8n/oxlint-config#build"] + }, "n8n#lint": { "dependsOn": ["^build", "@n8n/oxlint-config#build"] },