FairGarden Docs

Lint JavaScript Demo Focus

An ESLint rule that automatically inserts @focus comments around the preview section of demo files. These comments control which parts of the code are focused when rendered in documentation.

Overview

Demo files in docs/app/**/demos/** need @focus comments so FairGarden Docs knows which code to emphasize. Writing these by hand is tedious and error-prone. This ESLint rule detects the preview section of each demo and auto-fixes the comments in.

Key Features

  • Auto-fixable: Run eslint --fix to insert all comments automatically
  • Smart comment style: Uses JSX comments ({/* */}) inside wrapper elements and JS comments (//) elsewhere
  • Single vs multi-line: Uses @focus for single-line previews and @focus-start / @focus-end for multi-line
  • Named export detection: Supports both export default and named exports (filename match or single-export fallback)
  • Function body highlighting: Wraps the entire body when hooks or setup statements are present
  • Skip when present: Files that already contain @focus or @highlight anywhere are left untouched (a @highlight implicitly declares a focus region). @highlight-text does not count, since it only marks inline text within a line.

Installation & Usage

Import the rule and wire it into your ESLint flat config:

// eslint.config.mjs
import { lintJavascriptDemoFocus } from '@fairgarden/docs/pipeline/lintJavascriptDemoFocus';

export default [
  {
    files: ['docs/app/**/demos/**/*.tsx', 'docs/app/**/demos/**/*.jsx'],
    plugins: {
      'fairgarden-docs': { rules: { 'require-demo-focus': lintJavascriptDemoFocus } },
    },
    rules: {
      'fairgarden-docs/require-demo-focus': 'error',
    },
  },
];

With wrapReturn

Enable the wrapReturn option to wrap bare return <X /> statements in parentheses so the highlight comment can be placed inside:

rules: {
  'fairgarden-docs/require-demo-focus': ['error', { wrapReturn: true }],
},

How It Works

The rule identifies the main component export of a demo file and inserts comments based on the structure of its return value.

1. Wrapper elements

When the return contains a wrapper (div, Box, Stack, or React.Fragment), JSX comments are inserted inside the wrapper around its children:

// Before
export default function Demo() {
  return (
    <div>
      <Button>Click me</Button>
      <TextField label="Name" />
    </div>
  );
}

// After fix
export default function Demo() {
  return (
    <div>
      {/* @focus-start */}
      <Button>Click me</Button>
      <TextField label="Name" />
      {/* @focus-end */}
    </div>
  );
}

For a single child inside a wrapper:

// After fix
export default function Demo() {
  return (
    <div>
      {/* @focus */}
      <Button>Click me</Button>
    </div>
  );
}

2. Non-wrapper returns

When the return is a bare element (not wrapped in div, Box, Stack, or a fragment), JS comments are inserted before the return line:

// Before
export default function Demo() {
  return <Button>Click me</Button>;
}

// After fix
export default function Demo() {
  // @focus
  return <Button>Click me</Button>;
}

When the return already has parentheses, the comment is placed inside them:

// Before
export default function Demo() {
  return <Button>Click me</Button>;
}

// After fix
export default function Demo() {
  return (
    // @focus
    <Button>Click me</Button>
  );
}

With wrapReturn: true, bare returns without parentheses are wrapped so the comment appears inside:

// Before
export default function Demo() {
  return <Button>Click me</Button>;
}

// After fix (wrapReturn: true)
export default function Demo() {
  return (
    // @focus
    <Button>Click me</Button>
  );
}

3. Function body highlighting

When the component has setup code (hooks, variables, etc.) before the return, the entire function body is wrapped:

// Before
export default function Demo() {
  const code = useCode();
  return <CodeEditor code={code} />;
}

// After fix
export default function Demo() {
  // @focus-start @padding 1
  const code = useCode();
  return <CodeEditor code={code} />;
  // @focus-end
}

Named Export Detection

Not all demos use export default. The rule handles named exports with two strategies:

  1. Filename match (preferred): If a named export matches the filename (e.g., export function CodeEditor in CodeEditor.tsx), that export is used.
  2. Single export fallback: If there is exactly one named component export and no filename match, that export is used.

All common export forms are recognized: export function, export const with arrow or function expressions, and call-wrapped patterns like React.forwardRef or React.memo.

// CodeEditor.tsx — filename match
export function CodeEditor() {
  return <Editor />;
}

// Also exports helpers — only CodeEditor is processed
export function getEditorConfig() {
  return {};
}

Call-wrapped components work the same way:

// DialogTrigger.tsx — React.forwardRef with filename match
export const DialogTrigger = React.forwardRef(function DialogTrigger(props, ref) {
  return <button ref={ref} {...props} />;
});

Types

lintJavascriptDemoFocus

ESLint rule requiring demo files to have focus comments around the preview section.

type lintJavascriptDemoFocus = {
  meta: {
    type: 'suggestion';
    docs: {
      description: 'Require demo files to have @focus-start / @focus-end comments around the preview section.';
    };
    fixable: 'code';
    messages: {
      missingDemoFocusJsx: 'Demo file is missing {/* @focus-start */} and {/* @focus-end */} comments around the preview section. Run with --fix to add them automatically.';
      missingDemoFocusJsxSingle: 'Demo file is missing {/* @focus */} comment on the preview line. Run with --fix to add it automatically.';
      missingDemoFocusJs: 'Demo file is missing // @focus-start and // @focus-end comments around the preview section. Run with --fix to add them automatically.';
      missingDemoFocusJsSingle: 'Demo file is missing // @focus comment on the preview line. Run with --fix to add it automatically.';
      missingDemoFocusBody: 'Demo file is missing // @focus-start @padding 1 and // @focus-end comments around the function body. Run with --fix to add them automatically.';
    };
    schema: [
      {
        type: 'object';
        properties: {
          wrapReturn: {
            type: 'boolean';
            description: 'When true, bare return statements without parentheses are wrapped in return (...) and the highlight comment is placed inside the parentheses.';
          };
        };
        additionalProperties: false;
      },
    ];
  };
  create: ((context: RuleContext) => { ExportDefaultDeclaration?: undefined; ExportNamedDeclaration?: undefined; Program:exit?: undefined } | { ExportDefaultDeclaration: unknown; ExportNamedDeclaration: unknown; Program:exit: unknown });
}