Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/code-infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

Scripts and configs to be used across MUI repos.

## ESLint Plugin

This package includes a custom ESLint plugin (`material-ui`) with rules specific to MUI projects.

### Available Rules

- `material-ui/no-restricted-imports` - Restricts imports based on source string patterns using glob matching. Useful for preventing imports from internal modules or enforcing import conventions.

```js
// In your eslint.config.mjs:
{
rules: {
'material-ui/no-restricted-imports': [
'error',
[
{
pattern: '@mui/material/*',
message: 'Use the default import from @mui/material instead.'
},
{
pattern: '@mui/*/internal/**',
message: 'Do not import from internal modules.'
}
]
]
}
}
```

- `material-ui/no-restricted-resolved-imports` - Similar to `no-restricted-imports` but matches against resolved file paths instead of import source strings.
- Other rules - See `src/eslint/material-ui/rules/` for the full list of available rules.

## Publishing packages

1. Go to the publish action -
Expand Down
4 changes: 3 additions & 1 deletion packages/code-infra/src/eslint/material-ui/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import disallowReactApiInServerComponents from './rules/disallow-react-api-in-se
import docgenIgnoreBeforeComment from './rules/docgen-ignore-before-comment.mjs';
import muiNameMatchesComponentName from './rules/mui-name-matches-component-name.mjs';
import noEmptyBox from './rules/no-empty-box.mjs';
import noRestrictedImports from './rules/no-restricted-imports.mjs';
import noRestrictedResolvedImports from './rules/no-restricted-resolved-imports.mjs';
import noStyledBox from './rules/no-styled-box.mjs';
import rulesOfUseThemeVariants from './rules/rules-of-use-theme-variants.mjs';
Expand All @@ -19,9 +20,10 @@ export default /** @type {import('eslint').ESLint.Plugin} */ ({
'mui-name-matches-component-name': muiNameMatchesComponentName,
'rules-of-use-theme-variants': rulesOfUseThemeVariants,
'no-empty-box': noEmptyBox,
'no-restricted-imports': noRestrictedImports,
'no-restricted-resolved-imports': noRestrictedResolvedImports,
'no-styled-box': noStyledBox,
'straight-quotes': straightQuotes,
'disallow-react-api-in-server-components': disallowReactApiInServerComponents,
'no-restricted-resolved-imports': noRestrictedResolvedImports,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import moduleVisitorModule from 'eslint-module-utils/moduleVisitor';
import { minimatch } from 'minimatch';

/**
* @type {import('eslint-module-utils/moduleVisitor').default}
*/
const moduleVisitor = /** @type {any} */ (moduleVisitorModule).default || moduleVisitorModule;

/**
* @typedef {Object} PatternConfig
* @property {string} pattern - The glob pattern to match against import sources
* @property {string} [message] - Custom message to show when the pattern matches
*/

/**
* Creates an ESLint rule that restricts imports based on their source strings using glob patterns.
* This is similar to ESLint's built-in no-restricted-imports but with more robust pattern matching.
* Works with both ESM (import) and CommonJS (require) imports.
*
* @example
* // In your eslint.config.mjs:
* {
* rules: {
* 'material-ui/no-restricted-imports': [
* 'error',
* [
* {
* pattern: '@mui/material/*',
* message: 'Use the default import from @mui/material instead.'
* },
* {
* pattern: '@mui/*\/internal/**',
* message: 'Do not import from internal modules.'
* },
* {
* pattern: './**\/*.css'
* }
* ]
* ]
* }
* }
*/
export default /** @type {import('eslint').Rule.RuleModule} */ ({
meta: {
docs: {
description:
'Disallow imports that match specified patterns. Use glob patterns to restrict imports by their source string.',
},
messages: {
restrictedImport:
'Importing from "{{importSource}}" is not allowed because it matches the pattern "{{pattern}}".{{customMessage}}',
},
type: 'suggestion',
schema: [
{
type: 'array',
items: {
type: 'object',
properties: {
pattern: { type: 'string' },
message: { type: 'string' },
},
required: ['pattern'],
additionalProperties: false,
},
},
],
},
create(context) {
const options = context.options[0] || [];

if (!Array.isArray(options) || options.length === 0) {
return {};
}

return moduleVisitor(
(/** @type {any} */ source, /** @type {any} */ node) => {
const importSource = source.value;

if (!importSource || typeof importSource !== 'string') {
return;
}

// Check each pattern against the import source
for (const option of options) {
const { pattern, message = '' } = option;

if (minimatch(importSource, pattern)) {
context.report({
node,
messageId: 'restrictedImport',
data: {
importSource,
pattern,
customMessage: message ? ` ${message}` : '',
},
});

// Stop after first match
break;
}
}
},

{ commonjs: true, esmodule: true },
); // This handles both require() and import statements
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import eslint from 'eslint';
import parser from '@typescript-eslint/parser';
import rule from './no-restricted-imports.mjs';

const ruleTester = new eslint.RuleTester({
languageOptions: {
parser,
parserOptions: {
ecmaVersion: 2015,
sourceType: 'module',
},
},
});

ruleTester.run('no-restricted-imports', rule, {
valid: [
// No configuration - should allow everything
{
code: "import foo from 'foo';",
options: [[]],
},
// Pattern doesn't match
{
code: "import foo from 'foo';",
options: [[{ pattern: 'bar' }]],
},
{
code: "import foo from '@mui/material';",
options: [[{ pattern: '@mui/material/*' }]],
},
{
code: "import { Box } from '@mui/material';",
options: [[{ pattern: '@mui/material/Box' }]],
},
// Glob patterns - no match
{
code: "import foo from '@mui/material';",
options: [[{ pattern: '@mui/*/internal/**' }]],
},
{
code: "import foo from 'react';",
options: [[{ pattern: '**/*.css' }]],
},
{
code: "const foo = require('foo');",
options: [[{ pattern: 'bar' }]],
},
],
invalid: [
// Simple pattern match
{
code: "import foo from 'foo';",
options: [[{ pattern: 'foo' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: 'foo',
pattern: 'foo',
customMessage: '',
},
},
],
},
// Glob pattern with wildcard
{
code: "import Box from '@mui/material/Box';",
options: [[{ pattern: '@mui/material/*' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: '@mui/material/Box',
pattern: '@mui/material/*',
customMessage: '',
},
},
],
},
// Deep glob pattern
{
code: "import foo from '@mui/material/internal/utils';",
options: [[{ pattern: '@mui/*/internal/**' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: '@mui/material/internal/utils',
pattern: '@mui/*/internal/**',
customMessage: '',
},
},
],
},
// Custom message
{
code: "import foo from 'foo';",
options: [[{ pattern: 'foo', message: 'Use bar instead.' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: 'foo',
pattern: 'foo',
customMessage: ' Use bar instead.',
},
},
],
},
// Multiple patterns - first match wins
{
code: "import foo from '@mui/material/Box';",
options: [
[
{ pattern: '@mui/material/*', message: 'First message.' },
{ pattern: '@mui/material/Box', message: 'Second message.' },
],
],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: '@mui/material/Box',
pattern: '@mui/material/*',
customMessage: ' First message.',
},
},
],
},
// CommonJS require
{
code: "const foo = require('foo');",
options: [[{ pattern: 'foo' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: 'foo',
pattern: 'foo',
customMessage: '',
},
},
],
},
// Named imports
{
code: "import { Box } from '@mui/material/Box';",
options: [[{ pattern: '@mui/material/*' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: '@mui/material/Box',
pattern: '@mui/material/*',
customMessage: '',
},
},
],
},
// Namespace imports
{
code: "import * as Material from '@mui/material/Box';",
options: [[{ pattern: '@mui/material/*' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: '@mui/material/Box',
pattern: '@mui/material/*',
customMessage: '',
},
},
],
},
// File extensions
{
code: "import styles from './styles.css';",
options: [[{ pattern: './**/*.css' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: './styles.css',
pattern: './**/*.css',
customMessage: '',
},
},
],
},
// Multiple restricted patterns
{
code: "import foo from 'foo/bar/baz';",
options: [[{ pattern: 'foo/**', message: 'Do not import from foo internals.' }]],
errors: [
{
messageId: 'restrictedImport',
data: {
importSource: 'foo/bar/baz',
pattern: 'foo/**',
customMessage: ' Do not import from foo internals.',
},
},
],
},
],
});
Loading