Factory AI ESLint Plugin

repository·main·Indexed 42 days ago

https://github.com/factory-ai/eslint-plugin

An open-source ESLint plugin providing configurations and custom rules for Factory projects. It enforces strict organizational and architectural patterns—such as specific file naming for enums, types, constants, and errors—to drive AI coding agents toward more predictable results. Includes specialized configurations for base, recommended, frontend (React/Vite), and backend applications.

Tokens
30.5K
Snippets
79
Records
109
Agent score
50%

What's inside Factory AI ESLint Plugin

  1. Understand the @factory/constants-file-organization rule

    main

    The @factory/constants-file-organization rule enforces a centralized structure for constants to improve maintainability and prevent magic values from being scattered across the codebase.

    It enforces three specific constraints:

    1. Location: All exported constants must reside in files named constants.ts.
    2. Content: Files named constants.ts are restricted to const declarations only (no functions or other logic).
    3. Imports: Imports within constants.ts are strictly limited to:
      • External modules
      • Media files
      • Internal enums.ts, types.ts, constants.ts, or schema.ts files.

    Exemptions: Constants that are initialized via function calls, new expressions, or tagged templates are exempt from the location restriction (e.g., export const logger = createLogger() is allowed in non-constant files).

  2. Follow Factory's file organization conventions

    main

    The example project demonstrates the recommended file organization for projects using Factory's conventions. Use these specific filenames to categorize your code:

    • enums.ts: All enum definitions.
    • types.ts: All type and interface definitions.
    • constants.ts: All constant values.
    • errors.ts: All custom error classes.
    • user-service.ts: Business logic (the filename should match the primary exported functions).
    • index.ts: Public API exports.
  3. Avoid creating styled-components inside functions

    main

    To comply with the @factory/no-dynamic-styled-components rule, ensure all styled definitions are located at the module scope (outside of any function or component). Instead of using local variables inside a component to handle dynamic styles, use props to pass dynamic values to a module-scoped styled component.

    // INCORRECT: Recreates Container on every render
    function Card({ highlighted }) {
      const Container = styled.div`
        background: ${highlighted ? 'yellow' : 'white'};
      `;
    
      return <Container>Content</Container>;
    }
    
    // CORRECT: Container is stable; dynamic logic is handled via props
    const Container = styled.div<{ $highlighted: boolean }>`
      background: ${(props) => (props.$highlighted ? 'yellow' : 'white')};
    `;
    
    function Card({ highlighted }) {
      return <Container $highlighted={highlighted}>Content</Container>;
    }
  4. Use the @factory/no-use-effect-in-hooks rule

    main

    The @factory/no-use-effect-in-hooks rule prevents the use of effect hooks (any hook ending in effect, such as useEffect, useLayoutEffect, or useMountEffect) inside custom hooks (functions starting with use).

    Rationale

    Custom hooks are intended to be composable building blocks rather than side-effect containers. This rule enforces better architecture by:

    • Making hooks easier to test.
    • Ensuring components maintain control over when side effects run.
    • Making data flow more explicit and easier to understand.
    • Adhering to the principle of least surprise.

    Note: This rule does not prevent using effects inside regular React components; it only targets custom hooks.

    // Incorrect: Custom hook containing an effect
    function useUserData(userId: string) {
      const [user, setUser] = useState(null);
    
      useEffect(() => {
        fetchUser(userId).then(setUser);
      }, [userId]);
    
      return user;
    }
  5. Use the @factory/no-plain-html-text-elements rule

    main

    The @factory/no-plain-html-text-elements rule prevents developers from wrapping raw text directly inside standard HTML elements. Instead, you should use typography components provided by your design system (e.g., <Text>, <Heading>, <Paragraph>).

    This ensures consistent typography, proper accessibility, and adherence to design system styling guidelines.

    Restricted elements include: div, span, p, h1-h6, section, article, strong, em, li, td, th, and others.

    // ❌ Incorrect: Plain text in HTML elements
    <div>Hello World</div>
    <span>Click here</span>
    <p>This is a paragraph</p>
    <h1>Page Title</h1>
    
    // ✅ Correct: Use design system typography components
    <Text>Hello World</Text>
    <Label>Click here</Label>
    <Paragraph>This is a paragraph</Paragraph>
    <Heading level={1}>Page Title</Heading>
  6. Use @factory/jest-mock-absolute-paths to enforce absolute paths in Jest mocks

    main

    The @factory/jest-mock-absolute-paths rule ensures that all Jest mocking functions use absolute paths (typically via path aliases) instead of relative paths. This improves test reliability, prevents breakage when files are moved, and clarifies which modules are being mocked.

    Requirements:

    • All jest.mock(), jest.doMock(), and jest.unmock() calls must use absolute paths (e.g., @/utils/api).
    • Relative paths (e.g., ./utils or ../api) are prohibited.
    • Using require.resolve() inside a mock call is also prohibited.
    // Correct: Using absolute paths with aliases or external modules
    jest.mock('@/utils/api');
    jest.mock('@factory/common/logger');
    jest.mock('axios');
    jest.mock('@testing-library/react');
    
    // Incorrect: Using relative paths or require.resolve
    jest.mock('./utils');
    jest.mock('../services/api');
    jest.mock(require.resolve('some-module'));
  7. Use @factory/no-exported-string-union-types to enforce enums

    main

    The @factory/no-exported-string-union-types rule prevents developers from exporting type aliases that consist solely of string literal unions. Instead, it mandates the use of TypeScript enums.

    This rule is designed to improve runtime capabilities (iteration and logging), refactoring support, and maintain consistency with file organization patterns (such as using enums.ts).

    What is allowed:

    • Non-exported string unions (e.g., type Internal = 'a' | 'b';).
    • Unions containing non-string types (e.g., type Result = 'success' | number;).
    • Unions of other types (e.g., type Nullable<T> = T | null;).
    // Correct: Use enums instead
    export enum Status {
      Active = 'active',
      Inactive = 'inactive',
      Pending = 'pending',
    }
    
    // Correct: Non-exported unions are allowed
    type InternalStatus = 'loading' | 'ready';
    
    // Correct: Mixed unions are allowed
    export type Result = 'success' | 'error' | number;
    
    // Correct: Unions with other types are allowed
    export type Nullable<T> = T | null;
  8. Use the @factory/test-file-location rule

    main

    The @factory/test-file-location rule enforces that test files are colocated with the source files they test. This improves developer experience by making tests easy to find and ensuring that moving a source file automatically moves its corresponding tests.

    To satisfy this rule, test files must meet three requirements:

    1. Naming convention: Files must be named with the .test.ts or .test.tsx suffix.
    2. Colocation: Test files must reside in the same directory as the source file they test. They should not be placed in separate test/ or __tests__/ directories.
    3. Content: Test files must contain at least one test block (e.g., describe, it, or test).
    // Correct: Colocated with source file and contains test blocks
    // src/utils/format.ts
    // src/utils/format.test.ts
    describe('format', () => {
      it('should format dates correctly', () => {
        expect(formatDate(new Date())).toBe('2024-01-01');
      });
    });
  9. Use the @factory/require-tsx-test-stories-files rule

    main

    The @factory/require-tsx-test-stories-files rule enforces that every React component defined in a .tsx file is accompanied by both a unit test file and a Storybook stories file in the same directory. This ensures that components are both logically verified and visually documented.

    Requirements

    For any non-exempt .tsx file, the following must exist in the same directory:

    1. A .test.tsx file (e.g., Component.test.tsx)
    2. A .stories.tsx file (e.g., Component.stories.tsx)

    Exempt Files

    The rule ignores the following files, as they typically do not represent standalone UI components requiring visual documentation:

    • Barrel exports: index.tsx
    • Next.js App Router files: layout.tsx, page.tsx, loading.tsx, error.tsx, not-found.tsx, template.tsx, default.tsx
    • Definition files: types.tsx, constants.tsx, enums.tsx
    • Configuration files: style-props.tsx, config.tsx, schema.tsx
    // Incorrect
    src/components/Button.tsx
    
    // Correct
    src/components/Button.tsx
    src/components/Button.test.tsx
    src/components/Button.stories.tsx
  10. Use the @factory/require-route-middleware rule

    main

    The @factory/require-route-middleware rule enforces that Next.js route handlers in route.ts files call a middleware function as their very first statement. This ensures essential logic like authentication, authorization, logging, and rate limiting is executed before any route-specific logic.

    Recognized Middleware Patterns

    The rule looks for calls to functions following these naming patterns:

    • handleRouteMiddleware
    • handleAdminRouteMiddleware
    • handleCronMiddleware
    • handlePublicMiddleware

    Important Exclusions

    This rule does not apply to v0 API routes (/api/v0/). For those routes, use the @factory/require-v0-route-handle-middleware rule instead.

    // Correct usage: middleware is the first statement
    export async function GET(request: Request) {
      return handleRouteMiddleware(request, async () => {
        const data = await fetchData();
        return NextResponse.json(data);
      });
    }
  11. Use the @factory/types-file-organization rule

    main

    The @factory/types-file-organization rule enforces a centralized structure for TypeScript type definitions to prevent type sprawl and improve maintainability.

    To comply with this rule, you must follow these three constraints:

    1. Location: All exported TypeScript types and interfaces must reside in files named types.ts.
    2. Content: Files named types.ts are restricted to containing only type aliases and interface declarations.
    3. Imports: Imports within a types.ts file are strictly limited to:
      • External modules
      • Internal enums.ts files
      • Internal types.ts files
      • Internal schema.ts files
    // src/types.ts
    import { Status } from './enums';
    import type { BaseSchema } from './schema';
    
    export interface User {
      id: string;
      name: string;
      status: Status;
    }
    
    export type UserId = string;
    
    export type Config = {
      schema: BaseSchema;
      enabled: boolean;
    };