Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
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.
Demo files in docs/app/**/demos/** need @focus comments so the docs infrastructure 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.
eslint --fix to insert all comments automatically{/* */}) inside wrapper elements and JS comments (//) elsewhere@focus for single-line previews and @focus-start / @focus-end for multi-lineexport default and named exports (filename match or single-export fallback)@highlight or @focus anywhere are left untouchedImport the rule and wire it into your ESLint flat config:
// eslint.config.mjs
import { lintJavascriptDemoFocus } from '@mui/internal-docs-infra/pipeline/lintJavascriptDemoFocus';
export default [
{
files: ['docs/app/**/demos/**/*.tsx', 'docs/app/**/demos/**/*.jsx'],
plugins: {
'docs-infra': { rules: { 'require-demo-focus': lintJavascriptDemoFocus } },
},
rules: {
'docs-infra/require-demo-focus': 'error',
},
},
];
wrapReturnEnable the wrapReturn option to wrap bare return <X /> statements in parentheses so the highlight comment can be placed inside:
rules: {
'docs-infra/require-demo-focus': ['error', { wrapReturn: true }],
},
The rule identifies the main component export of a demo file and inserts comments based on the structure of its return value.
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>
<Button>Click me</Button>
<TextField label="Name" />
</div>
);
}
For a single child inside a wrapper:
// After fix
export default function Demo() {
return (
<div>
<Button>Click me</Button>
</div>
);
}
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() {
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 (
<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 (
<Button>Click me</Button>
);
}
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() {
const code = useCode();
return <CodeEditor code={code} />;
}
Not all demos use export default. The rule handles named exports with two strategies:
export function CodeEditor in CodeEditor.tsx), 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} />;
});
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 });
}