@flowfuse/flowfuse
Version:
An open source low-code development platform
411 lines (387 loc) • 17.4 kB
JavaScript
import js from '@eslint/js'
import tsPlugin from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import cypressPlugin from 'eslint-plugin-cypress'
import { flatConfigs as importXConfigs } from 'eslint-plugin-import-x'
import noOnlyTests from 'eslint-plugin-no-only-tests'
import vuePlugin from 'eslint-plugin-vue'
import globals from 'globals'
import neostandard, { resolveIgnoresFromGitignore } from 'neostandard'
export default [
// ignorePatterns (also honour .gitignore, like the other repos' configs)
{
ignores: [
...resolveIgnoresFromGitignore(),
'frontend/dist/',
'var/',
'**/*.svg',
'**/*.xml',
'**/*.d.ts',
'frontend/src/types/generated.ts'
]
},
// TODO(eslint-v9-follow-up): the ESLint 8 -> 9 / neostandard rule-set change left a number
// of now-stale inline `eslint-disable` comments in the codebase (for n/no-process-exit,
// n/no-extraneous-import, n/no-unpublished-require, n/handle-callback-err, no-unused-vars,
// no-useless-return, no-template-curly-in-string, promise/no-callback-in-promise,
// vue/one-component-per-file, etc.). They surface as "Unused eslint-disable directive"
// warnings. Silencing the report here to keep this migration PR small; remove this block
// and delete the stale directives in the follow-up PR.
{
linterOptions: {
reportUnusedDisableDirectives: 'off'
}
},
// extends: standard, plugin:import/recommended, plugin:promise/recommended, plugin:n/recommended
// neostandard is the flat-config successor to eslint-config-standard and registers
// (globally) the n and promise plugins plus @stylistic, subsuming the old promise/n
// recommended extends.
// NB: neostandard bundled eslint-plugin-import-x through 0.12.2 but DROPPED it in 0.12.7+
// (and 0.13.x), so plugin:import/recommended is NOT covered by neostandard any more — we
// register eslint-plugin-import-x explicitly below. Original `import/*` rules map to
// `import-x/*` (the eslint-plugin-import-x fork neostandard historically used).
js.configs.recommended,
...neostandard(),
// plugin:import/recommended — registers the `import-x` plugin globally and enables
// no-unresolved, named, namespace, default, export, no-duplicates, etc.
importXConfigs.recommended,
// Root config — env: { es2022: true, commonjs: true }
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'commonjs',
globals: {
...globals.node,
...globals.commonjs
}
},
settings: {
'import-x/core-modules': ['lottie-web-vue', '@heroicons/vue'],
n: {
allowModules: [
'flowforge-test-utils', // exists at test/node_modules/flowforge-test-utils
'promptly',
'lottie-web-vue',
'@heroicons/vue'
]
}
},
plugins: {
// @stylistic, import-x, n and promise are already registered globally by neostandard
'no-only-tests': noOnlyTests
},
rules: {
// Inbuilt
// SwitchCase: 0 matches the old core `indent: ['error', 4]` default
// (@stylistic/indent defaults SwitchCase to 1); the codebase aligns `case`
// with `switch`, so without this every switch statement is flagged.
'@stylistic/indent': ['error', 4, { SwitchCase: 0 }],
'object-shorthand': ['error'],
'sort-imports': [
'error',
{
ignoreDeclarationSort: true
}
],
'no-console': ['error', { allow: ['debug', 'info', 'warn', 'error'] }],
// plugin:import (import-x under neostandard)
'import-x/order': [
'error',
{
alphabetize: {
order: 'asc'
},
'newlines-between': 'always-and-inside-groups'
}
],
'import-x/no-unresolved': 'error',
// plugin:n
'n/file-extension-in-import': 'error',
'n/no-missing-import': 'error',
'n/no-missing-require': 'error',
// plugin:no-only-tests
'no-only-tests/no-only-tests': 'error',
// plugin:promise
'promise/catch-or-return': ['error', { allowFinally: true }]
}
},
// ESM config/tooling files (e.g. this eslint.config.mjs) — the root block sets
// sourceType: 'commonjs' for everything, which mis-parses .mjs import/export.
{
files: ['**/*.mjs'],
languageOptions: {
sourceType: 'module'
}
},
// Frontend runs in the browser and builds with webpack
// extends: plugin:vue/vue3-recommended
// Scope every Vue config object to frontend/** — eslint-plugin-vue's flat base
// config has no `files` key and sets sourceType: 'module' globally, which would
// otherwise force the CommonJS forge/** backend into module (strict) mode. The
// original .eslintrc only applied the Vue rules under frontend/**.
...vuePlugin.configs['flat/recommended'].map(config => ({
...config,
files: ['frontend/**']
})),
{
files: ['frontend/**/*.js', 'frontend/**/*.vue'],
languageOptions: {
sourceType: 'module',
globals: {
...globals.browser
},
parserOptions: {
parser: {
ts: tsParser
}
}
},
rules: {
// plugin:vue
'vue/html-indent': ['warn', 4],
// plugin:vue - style rules
'vue/max-attributes-per-line': 'off',
'vue/attribute-hyphenation': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/component-definition-name-casing': 'off',
// TODO(eslint-v9-follow-up): vue/no-required-prop-with-default is new in
// eslint-plugin-vue v10 and flags a required prop that also declares a default.
// Disabled to keep this migration PR small; fix the offending prop(s) and remove
// this in the follow-up PR.
'vue/no-required-prop-with-default': 'off',
// plugin:promise
'promise/always-return': 'off', // common Vue.js pattern
// Frontend runs through webpack, not Node — Node-based resolution rules
// do not understand webpack aliases (@/) or .vue file resolution
'import-x/no-unresolved': 'off',
'n/no-missing-import': 'off',
'n/no-missing-require': 'off',
// webpack resolves extensions, not Node; frontend import style is deliberately
// mixed (some with .js, some bare) so don't enforce Node's extension rule here
'n/file-extension-in-import': 'off'
}
},
// TypeScript files in the frontend — must come after the Vue/frontend block so
// @typescript-eslint/parser wins for .ts files (later configs take precedence)
// extends: plugin:@typescript-eslint/recommended
{
files: ['frontend/src/**/*.ts'],
languageOptions: {
parser: tsParser,
sourceType: 'module',
globals: {
...globals.browser
}
},
plugins: {
'@typescript-eslint': tsPlugin
},
rules: {
...tsPlugin.configs['eslint-recommended'].overrides?.[0]?.rules,
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'warn',
'import-x/no-unresolved': 'off',
'n/no-missing-import': 'off',
'n/no-missing-require': 'off',
// webpack/ts resolve extensions, not Node; .ts files were never linted by the
// v8 lint script (its globs omitted *.ts), so this Node rule shouldn't apply here
'n/file-extension-in-import': 'off'
}
},
// UI components use kebab-case
{
files: ['frontend/src/ui-components/**'],
rules: {
'vue/component-definition-name-casing': ['error', 'kebab-case'],
'vue/first-attribute-linebreak': 'off',
'vue/html-self-closing': 'off'
}
},
// System, unit, and E2E tests run using Mocha
{
files: ['test/**'],
languageOptions: {
globals: {
...globals.mocha
}
}
},
// Cypress is used for E2E
// extends: eslint-plugin-cypress recommended (v6 — flat config is the package's main
// entry; the deprecated `/flat` subpath was removed in v6)
// Keep ecmaVersion pinned to 2022 (matches the root parserOptions.ecmaVersion) so modern
// syntax parses. v6's recommended no longer pins ecmaVersion, but this stays defensive.
{
files: ['test/e2e/**'],
plugins: cypressPlugin.configs.recommended.plugins,
languageOptions: {
...cypressPlugin.configs.recommended.languageOptions,
ecmaVersion: 2022
},
rules: {
...cypressPlugin.configs.recommended.rules,
// plugin:cypress
'cypress/require-data-selectors': 'warn',
'cypress/assertion-before-screenshot': 'error',
'cypress/no-force': 'warn',
'cypress/no-pause': 'error',
// plugin:promise
'promise/always-return': 'off',
'promise/catch-or-return': 'off'
}
},
// Front end tests are modules and run in jsdom (browser globals available)
{
files: ['test/unit/frontend/**', 'test/e2e/frontend/**'],
languageOptions: {
sourceType: 'module',
globals: {
...globals.browser
}
}
},
// Ignore unresolved @ imports and Node import errors only for test stores files, has issues with aliases
{
files: ['test/unit/frontend/stores/**/*'],
rules: {
'import-x/no-unresolved': [2, { ignore: ['^@/'] }],
'n/no-missing-import': 'off'
}
},
// ==========================================================================
// Auto-generated rule overrides for old files that fail on newly introduced rules
// ==========================================================================
{
files: [
'test/e2e/frontend/cypress/tests/admin.spec.js',
'test/e2e/frontend/cypress/tests/admin/instance-types.spec.js',
'test/e2e/frontend/cypress/tests/admin/stacks.spec.js',
'test/e2e/frontend/cypress/tests/admin/templates.spec.js',
'test/e2e/frontend/cypress/tests/applications.spec.js',
'test/e2e/frontend/cypress/tests/auth.spec.js',
'test/e2e/frontend/cypress/tests/devices.spec.js',
'test/e2e/frontend/cypress/tests/instances.spec.js',
'test/e2e/frontend/cypress/tests/instances/devices.spec.js',
'test/e2e/frontend/cypress/tests/instances/snapshots.spec.js',
'test/e2e/frontend/cypress/tests/instances/staging.spec.js',
'test/e2e/frontend/cypress/tests/invitations.spec.js',
'test/e2e/frontend/cypress/tests/team/team.spec.js',
'test/e2e/frontend/cypress/tests/team/audit-log.spec.js',
'test/e2e/frontend/cypress/tests/team/team-membership.spec.js',
'test/e2e/frontend/cypress/tests/terms-and-conditions.spec.js'
],
rules: {
'cypress/require-data-selectors': 'off'
}
},
{
files: [
'frontend/src/pages/instance/Overview.vue'
],
rules: {
'promise/catch-or-return': 'off'
}
},
{
files: [
'frontend/src/components/auth/UpdateExpiredPassword.vue',
'frontend/src/components/FormRow.vue',
'frontend/src/components/NavItem.vue',
'frontend/src/components/PageHeader.vue',
'frontend/src/components/SectionTopMenu.vue',
'frontend/src/components/TeamSelection.vue',
'frontend/src/pages/device/Overview.vue',
'frontend/src/pages/device/Settings/Danger.vue',
'frontend/src/pages/device/Settings/dialogs/ConfirmDeviceDeleteDialog.vue',
'frontend/src/pages/device/Settings/Environment.vue',
'frontend/src/pages/device/Settings/General.vue',
'frontend/src/pages/team/components/MemberSummaryList.vue',
'frontend/src/pages/team/components/ProjectSummaryList.vue',
'frontend/src/pages/team/Devices/dialogs/CreateProvisioningTokenDialog.vue'
],
rules: {
'vue/attributes-order': 'off'
}
},
{
files: [
'frontend/src/components/Accordion.vue',
'frontend/src/components/CodePreviewer.vue',
'frontend/src/components/Loading.vue',
'frontend/src/layouts/Box.vue',
'frontend/src/layouts/Platform.vue',
'frontend/src/main.js'
],
rules: {
'vue/component-definition-name-casing': 'off'
}
},
{
files: [
'frontend/src/components/audit-log/AuditEntry.vue',
'frontend/src/components/audit-log/AuditEntryIcon.vue',
'frontend/src/components/audit-log/AuditEntryVerbose.vue',
'frontend/src/components/audit-log/AuditLog.vue',
'frontend/src/components/auth/UpdateExpiredPassword.vue',
'frontend/src/components/DropdownMenu.vue',
'frontend/src/components/FormRow.vue',
'frontend/src/components/PageHeader.vue',
'frontend/src/components/SectionTopMenu.vue',
'frontend/src/components/tables/cells/InviteUserCell.vue',
'frontend/src/components/TeamSelection.vue',
'frontend/src/pages/device/components/DeviceLastSeenBadge.vue',
'frontend/src/pages/device/Overview.vue',
'frontend/src/pages/device/Settings/Danger.vue',
'frontend/src/pages/device/Settings/dialogs/ConfirmDeviceDeleteDialog.vue',
'frontend/src/pages/device/Settings/Environment.vue',
'frontend/src/pages/device/Settings/General.vue',
'frontend/src/pages/device/Settings/index.vue',
'frontend/src/pages/team/components/LibraryEntryTypeIcon.vue',
'frontend/src/pages/team/components/MemberSummaryList.vue',
'frontend/src/pages/team/components/ProjectSummaryList.vue',
'frontend/src/pages/team/components/TeamUserEditButton.vue',
'frontend/src/pages/team/create.vue',
'frontend/src/pages/team/Devices/dialogs/CreateProvisioningTokenDialog.vue',
'frontend/src/pages/team/Devices/dialogs/DeviceCredentialsDialog.vue',
'frontend/src/pages/team/Devices/dialogs/ProvisioningCredentialsDialog.vue',
'frontend/src/pages/team/dialogs/ChangeTeamRoleDialog.vue',
'frontend/src/pages/team/dialogs/ConfirmTeamDeleteDialog.vue',
'frontend/src/pages/team/dialogs/ConfirmTeamUserRemoveDialog.vue'
],
rules: {
'vue/order-in-components': 'off'
}
},
{
files: [
'frontend/src/components/DropdownMenu.vue',
'frontend/src/components/FormRow.vue',
'frontend/src/components/SectionSideMenu.vue',
'frontend/src/components/SideNavigation.vue',
'frontend/src/components/tables/cells/InviteUserCell.vue',
'frontend/src/components/tables/cells/TeamCell.vue',
'frontend/src/components/tables/cells/TeamTypeCell.vue',
'frontend/src/components/tables/cells/UserCell.vue',
'frontend/src/components/tables/cells/UserRoleCell.vue',
'frontend/src/pages/application/Debug.vue',
'frontend/src/pages/device/components/DeviceLastSeenBadge.vue',
'frontend/src/pages/device/Overview.vue',
'frontend/src/pages/device/Settings/Danger.vue',
'frontend/src/pages/device/Settings/Environment.vue',
'frontend/src/pages/device/Settings/General.vue',
'frontend/src/pages/device/Settings/index.vue',
'frontend/src/pages/instance/components/ExportInstanceComponents.vue',
'frontend/src/pages/instance/components/ImportInstanceComponents.vue',
'frontend/src/pages/team/components/MemberSummaryList.vue',
'frontend/src/pages/team/components/ProjectSummaryList.vue',
'frontend/src/pages/team/components/ProjectTypeSummary.vue',
'frontend/src/pages/team/components/TeamUserEditButton.vue',
'frontend/src/pages/team/Devices/dialogs/CreateProvisioningTokenDialog.vue',
'frontend/src/pages/team/Devices/dialogs/DeviceCredentialsDialog.vue',
'frontend/src/pages/team/Devices/dialogs/ProvisioningCredentialsDialog.vue'
],
rules: {
'vue/require-prop-types': 'off'
}
}
]