@fluidframework/eslint-config-fluid
Version:
Shareable ESLint config for the Fluid Framework
309 lines (293 loc) • 11.4 kB
text/typescript
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/**
* File-specific config overrides and shared config objects.
*
* This module contains ESLint configurations that apply to specific file patterns or
* provide specialized behavior:
*
* - dependConfig: eslint-plugin-depend configuration for dependency checking
* - useProjectService: TypeScript project service for automatic tsconfig discovery
* - testProjectConfig: Test file configuration with explicit project paths and relaxed rules
* - internalModulesConfig: import-x/no-internal-modules rule for production code
* - reactConfig: React and React Hooks plugin configurations
* - cjsFileConfig: CommonJS file rule overrides
* - jsNoProject: Disables type-aware parsing for JS and .d.ts files
* - jsTypeAwareDisable: Disables type-aware rules for JS files
* - reactRecommendedOverride: React file overrides for recommended config factory
* - testRecommendedOverride: Test file overrides for recommended config
* - sharedConfigs: Collection of all shared configs in a config array
*/
import eslintReact from "@eslint-react/eslint-plugin";
import dependPlugin from "eslint-plugin-depend";
import noOnlyTestsPlugin from "eslint-plugin-no-only-tests";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import type { ESLint, Linter } from "eslint";
import { permittedImports, restrictedImportPaths, testFilePatterns } from "../constants.mjs";
import type { FlatConfigArray } from "./base.mjs";
const reactFilePatterns = ["**/*.jsx", "**/*.tsx"] as const;
const reactRecommendedTypeScript = eslintReact.configs[
"recommended-typescript"
] satisfies Linter.Config;
const reactHooksRecommended = reactHooksPlugin.configs.flat.recommended satisfies Linter.Config;
/**
* eslint-plugin-depend configuration.
*/
export const dependConfig = {
plugins: {
depend: dependPlugin,
},
rules: {
"depend/ban-dependencies": [
"error",
{
allowed: ["axios", "fs-extra"],
},
],
},
} as const satisfies Linter.Config;
/**
* Use projectService for automatic tsconfig discovery instead of manual project configuration.
*/
export const useProjectService = {
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
languageOptions: {
parserOptions: {
// Explicitly clear any inherited project setting to avoid conflicts with projectService.
// Newer typescript-eslint versions error if both project and projectService are enabled.
project: null,
projectService: true,
// Set tsconfigRootDir to the current working directory so the parser can locate tsconfig
// files unambiguously. Without this, typescript-eslint tries to auto-detect the root by
// inspecting the call stack for the nearest eslint.config file. In this monorepo the
// shared config lives in a different directory than the consuming packages, and VS Code's
// TypeScript project service can accumulate multiple candidate roots as files from
// different packages are visited, leading to "multiple candidate TSConfigRootDirs" errors.
tsconfigRootDir: process.cwd(),
},
},
} as const satisfies Linter.Config;
/**
* Test file configuration with explicit project paths and rule overrides.
*/
export const testProjectConfig = {
files: ["src/test/**", ...testFilePatterns],
plugins: {
"no-only-tests": noOnlyTestsPlugin,
},
rules: {
/**
* Disallow .only() in tests (e.g. describe.only, it.only) to prevent accidentally
* committing focused tests that would skip the rest of the suite in CI.
*
* @see https://github.com/levibuzolic/eslint-plugin-no-only-tests
*/
"no-only-tests/no-only-tests": "error",
"@typescript-eslint/no-invalid-this": "off",
"@typescript-eslint/unbound-method": "off",
"import-x/no-nodejs-modules": "off",
"import-x/no-deprecated": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/no-restricted-imports": [
"error",
{
paths: restrictedImportPaths,
},
],
"import-x/no-internal-modules": [
"error",
{
// Any and all `@fluid*` import paths are allowed in test files.
// Preferably, external (alpha/beta/public) entrypoints are used
// for clarity where testing is somewhat whitebox versus validating
// customer experience.
allow: ["@fluid*/**", ...permittedImports],
},
],
"import-x/no-extraneous-dependencies": ["error", { devDependencies: true }],
},
} as const satisfies Linter.Config;
/**
* Override import-x/no-internal-modules for non-test files to include /legacy imports.
*/
export const internalModulesConfig = {
files: [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts",
"**/*.js",
"**/*.jsx",
"**/*.mjs",
"**/*.cjs",
],
ignores: ["src/test/**", ...testFilePatterns],
rules: {
"import-x/no-internal-modules": [
"error",
{
allow: permittedImports,
},
],
},
} as const satisfies Linter.Config;
/**
* React rules - extends @eslint-react recommended-typescript and react-hooks/recommended.
*
* Uses @eslint-react/eslint-plugin instead of eslint-plugin-react for ESLint 10 compatibility.
* eslint-plugin-react-hooks is kept separately because it provides React Compiler rules
* and already supports ESLint 10.
*/
export const reactConfig = [
// @eslint-react recommended-typescript preset
{
...reactRecommendedTypeScript,
files: [...reactFilePatterns],
rules: {
...reactRecommendedTypeScript.rules,
"@eslint-react/dom/no-unsafe-target-blank": "error",
"@eslint-react/no-children-prop": "error",
"@eslint-react/no-useless-fragment": "error",
"@eslint-react/jsx-no-comment-textnodes": "error",
},
},
// react-hooks/recommended rules with custom overrides
{
...reactHooksRecommended,
files: [...reactFilePatterns],
plugins: {
...reactHooksRecommended.plugins,
// reactHooksPlugin.configs.flat does not conform. It is not a `ConfigObject`.
"react-hooks": reactHooksPlugin as ESLint.Plugin,
},
rules: {
...reactHooksRecommended.rules,
"react-hooks/immutability": "warn",
"react-hooks/refs": "warn",
"react-hooks/rules-of-hooks": "warn",
"react-hooks/set-state-in-effect": "warn",
"react-hooks/static-components": "warn",
},
},
] as const satisfies FlatConfigArray;
/**
* CommonJS files can use __dirname and require.
*/
export const cjsFileConfig = {
files: ["**/*.cts", "**/*.cjs"],
rules: {
"unicorn/prefer-module": "off",
},
} as const satisfies Linter.Config;
/**
* Disable type-aware parsing for JS files and .d.ts files.
*/
export const jsNoProject = {
files: ["**/*.js", "**/*.cjs", "**/*.mjs", "**/*.d.ts"],
languageOptions: { parserOptions: { project: null, projectService: false } },
} as const satisfies Linter.Config;
/**
* Disable type-required @typescript-eslint rules for pure JS files and .d.ts files.
*/
export const jsTypeAwareDisable = {
files: ["**/*.js", "**/*.cjs", "**/*.mjs", "**/*.d.ts"],
rules: {
"@typescript-eslint/await-thenable": "off",
"@typescript-eslint/consistent-return": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-array-delete": "off",
"@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
"@typescript-eslint/no-deprecated": "off",
"@typescript-eslint/no-duplicate-type-constituents": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-for-in-array": "off",
"@typescript-eslint/no-implied-eval": "off",
"@typescript-eslint/no-meaningless-void-operator": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-mixed-enums": "off",
"@typescript-eslint/no-redundant-type-constituents": "off",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
"@typescript-eslint/no-unnecessary-condition": "off",
"@typescript-eslint/no-unnecessary-qualifier": "off",
"@typescript-eslint/no-unnecessary-template-expression": "off",
"@typescript-eslint/no-unnecessary-type-arguments": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-unnecessary-type-parameters": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-type-assertion": "off",
"@typescript-eslint/no-unsafe-unary-minus": "off",
"@typescript-eslint/non-nullable-type-assertion-style": "off",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/prefer-destructuring": "off",
"@typescript-eslint/prefer-find": "off",
"@typescript-eslint/prefer-includes": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/prefer-optional-chain": "off",
"@typescript-eslint/prefer-promise-reject-errors": "off",
"@typescript-eslint/prefer-readonly": "off",
"@typescript-eslint/prefer-readonly-parameter-types": "off",
"@typescript-eslint/prefer-reduce-type-parameter": "off",
"@typescript-eslint/prefer-regexp-exec": "off",
"@typescript-eslint/prefer-return-this-type": "off",
"@typescript-eslint/prefer-string-starts-ends-with": "off",
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/related-getter-setter-pairs": "off",
"@typescript-eslint/require-array-sort-compare": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/return-await": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/switch-exhaustiveness-check": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/use-unknown-in-catch-callback-variable": "off",
},
} as const satisfies Linter.Config;
/**
* React file overrides for recommended config factory.
*/
export const reactRecommendedOverride = {
files: [...reactFilePatterns],
rules: {
"unicorn/consistent-function-scoping": "off",
},
} as const satisfies Linter.Config;
/**
* Test file overrides for recommended config factory.
*/
export const testRecommendedOverride = {
// Use of spread operator shouldn't really be needed here. Under VS Code, a
// complaint is raised that
// The type 'readonly [...]' is 'readonly' and cannot be assigned to the mutable type '(string | string[])[]'.ts(4104)
// without spread. But that doesn't appear in other uses. Use spread to pacify
// that environment. (Remember mutability is not well checked in TS generally.
// So an extra copy if safety was needed isn't a problem.)
files: [...testFilePatterns],
rules: {
"unicorn/consistent-function-scoping": "off",
"unicorn/prefer-module": "off",
},
} as const satisfies Linter.Config;
/**
* Full set of shared configuration objects in config array.
*/
export const sharedConfigs = [
useProjectService,
testProjectConfig,
internalModulesConfig,
...reactConfig,
cjsFileConfig,
jsNoProject,
jsTypeAwareDisable,
] as const satisfies FlatConfigArray;