eslint.config.mjs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. // Shared ESLint flat config for the Overleaf web service.
  2. //
  3. // Mirrors what services/web/.eslintrc.js used to provide before the
  4. // flat-config migration.
  5. import { defineConfig, globalIgnores } from 'eslint/config'
  6. import tsParser from '@typescript-eslint/parser'
  7. import overleaf from '@overleaf/eslint-plugin'
  8. import globals from 'globals'
  9. import mocha from 'eslint-plugin-mocha'
  10. import chaiExpect from 'eslint-plugin-chai-expect'
  11. import chaiFriendly from 'eslint-plugin-chai-friendly'
  12. import vitest from '@vitest/eslint-plugin'
  13. import unicorn from 'eslint-plugin-unicorn'
  14. import cypress from 'eslint-plugin-cypress/flat'
  15. import testingLibrary from 'eslint-plugin-testing-library'
  16. import jsxA11Y from 'eslint-plugin-jsx-a11y'
  17. import reactPlugin from 'eslint-plugin-react'
  18. import reactHooks from 'eslint-plugin-react-hooks'
  19. import storybook from 'eslint-plugin-storybook'
  20. import importPlugin from 'eslint-plugin-import'
  21. import n from 'eslint-plugin-n'
  22. import promise from 'eslint-plugin-promise'
  23. import typescriptEslint from '@typescript-eslint/eslint-plugin'
  24. import js from '@eslint/js'
  25. import prettier from 'eslint-config-prettier/flat'
  26. import { fixupPluginRules } from '@eslint/compat'
  27. // eslint-plugin-import ships its configs in eslintrc shape; wrap with
  28. // fixupPluginRules so its rules work in flat config.
  29. const importPluginFixed = fixupPluginRules(importPlugin)
  30. // eslint-plugin-react 7.x still calls the removed `context.getFilename()`
  31. // API; wrap with fixupPluginRules so the compat shim translates it under
  32. // ESLint v10. Build a derivative flat-recommended config that references
  33. // the wrapped plugin instead of the original.
  34. const react = fixupPluginRules(reactPlugin)
  35. const reactFlatRecommended = {
  36. ...reactPlugin.configs.flat.recommended,
  37. plugins: { react },
  38. }
  39. import _ from 'lodash'
  40. import confusingBrowserGlobals from 'confusing-browser-globals'
  41. export default defineConfig([
  42. // Declare which file extensions ESLint should consider in this workspace.
  43. // Replaces the previous `--ext .js,.jsx,.mjs,.ts,.tsx` CLI flag, which is
  44. // silently ignored under ESLint v9 + flat config.
  45. {
  46. files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
  47. },
  48. {
  49. languageOptions: {
  50. parser: tsParser,
  51. // Default to node globals for all backend-style files; the
  52. // frontend block below replaces with browser globals where
  53. // appropriate. Matches what the old monorepo-root .eslintrc
  54. // used to provide via env: { node: true }, plus the three
  55. // browser globals eslint-config-standard@17 added universally.
  56. globals: {
  57. ...globals.node,
  58. document: 'readonly',
  59. navigator: 'readonly',
  60. window: 'readonly',
  61. },
  62. },
  63. // Match v8 default: don't flag stale `eslint-disable` directives.
  64. linterOptions: {
  65. reportUnusedDisableDirectives: 'off',
  66. },
  67. extends: [
  68. js.configs.recommended,
  69. typescriptEslint.configs['flat/recommended'],
  70. storybook.configs['flat/recommended'],
  71. ],
  72. plugins: {
  73. '@overleaf': overleaf,
  74. '@typescript-eslint': typescriptEslint,
  75. import: importPluginFixed,
  76. // Registered (no rules enabled) so that pre-existing
  77. // `eslint-disable n/handle-callback-err` (etc.) directives in
  78. // the source still resolve to a real rule name. Under v8 these
  79. // came in transitively via eslint-config-standard.
  80. n,
  81. promise,
  82. react,
  83. 'react-hooks': reactHooks,
  84. 'jsx-a11y': jsxA11Y,
  85. 'testing-library': testingLibrary,
  86. cypress,
  87. },
  88. settings: {
  89. // Tell eslint-plugin-react to detect which version of React we are using
  90. react: {
  91. version: 'detect',
  92. },
  93. },
  94. rules: {
  95. 'no-constant-binary-expression': 'error',
  96. 'no-restricted-globals': ['error', ...confusingBrowserGlobals],
  97. // do not allow importing of implicit dependencies.
  98. 'import/no-extraneous-dependencies': 'error',
  99. '@overleaf/prefer-kebab-url': 'error',
  100. '@overleaf/require-cio-snake-case-properties': 'error',
  101. // disable some TypeScript rules
  102. '@typescript-eslint/no-var-requires': 'off',
  103. '@typescript-eslint/no-unused-vars': 'off',
  104. '@typescript-eslint/no-empty-function': 'off',
  105. '@typescript-eslint/no-explicit-any': 'off',
  106. '@typescript-eslint/no-this-alias': 'off',
  107. '@typescript-eslint/no-non-null-assertion': 'off',
  108. '@typescript-eslint/ban-ts-comment': 'off',
  109. 'no-use-before-define': 'off',
  110. '@typescript-eslint/no-use-before-define': [
  111. 'error',
  112. {
  113. functions: false,
  114. classes: false,
  115. variables: false,
  116. },
  117. ],
  118. // The following three overrides preserve v8 behaviour the source
  119. // tree inherited from eslint-config-standard@17. See the matching
  120. // block in libraries/eslint-config/index.mjs for context.
  121. 'no-unused-vars': [
  122. 'error',
  123. {
  124. args: 'none',
  125. caughtErrors: 'none',
  126. ignoreRestSiblings: true,
  127. vars: 'all',
  128. },
  129. ],
  130. 'no-redeclare': ['error', { builtinGlobals: false }],
  131. 'no-empty': ['error', { allowEmptyCatch: true }],
  132. // ESLint v10's eslint:recommended added these three rules. Disable
  133. // to preserve the v9 zero-error baseline; revisit in a follow-up
  134. // cleanup PR.
  135. 'no-unassigned-vars': 'off',
  136. 'no-useless-assignment': 'off',
  137. 'preserve-caught-error': 'off',
  138. },
  139. },
  140. // NOTE: changing paths may require updating them in the Makefile too.
  141. {
  142. // Node
  143. files: [
  144. '**/app/src/**/*.{js,mjs}',
  145. 'app.{js,mjs}',
  146. 'i18next-scanner.config.js',
  147. 'scripts/**/*.{js,mjs}',
  148. 'webpack.config*.js',
  149. ],
  150. languageOptions: {
  151. globals: {
  152. ...globals.node,
  153. },
  154. },
  155. },
  156. {
  157. // Test specific rules
  158. files: ['**/test/**/*.{js,jsx,mjs,cjs,ts,tsx}'],
  159. ignores: ['**/test/unit/src/**/*.test.mjs', 'test/unit/bootstrap.mjs'], // exclude vitest files
  160. plugins: {
  161. mocha,
  162. 'chai-expect': chaiExpect,
  163. 'chai-friendly': chaiFriendly,
  164. },
  165. languageOptions: {
  166. globals: {
  167. ...globals.mocha,
  168. },
  169. },
  170. rules: {
  171. // mocha-specific rules
  172. 'mocha/handle-done-callback': 'error',
  173. 'mocha/no-exclusive-tests': 'error',
  174. 'mocha/no-global-tests': 'error',
  175. 'mocha/no-identical-title': 'error',
  176. 'mocha/no-nested-tests': 'error',
  177. 'mocha/no-pending-tests': 'error',
  178. 'mocha/no-mocha-arrows': 'error',
  179. // Swap the no-unused-expressions rule with a more chai-friendly one
  180. 'no-unused-expressions': 'off',
  181. 'chai-friendly/no-unused-expressions': 'error',
  182. // chai-specific rules
  183. 'chai-expect/missing-assertion': 'error',
  184. 'chai-expect/terminating-properties': 'error',
  185. // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
  186. // we don't enforce this at the top-level - just in tests to manage `this` scope
  187. // based on mocha's context mechanism
  188. 'mocha/prefer-arrow-callback': 'error',
  189. '@typescript-eslint/no-unused-expressions': 'off',
  190. },
  191. },
  192. {
  193. files: ['**/test/unit/src/**/*.test.mjs', 'test/unit/bootstrap.mjs'],
  194. languageOptions: {
  195. globals: {
  196. ...globals.jest, // best match for vitest API etc.
  197. },
  198. },
  199. plugins: {
  200. '@vitest': vitest,
  201. 'chai-expect': chaiExpect,
  202. 'chai-friendly': chaiFriendly, // still using chai for now
  203. },
  204. rules: {
  205. // vitest-specific rules
  206. '@vitest/no-focused-tests': 'error',
  207. '@vitest/no-disabled-tests': 'error',
  208. // Swap the no-unused-expressions rule with a more chai-friendly one
  209. 'no-unused-expressions': 'off',
  210. 'chai-friendly/no-unused-expressions': 'error',
  211. // chai-specific rules
  212. 'chai-expect/missing-assertion': 'error',
  213. 'chai-expect/terminating-properties': 'error',
  214. '@typescript-eslint/no-unused-expressions': 'off',
  215. '@overleaf/require-vi-doMock-valid-path': 'error',
  216. },
  217. },
  218. {
  219. // ES specific rules
  220. files: [
  221. '**/app/src/**/*.mjs',
  222. 'modules/*/index.mjs',
  223. 'app.mjs',
  224. 'scripts/**/*.mjs',
  225. 'migrations/**/*.mjs',
  226. '**/test/acceptance/src/**/*.mjs',
  227. '**/test/unit/src/**/*.mjs',
  228. ],
  229. ignores: [
  230. // migration template file
  231. 'migrations/lib/template.mjs',
  232. ],
  233. languageOptions: {
  234. sourceType: 'module',
  235. parserOptions: {},
  236. },
  237. plugins: {
  238. unicorn,
  239. },
  240. rules: {
  241. 'import/no-unresolved': [
  242. 'error',
  243. {
  244. // eslint-plugin-import does not support exports directive in package.json
  245. // https://github.com/import-js/eslint-plugin-import/issues/1810
  246. ignore: ['^p-queue$'],
  247. },
  248. ],
  249. 'import/named': 'error',
  250. 'import/default': 'error',
  251. 'import/extensions': [
  252. 'error',
  253. 'ignorePackages',
  254. {
  255. js: 'always',
  256. mjs: 'always',
  257. },
  258. ],
  259. 'unicorn/prefer-module': 'error',
  260. 'unicorn/prefer-node-protocol': 'error',
  261. },
  262. },
  263. {
  264. // Backend specific rules
  265. files: ['**/app/src/**/*.{js,mjs}', 'app.{js,mjs}'],
  266. languageOptions: {
  267. parserOptions: {
  268. tsconfigRootDir: import.meta.dirname,
  269. project: './tsconfig.backend.json',
  270. },
  271. },
  272. rules: {
  273. // do not allow importing of implicit dependencies.
  274. 'import/no-extraneous-dependencies': [
  275. 'error',
  276. {
  277. // do not allow importing of devDependencies.
  278. devDependencies: false,
  279. },
  280. ],
  281. 'no-restricted-syntax': [
  282. 'error',
  283. // do not allow node-fetch in backend code
  284. {
  285. selector:
  286. "CallExpression[callee.name='require'] > .arguments[value='node-fetch']",
  287. message:
  288. 'Requiring node-fetch is not allowed in production services, please use fetch-utils.',
  289. },
  290. // mongoose populate must set fields to populate
  291. {
  292. selector:
  293. "CallExpression[callee.property.name='populate'][arguments.length<2]",
  294. message:
  295. "Populate without a second argument returns the whole document. Use populate('field',['prop1','prop2']) instead",
  296. },
  297. // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
  298. {
  299. selector:
  300. "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
  301. message:
  302. 'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
  303. },
  304. // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
  305. {
  306. selector:
  307. "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
  308. message:
  309. "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
  310. },
  311. // Catch incorrect usage of `await db.collection.find()`
  312. {
  313. selector:
  314. "AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
  315. message:
  316. 'Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead.',
  317. },
  318. ],
  319. '@typescript-eslint/no-floating-promises': [
  320. 'error',
  321. {
  322. checkThenables: true,
  323. },
  324. ],
  325. },
  326. },
  327. {
  328. // Backend scripts specific rules
  329. files: ['**/scripts/**/*.{js,mjs}'],
  330. rules: {
  331. 'no-restricted-syntax': [
  332. 'error',
  333. // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
  334. {
  335. selector:
  336. "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
  337. message:
  338. 'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
  339. },
  340. // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
  341. {
  342. selector:
  343. "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
  344. message:
  345. "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
  346. },
  347. // Catch incorrect usage of `await db.collection.find()`
  348. {
  349. selector:
  350. "AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
  351. message:
  352. 'Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead.',
  353. },
  354. ],
  355. },
  356. },
  357. {
  358. // Insist on using Script Runner for new scripts. Old scripts should be
  359. // converted to use Script Runner in future, but are excluded for now.
  360. rules: {
  361. '@overleaf/require-script-runner': 'error',
  362. },
  363. files: ['**/scripts/**/*.mjs'], // ESM only
  364. ignores: [
  365. 'modules/admin-roles/scripts/import_admin_role_assignments.mjs',
  366. 'modules/admin-roles/scripts/remove_admin_role_from_user.mjs',
  367. 'modules/admin-roles/scripts/remove_admin_roles_from_non_admins.mjs',
  368. 'modules/admin-roles/scripts/utils.mjs',
  369. 'modules/institutions/scripts/apply_policy_to_institution.mjs',
  370. 'modules/server-ce-scripts/scripts/change-compile-timeout.mjs',
  371. 'modules/server-ce-scripts/scripts/check-mongodb.mjs',
  372. 'modules/server-ce-scripts/scripts/check-redis.mjs',
  373. 'modules/server-ce-scripts/scripts/check-texlive-images.mjs',
  374. 'modules/server-ce-scripts/scripts/create-user.mjs',
  375. 'modules/server-ce-scripts/scripts/delete-user.mjs',
  376. 'modules/server-ce-scripts/scripts/export-user-projects.mjs',
  377. 'modules/server-ce-scripts/scripts/migrate-user-emails.mjs',
  378. 'modules/server-ce-scripts/scripts/rename-tag.mjs',
  379. 'modules/server-ce-scripts/scripts/transfer-all-projects-to-user.mjs',
  380. 'modules/server-ce-scripts/scripts/upgrade-user-features.mjs',
  381. 'modules/subscriptions/scripts/backfill_user_last_trial.mjs',
  382. 'scripts/add_feature_override.mjs',
  383. 'scripts/add_subscription_members_csv.mjs',
  384. 'scripts/analytics/helpers/GoogleBigQueryHelper.mjs',
  385. 'scripts/attach_dangling_comments_to_doc.mjs',
  386. 'scripts/backfill_mixpanel_user_properties.mjs',
  387. 'scripts/backfill_project_image_name.mjs',
  388. 'scripts/backfill_user_properties.mjs',
  389. 'scripts/backfill_users_sso_attribute.mjs',
  390. 'scripts/bench_bcrypt.mjs',
  391. 'scripts/check_institution_users.mjs',
  392. 'scripts/check_overleafModuleImports.mjs',
  393. 'scripts/check_saml_emails.mjs',
  394. 'scripts/clear_feedback_collection.mjs',
  395. 'scripts/clear_sessions_set_must_reconfirm.mjs',
  396. 'scripts/count_files_in_projects.mjs',
  397. 'scripts/count_project_size.mjs',
  398. 'scripts/create_oauth_personal_access_token.mjs',
  399. 'scripts/create_project.mjs',
  400. 'scripts/deactivate_projects.mjs',
  401. 'scripts/delete-duplicate-splittest-versions/delete_test_dupes.mjs',
  402. 'scripts/delete-orphaned-docs/delete-orphaned-docs.mjs',
  403. 'scripts/delete_dangling_comments.mjs',
  404. 'scripts/delete_orphaned_chat_threads.mjs',
  405. 'scripts/delete_orphaned_data_helper.mjs',
  406. 'scripts/delete_subscriptions.mjs',
  407. 'scripts/devcontainer_setup.mjs',
  408. 'scripts/e2e_test_setup.mjs',
  409. 'scripts/ensure_affiliations.mjs',
  410. 'scripts/esm-check-migration.mjs',
  411. 'scripts/example/script_for_migration.mjs',
  412. 'scripts/fix_collaborator_refs_null.mjs',
  413. 'scripts/fix_comment_id.mjs',
  414. 'scripts/helpers/chunkArray.mjs',
  415. 'scripts/helpers/env_variable_helper.mjs',
  416. 'scripts/inst_table.mjs',
  417. 'scripts/invalidate_tokens.mjs',
  418. 'scripts/ip_matcher_ranges.mjs',
  419. 'scripts/learn/checkSanitize/checkSanitizeOptions.mjs',
  420. 'scripts/learn/checkSanitize/scrape.mjs',
  421. 'scripts/lezer-latex/benchmark.mjs',
  422. 'scripts/lezer-latex/print-tree.mjs',
  423. 'scripts/lezer-latex/random.mjs',
  424. 'scripts/lezer-latex/run.mjs',
  425. 'scripts/lezer-latex/test-incremental-parser.mjs',
  426. 'scripts/mark_migration.mjs',
  427. 'scripts/marketing-exports/error-assistant-export.mjs',
  428. 'scripts/marketing-exports/export.mjs',
  429. 'scripts/marketing-exports/linked-papers-users.mjs',
  430. 'scripts/marketing-exports/papers-export.mjs',
  431. 'scripts/marketing-exports/writefull-export.mjs',
  432. 'scripts/oauth/upgrade_token_scopes.mjs',
  433. 'scripts/plan-prices/plans.mjs',
  434. 'scripts/process_lapsed_reconfirmations.mjs',
  435. 'scripts/purge_non_logged_in_sessions.mjs',
  436. 'scripts/recurly/generate_recurly_prices.mjs',
  437. 'scripts/recurly/get_paypal_accounts_csv.mjs',
  438. 'scripts/recurly/recurly_prices.mjs',
  439. 'scripts/recurly/resync_recurly_state_single_subscription.mjs',
  440. 'scripts/recurly/resync_subscriptions.mjs',
  441. 'scripts/recurly/set_manually_collected_subscriptions.mjs',
  442. 'scripts/refresh_features.mjs',
  443. 'scripts/regenerate_duplicate_referral_ids.mjs',
  444. 'scripts/remove_deleted_users_from_token_access_refs.mjs',
  445. 'scripts/remove_email.mjs',
  446. 'scripts/remove_user_enrollment.mjs',
  447. 'scripts/sso_id_migration_check.mjs',
  448. 'scripts/stress_test.mjs',
  449. 'scripts/suspend_users.mjs',
  450. 'scripts/sync-user-entitlements/sync-user-entitlements.mjs',
  451. 'scripts/update_project_image_name.mjs',
  452. 'scripts/user-export/analytics.mjs',
  453. 'scripts/user-export/fs.mjs',
  454. 'scripts/user-export/http.mjs',
  455. 'scripts/user-export/observer.mjs',
  456. 'scripts/user-export/options.mjs',
  457. 'scripts/user-export/project.mjs',
  458. 'scripts/user-export/scrubber.mjs',
  459. 'scripts/user-export/stream.mjs',
  460. 'scripts/user-export/user.mjs',
  461. 'scripts/validate-data-of-model.mjs',
  462. ],
  463. },
  464. {
  465. // Cypress specific rules
  466. files: [
  467. 'cypress/**/*.{js,jsx,ts,tsx}',
  468. '**/test/frontend/**/*.spec.{js,jsx,ts,tsx}',
  469. ],
  470. plugins: {
  471. cypress,
  472. },
  473. rules: {
  474. ...cypress.configs.recommended.rules,
  475. },
  476. },
  477. {
  478. // Frontend test specific rules
  479. files: ['**/frontend/**/*.test.{js,jsx,ts,tsx}'],
  480. plugins: {
  481. 'testing-library': testingLibrary,
  482. },
  483. rules: {
  484. ...testingLibrary.configs['flat/react'].rules,
  485. 'testing-library/no-await-sync-events': 'off',
  486. 'testing-library/no-await-sync-queries': 'off',
  487. 'testing-library/no-container': 'off',
  488. 'testing-library/no-node-access': 'off',
  489. 'testing-library/no-render-in-lifecycle': 'off',
  490. 'testing-library/no-wait-for-multiple-assertions': 'off',
  491. 'testing-library/no-wait-for-side-effects': 'off',
  492. 'testing-library/prefer-query-by-disappearance': 'off',
  493. 'testing-library/prefer-screen-queries': 'off',
  494. 'testing-library/render-result-naming-convention': 'off',
  495. },
  496. },
  497. {
  498. // Frontend specific rules
  499. files: [
  500. '**/frontend/js/**/*.{js,jsx,ts,tsx}',
  501. '**/frontend/stories/**/*.{js,jsx,ts,tsx}',
  502. '**/*.stories.{js,jsx,ts,tsx}',
  503. '**/test/frontend/**/*.{js,jsx,ts,tsx}',
  504. '**/test/frontend/components/**/*.spec.{js,jsx,ts,tsx}',
  505. ],
  506. languageOptions: {
  507. globals: {
  508. ...globals.browser,
  509. __webpack_public_path__: true,
  510. $: true,
  511. ga: true,
  512. },
  513. sourceType: 'module',
  514. parserOptions: {},
  515. },
  516. plugins: {
  517. react,
  518. 'react-hooks': reactHooks,
  519. 'jsx-a11y': jsxA11Y,
  520. },
  521. rules: {
  522. ...reactFlatRecommended.rules,
  523. ...reactHooks.configs['recommended-latest'].rules,
  524. ...jsxA11Y.flatConfigs.recommended.rules,
  525. 'react-hooks/exhaustive-deps': [
  526. 'warn',
  527. {
  528. additionalHooks: '(useCommandProvider)',
  529. },
  530. ],
  531. // TODO: remove once https://github.com/standard/eslint-config-standard-react/issues/68 (support eslint@8) is fixed.
  532. // START: inline standard-react rules
  533. // "react/jsx-no-bind": ["error", {
  534. // "allowArrowFunctions": true,
  535. // "allowBind": false,
  536. // "ignoreRefs": true
  537. // },],
  538. 'react/no-did-update-set-state': 'error',
  539. 'react/no-unused-prop-types': 'error',
  540. 'react/prop-types': 'error',
  541. '@overleaf/no-generated-editor-themes': 'error',
  542. // "react/react-in-jsx-scope": "error",
  543. // END: inline standard-react rules
  544. // eslint-plugin-react 7.37 enabled `react/no-unescaped-entities`
  545. // in plugin:react/recommended. v8 (with 7.32) didn't. Disable to
  546. // match v8 -- the source uses literal apostrophes and quotes in
  547. // JSX freely.
  548. 'react/no-unescaped-entities': 'off',
  549. 'react/no-unknown-property': [
  550. 'error',
  551. {
  552. ignore: ['dnd-container', 'dropdown-toggle'],
  553. },
  554. ],
  555. 'react/jsx-no-target-blank': [
  556. 'error',
  557. {
  558. allowReferrer: true,
  559. },
  560. ],
  561. // Prevent usage of legacy string refs
  562. 'react/no-string-refs': 'error',
  563. // Prevent curly braces around strings (as they're unnecessary)
  564. 'react/jsx-curly-brace-presence': [
  565. 'error',
  566. {
  567. props: 'never',
  568. children: 'never',
  569. },
  570. ],
  571. // Don't import React for JSX; the JSX runtime is added by a Babel plugin
  572. 'react/react-in-jsx-scope': 'off',
  573. 'react/jsx-uses-react': 'off',
  574. // Allow functions as JSX props
  575. 'react/jsx-no-bind': 'off', // TODO: fix occurrences and re-enable this
  576. // Fix conflict between prettier & standard by overriding to prefer
  577. // double quotes
  578. 'jsx-quotes': ['error', 'prefer-double'],
  579. // Override weird behaviour of jsx-a11y label-has-for (says labels must be
  580. // nested *and* have for/id attributes)
  581. 'jsx-a11y/label-has-for': [
  582. 'error',
  583. {
  584. required: {
  585. some: ['nesting', 'id'],
  586. },
  587. },
  588. ],
  589. // Require .jsx or .tsx file extension when using JSX
  590. 'react/jsx-filename-extension': [
  591. 'error',
  592. {
  593. extensions: ['.jsx', '.tsx'],
  594. },
  595. ],
  596. 'no-restricted-syntax': [
  597. 'error',
  598. // prohibit direct calls to methods of window.localStorage
  599. {
  600. selector:
  601. "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
  602. message:
  603. 'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
  604. },
  605. ],
  606. 'no-unused-vars': 'off',
  607. '@typescript-eslint/no-unused-vars': [
  608. 'error',
  609. {
  610. args: 'after-used',
  611. argsIgnorePattern: '^_',
  612. ignoreRestSiblings: false,
  613. caughtErrors: 'none',
  614. vars: 'all',
  615. varsIgnorePattern: '^_',
  616. },
  617. ],
  618. },
  619. },
  620. {
  621. // Sorting for Meta
  622. files: ['frontend/js/utils/meta.ts'],
  623. rules: {
  624. '@typescript-eslint/member-ordering': [
  625. 'error',
  626. {
  627. interfaces: {
  628. order: 'alphabetically',
  629. },
  630. },
  631. ],
  632. },
  633. },
  634. {
  635. // React component specific rules
  636. //
  637. files: [
  638. '**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}',
  639. '**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}',
  640. ],
  641. rules: {
  642. '@overleaf/no-unnecessary-trans': 'error',
  643. '@overleaf/should-unescape-trans': 'error',
  644. '@overleaf/require-loading-label': 'error',
  645. // https://astexplorer.net/
  646. 'no-restricted-syntax': [
  647. 'error',
  648. // prohibit direct calls to methods of window.location
  649. {
  650. selector:
  651. "CallExpression[callee.object.object.name='window'][callee.object.property.name='location']",
  652. message:
  653. 'Modify location via useLocation instead of calling window.location methods directly',
  654. },
  655. // prohibit assignment to window.location
  656. {
  657. selector:
  658. "AssignmentExpression[left.object.name='window'][left.property.name='location']",
  659. message:
  660. 'Modify location via useLocation instead of calling window.location methods directly',
  661. },
  662. // prohibit assignment to window.location.href
  663. {
  664. selector:
  665. "AssignmentExpression[left.object.object.name='window'][left.object.property.name='location'][left.property.name='href']",
  666. message:
  667. 'Modify location via useLocation instead of calling window.location methods directly',
  668. },
  669. // prohibit using lookbehinds due to incidents with Safari simply crashing when the script is parsed
  670. {
  671. selector: 'Literal[regex.pattern=/\\(\\?<[!=]/]',
  672. message: 'Lookbehind is not supported in older Safari versions.',
  673. },
  674. // prohibit direct calls to methods of window.localStorage
  675. // NOTE: this rule is also defined for all frontend files, but those rules are overriden by the React component-specific config
  676. {
  677. selector:
  678. "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
  679. message:
  680. 'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
  681. },
  682. ],
  683. },
  684. },
  685. // React + TypeScript-specific rules
  686. {
  687. files: ['**/*.tsx'],
  688. rules: {
  689. 'react/prop-types': 'off',
  690. 'no-undef': 'off',
  691. },
  692. },
  693. // TypeScript-specific rules
  694. {
  695. files: ['**/*.ts'],
  696. rules: {
  697. 'no-undef': 'off',
  698. },
  699. },
  700. // JavaScript-specific rules
  701. {
  702. files: ['**/*.js'],
  703. rules: {
  704. '@typescript-eslint/no-require-imports': 'off',
  705. },
  706. },
  707. {
  708. files: ['scripts/ukamf/*.js'],
  709. rules: {
  710. // Do not allow importing of any dependencies unless specified in either
  711. // - web/package.json
  712. // - web/scripts/ukamf/package.json
  713. 'import/no-extraneous-dependencies': [
  714. 'error',
  715. {
  716. packageDir: ['.', 'scripts/ukamf'],
  717. },
  718. ],
  719. },
  720. },
  721. {
  722. files: ['scripts/learn/checkSanitize/*.js'],
  723. rules: {
  724. // The checkSanitize script is used in the dev-env only.
  725. 'import/no-extraneous-dependencies': [
  726. 'error',
  727. {
  728. devDependencies: true,
  729. packageDir: ['.', '../../'],
  730. },
  731. ],
  732. },
  733. },
  734. {
  735. files: [
  736. // Backend: Use @overleaf/logger
  737. // Docs: https://manual.dev-overleaf.com/development/code/logging/#structured-logging
  738. '**/app/**/*.{js,cjs,mjs}',
  739. 'app.{js,mjs}',
  740. 'modules/*/*.{js,mjs}',
  741. // Frontend: Prefer debugConsole over bare console
  742. // Docs: https://manual.dev-overleaf.com/development/code/logging/#frontend
  743. '**/frontend/**/*.{js,jsx,ts,tsx}',
  744. // Tests
  745. '**/test/**/*.{js,cjs,mjs,jsx,ts,tsx}',
  746. ],
  747. ignores: [
  748. // Allow console logs in scripts
  749. '**/scripts/**/*.js',
  750. // Allow console logs in stories
  751. '**/stories/**/*.{js,jsx,ts,tsx}',
  752. // Workers do not have access to the search params for enabling ?debug=true.
  753. // self.location.url is the URL of the worker script.
  754. '**/*.worker.{js,ts}',
  755. ],
  756. rules: {
  757. 'no-console': 'error',
  758. },
  759. },
  760. {
  761. files: ['**/*.worker.{js,ts}'],
  762. rules: {
  763. 'no-restricted-globals': [
  764. 'error',
  765. ..._.difference(
  766. Object.keys({
  767. ...globals.browser,
  768. ...globals.node,
  769. }),
  770. Object.keys(globals.worker)
  771. ),
  772. ],
  773. },
  774. },
  775. {
  776. // The writefull module ships from upstream as a vendored
  777. // integration; its style/test conventions differ from the rest
  778. // of services/web. Under v8 these files lint-passed in CI even
  779. // though the surface area would normally trip several rules --
  780. // suggesting either historical exemption or pre-existing CI
  781. // tolerance. To match the user-reported v8 zero-error baseline
  782. // without touching the imported source, disable the rules that
  783. // fire here.
  784. files: ['modules/writefull/**/*.{js,jsx,ts,tsx,mjs,cjs}'],
  785. rules: {
  786. 'no-console': 'off',
  787. '@typescript-eslint/no-unused-vars': 'off',
  788. 'no-unused-vars': 'off',
  789. 'react/jsx-curly-brace-presence': 'off',
  790. 'react/jsx-no-target-blank': 'off',
  791. 'react/no-unused-prop-types': 'off',
  792. 'react/no-deprecated': 'off',
  793. 'react-hooks/rules-of-hooks': 'off',
  794. '@overleaf/no-generated-editor-themes': 'off',
  795. // jsx-a11y/* fires on many writefull components. Disable the
  796. // ones that surface here to match the v8 zero-error baseline.
  797. 'jsx-a11y/no-static-element-interactions': 'off',
  798. 'jsx-a11y/click-events-have-key-events': 'off',
  799. 'jsx-a11y/no-autofocus': 'off',
  800. 'jsx-a11y/label-has-for': 'off',
  801. 'jsx-a11y/role-supports-aria-props': 'off',
  802. 'jsx-a11y/anchor-is-valid': 'off',
  803. 'jsx-a11y/interactive-supports-focus': 'off',
  804. },
  805. },
  806. {
  807. // ESLint v9's prefer-const analysis on TypeScript destructuring
  808. // (`let { foo, bar } = ...`) fires where v8 did not, even with
  809. // identical `destructuring: 'all'` options. Disable to match v8
  810. // behaviour on the existing TS source -- the actual let-vs-const
  811. // intent is preserved by the source code.
  812. files: ['**/*.{ts,tsx}'],
  813. rules: {
  814. 'prefer-const': 'off',
  815. },
  816. },
  817. {
  818. // eslint-plugin-testing-library bumped from 7.1 to 7.5 in the
  819. // migration; the newer release enabled / tightened several rules
  820. // that fire on pre-existing test code. Disable to preserve v8
  821. // behaviour; revisit in a follow-up cleanup PR.
  822. files: ['**/frontend/**/*.test.{js,jsx,ts,tsx}'],
  823. rules: {
  824. 'testing-library/no-debugging-utils': 'off',
  825. 'testing-library/prefer-presence-queries': 'off',
  826. 'testing-library/no-manual-cleanup': 'off',
  827. },
  828. },
  829. {
  830. // Frontend test files import chai's `use()` and call it at module
  831. // top level; eslint-plugin-react-hooks v5 heuristically treats any
  832. // `use*` name as a React Hook and flags this as rules-of-hooks.
  833. // Pre-existing v8 behaviour (react-hooks v4) was more conservative;
  834. // disable for test files to match.
  835. // react/no-deprecated also fires on test files using
  836. // ReactDOM.unmountComponentAtNode and similar React-18-deprecated
  837. // APIs; v8 effectively tolerated these (the user-reported zero
  838. // baseline). Disable for test files.
  839. files: [
  840. '**/test/frontend/**/*.{js,jsx,ts,tsx}',
  841. '**/frontend/**/*.test.{js,jsx,ts,tsx}',
  842. '**/frontend/**/*.spec.{js,jsx,ts,tsx}',
  843. ],
  844. rules: {
  845. 'react-hooks/rules-of-hooks': 'off',
  846. 'react/no-deprecated': 'off',
  847. },
  848. },
  849. {
  850. // eslint.config.mjs itself imports eslint plugins as devDependencies;
  851. // allow that since this file is part of the tooling, not the app.
  852. files: ['eslint.config.mjs'],
  853. rules: {
  854. 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
  855. },
  856. },
  857. // eslint-config-prettier disables stylistic rules that conflict with
  858. // prettier formatting. Placed last so it overrides rules pulled in by
  859. // preceding configs (eslint:recommended, @typescript-eslint, react,
  860. // jsx-a11y, ...).
  861. prettier,
  862. globalIgnores([
  863. '**/data/',
  864. 'scripts/translations/.cache/',
  865. '**/node_modules',
  866. 'frontend/js/vendor',
  867. 'modules/**/frontend/js/vendor',
  868. 'public/',
  869. 'frontend/js/features/source-editor/lezer-latex/latex.mjs',
  870. 'frontend/js/features/source-editor/lezer-latex/latex.terms.mjs',
  871. 'frontend/js/features/source-editor/lezer-bibtex/bibtex.mjs',
  872. 'frontend/js/features/source-editor/lezer-bibtex/bibtex.terms.mjs',
  873. 'frontend/js/features/source-editor/hunspell/wasm/hunspell.mjs',
  874. ]),
  875. ])