diff --git a/.eslintrc.js b/.eslintrc.js index 9560ab5032ba54..24f8b126a74ca9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -408,6 +408,21 @@ module.exports = /** @type {Config} */ ({ 'react/prop-types': 'off', }, }, + { + files: ['packages/*/src/*/*.?(c|m)[jt]s?(x)'], + excludedFiles: [ + '*.spec.*', + '*.test.*', + // deprecated library + '**/mui-base/**/*', + '**/mui-joy/**/*', + // used internally, not used on app router yet + '**/mui-docs/**/*', + ], + rules: { + 'material-ui/disallow-react-api-in-server-components': 'error', + }, + }, { files: ['packages/*/src/**/*.?(c|m)[jt]s?(x)'], excludedFiles: '*.spec.*', diff --git a/packages/eslint-plugin-material-ui/src/index.js b/packages/eslint-plugin-material-ui/src/index.js index ae6e389b24cc00..dc2aec3970eacf 100644 --- a/packages/eslint-plugin-material-ui/src/index.js +++ b/packages/eslint-plugin-material-ui/src/index.js @@ -8,4 +8,5 @@ module.exports.rules = { 'no-empty-box': require('./rules/no-empty-box'), 'no-styled-box': require('./rules/no-styled-box'), 'straight-quotes': require('./rules/straight-quotes'), + 'disallow-react-api-in-server-components': require('./rules/disallow-react-api-in-server-components'), }; diff --git a/packages/eslint-plugin-material-ui/src/rules/disallow-react-api-in-server-components.js b/packages/eslint-plugin-material-ui/src/rules/disallow-react-api-in-server-components.js new file mode 100644 index 00000000000000..02d894e764c133 --- /dev/null +++ b/packages/eslint-plugin-material-ui/src/rules/disallow-react-api-in-server-components.js @@ -0,0 +1,51 @@ +module.exports = { + create(context) { + let hasUseClientDirective = false; + const apis = new Set([ + 'useState', + 'useEffect', + 'useLayoutEffect', + 'useReducer', + 'useTransition', + 'createContext', + ]); + return { + Program(node) { + hasUseClientDirective = node.body.some( + (statement) => + statement.type === 'ExpressionStatement' && + statement.expression.type === 'Literal' && + statement.expression.value === 'use client', + ); + }, + CallExpression(node) { + if ( + !hasUseClientDirective && + node.callee.type === 'MemberExpression' && + node.callee.object.name === 'React' && + apis.has(node.callee.property.name) + ) { + context.report({ + node, + message: `Using 'React.${node.callee.property.name}' is forbidden if the file doesn't have a 'use client' directive.`, + fix(fixer) { + const sourceCode = context.getSourceCode(); + if ( + sourceCode.text.includes('"use server"') || + sourceCode.text.includes("'use server'") + ) { + return null; + } + + const firstToken = sourceCode.ast.body[0]; + return fixer.insertTextBefore(firstToken, "'use client';\n"); + }, + }); + } + }, + }; + }, + meta: { + fixable: 'code', + }, +}; diff --git a/packages/mui-lab/src/Timeline/TimelineContext.ts b/packages/mui-lab/src/Timeline/TimelineContext.ts index c7e7e2796ba7cd..760bc1218083b8 100644 --- a/packages/mui-lab/src/Timeline/TimelineContext.ts +++ b/packages/mui-lab/src/Timeline/TimelineContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; /** diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts b/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts index 8a93fe171954ef..a44e8dfb01cfdc 100644 --- a/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts +++ b/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; type ButtonPositionClassName = string; diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts b/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts index d336154db99963..66e9d9b3e41b09 100644 --- a/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts +++ b/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import type { ButtonGroupProps } from './ButtonGroup'; diff --git a/packages/mui-material/src/Dialog/DialogContext.ts b/packages/mui-material/src/Dialog/DialogContext.ts index d73b6aa7028425..fffd2cffd78ad3 100644 --- a/packages/mui-material/src/Dialog/DialogContext.ts +++ b/packages/mui-material/src/Dialog/DialogContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; interface DialogContextValue { diff --git a/packages/mui-material/src/FormControl/FormControlContext.ts b/packages/mui-material/src/FormControl/FormControlContext.ts index 606057104b1457..15daa1d1b6dee9 100644 --- a/packages/mui-material/src/FormControl/FormControlContext.ts +++ b/packages/mui-material/src/FormControl/FormControlContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import type { FormControlProps } from './FormControl'; diff --git a/packages/mui-material/src/Hidden/withWidth.js b/packages/mui-material/src/Hidden/withWidth.js index d0a534c476c905..56ea1b458a8dce 100644 --- a/packages/mui-material/src/Hidden/withWidth.js +++ b/packages/mui-material/src/Hidden/withWidth.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import getDisplayName from '@mui/utils/getDisplayName'; diff --git a/packages/mui-material/src/RadioGroup/RadioGroupContext.ts b/packages/mui-material/src/RadioGroup/RadioGroupContext.ts index 0679181062d1d4..230ae5a4b5be72 100644 --- a/packages/mui-material/src/RadioGroup/RadioGroupContext.ts +++ b/packages/mui-material/src/RadioGroup/RadioGroupContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; export interface RadioGroupContextValue { diff --git a/packages/mui-material/src/Step/StepContext.ts b/packages/mui-material/src/Step/StepContext.ts index 25acdf43afc430..a48e883b22fd62 100644 --- a/packages/mui-material/src/Step/StepContext.ts +++ b/packages/mui-material/src/Step/StepContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; export interface StepContextType { diff --git a/packages/mui-material/src/Stepper/StepperContext.ts b/packages/mui-material/src/Stepper/StepperContext.ts index 41fadd10bd6b4a..1bdbb3608a9783 100644 --- a/packages/mui-material/src/Stepper/StepperContext.ts +++ b/packages/mui-material/src/Stepper/StepperContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; export interface StepperContextType { diff --git a/packages/mui-material/src/Table/Tablelvl2Context.js b/packages/mui-material/src/Table/Tablelvl2Context.js index f864806b6e5c39..6b1471c8f156ee 100644 --- a/packages/mui-material/src/Table/Tablelvl2Context.js +++ b/packages/mui-material/src/Table/Tablelvl2Context.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; /** diff --git a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts index df044c2b87ca7b..36d0f2c40abe1d 100644 --- a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts +++ b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; type ToggleButtonPositionClassName = string; diff --git a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts index 88c3153db2be8c..6140564c895f22 100644 --- a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts +++ b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import type { ToggleButtonGroupProps } from './ToggleButtonGroup'; diff --git a/packages/mui-private-theming/src/useTheme/ThemeContext.js b/packages/mui-private-theming/src/useTheme/ThemeContext.js index cbd9f0ed799a25..59f9db29f9ea6f 100644 --- a/packages/mui-private-theming/src/useTheme/ThemeContext.js +++ b/packages/mui-private-theming/src/useTheme/ThemeContext.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; const ThemeContext = React.createContext(null); diff --git a/packages/mui-styles/src/StylesProvider/StylesProvider.js b/packages/mui-styles/src/StylesProvider/StylesProvider.js index 32306d2e2b6841..98ddcfa8953f95 100644 --- a/packages/mui-styles/src/StylesProvider/StylesProvider.js +++ b/packages/mui-styles/src/StylesProvider/StylesProvider.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { exactProp } from '@mui/utils'; diff --git a/packages/mui-styles/src/makeStyles/makeStyles.js b/packages/mui-styles/src/makeStyles/makeStyles.js index d18086cb7e6f9a..ebd03f16e6ba17 100644 --- a/packages/mui-styles/src/makeStyles/makeStyles.js +++ b/packages/mui-styles/src/makeStyles/makeStyles.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import { getDynamicStyles } from 'jss'; import mergeClasses from '../mergeClasses'; diff --git a/packages/mui-system/src/RtlProvider/index.js b/packages/mui-system/src/RtlProvider/index.js index 17c01ee6d6b564..34f9bc5c9d0cba 100644 --- a/packages/mui-system/src/RtlProvider/index.js +++ b/packages/mui-system/src/RtlProvider/index.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.js b/packages/mui-system/src/cssVars/createCssVarsProvider.js index 2ed616f210866f..3946722bd4574d 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.js @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { GlobalStyles } from '@mui/styled-engine';