eslint-plugin-nextfriday 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # eslint-plugin-nextfriday
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#16](https://github.com/next-friday/eslint-plugin-nextfriday/pull/16) [`6059183`](https://github.com/next-friday/eslint-plugin-nextfriday/commit/60591838ee3bbe9c5f1a497cb571028f973311b6) Thanks [@nextfridaydeveloper](https://github.com/nextfridaydeveloper)! - A new ESLint rule `enforce-readonly-component-props` that enforces the use of `Readonly<>` wrapper for React component props when using named types or interfaces. This rule helps prevent accidental mutations of props and makes the immutable nature of React props explicit in the type system.
8
+
3
9
  ## 1.1.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -69,6 +69,7 @@ export default [
69
69
  "nextfriday/jsx-pascal-case": "error",
70
70
  "nextfriday/prefer-interface-over-inline-types": "error",
71
71
  "nextfriday/react-props-destructure": "error",
72
+ "nextfriday/enforce-readonly-component-props": "error",
72
73
  },
73
74
  },
74
75
  ];
@@ -117,6 +118,7 @@ module.exports = {
117
118
  | [prefer-interface-over-inline-types](docs/rules/PREFER_INTERFACE_OVER_INLINE_TYPES.md) | Enforce interface declarations over inline types for React props | ❌ |
118
119
  | [prefer-react-import-types](docs/rules/PREFER_REACT_IMPORT_TYPES.md) | Enforce direct imports from 'react' instead of React.X | ✅ |
119
120
  | [react-props-destructure](docs/rules/REACT_PROPS_DESTRUCTURE.md) | Enforce destructuring props inside React component body | ❌ |
121
+ | [enforce-readonly-component-props](docs/rules/ENFORCE_READONLY_COMPONENT_PROPS.md) | Enforce Readonly wrapper for React component props | ✅ |
120
122
 
121
123
  ## Configurations
122
124
 
@@ -148,6 +150,7 @@ Includes all base rules plus React-specific rules:
148
150
  - `nextfriday/jsx-pascal-case`: `"error"`
149
151
  - `nextfriday/prefer-interface-over-inline-types`: `"error"`
150
152
  - `nextfriday/react-props-destructure`: `"error"`
153
+ - `nextfriday/enforce-readonly-component-props`: `"error"`
151
154
 
152
155
  #### `react/recommended`
153
156
 
@@ -163,6 +166,7 @@ Includes all rules suitable for Next.js projects:
163
166
  - `nextfriday/jsx-pascal-case`: `"error"`
164
167
  - `nextfriday/prefer-interface-over-inline-types`: `"error"`
165
168
  - `nextfriday/react-props-destructure`: `"error"`
169
+ - `nextfriday/enforce-readonly-component-props`: `"error"`
166
170
 
167
171
  #### `nextjs/recommended`
168
172
 
@@ -0,0 +1,104 @@
1
+ # enforce-readonly-component-props
2
+
3
+ Enforce `Readonly<>` wrapper for React component props when using named types or interfaces.
4
+
5
+ ## Rule Details
6
+
7
+ This rule enforces that React component props using named types (interfaces or type aliases) must be wrapped with `Readonly<>` for immutability. This helps prevent accidental mutations of props and makes the immutable nature of React props explicit in the type system.
8
+
9
+ ## Examples
10
+
11
+ ### ❌ Incorrect
12
+
13
+ ```tsx
14
+ interface Props {
15
+ children: ReactNode;
16
+ }
17
+ const Component = (props: Props) => <div>{props.children}</div>;
18
+ ```
19
+
20
+ ```tsx
21
+ type ComponentProps = {
22
+ title: string;
23
+ onClick: () => void;
24
+ };
25
+ const Component = (props: ComponentProps) => <div>{props.title}</div>;
26
+ ```
27
+
28
+ ```tsx
29
+ interface LayoutProps {
30
+ children: ReactNode;
31
+ title?: string;
32
+ }
33
+ function Layout(props: LayoutProps) {
34
+ return <div>{props.children}</div>;
35
+ }
36
+ ```
37
+
38
+ ### ✅ Correct
39
+
40
+ ```tsx
41
+ interface Props {
42
+ children: ReactNode;
43
+ }
44
+ const Component = (props: Readonly<Props>) => <div>{props.children}</div>;
45
+ ```
46
+
47
+ ```tsx
48
+ type ComponentProps = {
49
+ title: string;
50
+ onClick: () => void;
51
+ };
52
+ const Component = (props: Readonly<ComponentProps>) => <div>{props.title}</div>;
53
+ ```
54
+
55
+ ```tsx
56
+ interface LayoutProps {
57
+ children: ReactNode;
58
+ title?: string;
59
+ }
60
+ function Layout(props: Readonly<LayoutProps>) {
61
+ return <div>{props.children}</div>;
62
+ }
63
+ ```
64
+
65
+ ```tsx
66
+ // Inline types are handled by prefer-interface-over-inline-types rule
67
+ const Component = (props: { children: ReactNode }) => <div>{props.children}</div>;
68
+ ```
69
+
70
+ ```tsx
71
+ // Non-React functions are ignored
72
+ interface HelperProps {
73
+ value: number;
74
+ }
75
+ const helper = (props: HelperProps) => {
76
+ return props.value * 2;
77
+ };
78
+ ```
79
+
80
+ ## When Not To Use
81
+
82
+ This rule should not be used if:
83
+
84
+ - You prefer inline types for component props (use `prefer-interface-over-inline-types` instead)
85
+ - You don't want to enforce immutability patterns in your React components
86
+ - You're working with a codebase that doesn't use TypeScript strict mode
87
+
88
+ ## Fixable
89
+
90
+ This rule is fixable using ESLint's `--fix` option. The fixer will automatically wrap named types with `Readonly<>`.
91
+
92
+ ## Related Rules
93
+
94
+ - [`prefer-interface-over-inline-types`](./PREFER_INTERFACE_OVER_INLINE_TYPES.md) - Enforces interface declarations over inline types
95
+ - [`react-props-destructure`](./REACT_PROPS_DESTRUCTURE.md) - Enforces destructuring props inside component body
96
+
97
+ ## Configuration
98
+
99
+ This rule is included in the following configurations:
100
+
101
+ - `react` (warn)
102
+ - `react/recommended` (error)
103
+ - `nextjs` (warn)
104
+ - `nextjs/recommended` (error)
package/lib/index.cjs CHANGED
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(index_exports);
40
40
  // package.json
41
41
  var package_default = {
42
42
  name: "eslint-plugin-nextfriday",
43
- version: "1.1.1",
43
+ version: "1.2.0",
44
44
  description: "A comprehensive ESLint plugin providing custom rules and configurations for Next Friday development workflows.",
45
45
  keywords: [
46
46
  "eslint",
@@ -155,19 +155,111 @@ var package_default = {
155
155
  }
156
156
  };
157
157
 
158
- // src/rules/file-kebab-case.ts
159
- var import_path = __toESM(require("path"), 1);
158
+ // src/rules/enforce-readonly-component-props.ts
160
159
  var import_utils = require("@typescript-eslint/utils");
161
160
  var createRule = import_utils.ESLintUtils.RuleCreator(
162
161
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
163
162
  );
163
+ var enforceReadonlyComponentProps = createRule({
164
+ name: "enforce-readonly-component-props",
165
+ meta: {
166
+ type: "suggestion",
167
+ docs: {
168
+ description: "Enforce Readonly wrapper for React component props when using named types or interfaces"
169
+ },
170
+ fixable: "code",
171
+ schema: [],
172
+ messages: {
173
+ useReadonly: "Component props should be wrapped with Readonly<> for immutability"
174
+ }
175
+ },
176
+ defaultOptions: [],
177
+ create(context) {
178
+ function hasJSXInConditional(node) {
179
+ return node.consequent.type === import_utils.AST_NODE_TYPES.JSXElement || node.consequent.type === import_utils.AST_NODE_TYPES.JSXFragment || node.alternate.type === import_utils.AST_NODE_TYPES.JSXElement || node.alternate.type === import_utils.AST_NODE_TYPES.JSXFragment;
180
+ }
181
+ function hasJSXInLogical(node) {
182
+ return node.right.type === import_utils.AST_NODE_TYPES.JSXElement || node.right.type === import_utils.AST_NODE_TYPES.JSXFragment;
183
+ }
184
+ function hasJSXReturn(block) {
185
+ return block.body.some((stmt) => {
186
+ if (stmt.type === import_utils.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
187
+ return stmt.argument.type === import_utils.AST_NODE_TYPES.JSXElement || stmt.argument.type === import_utils.AST_NODE_TYPES.JSXFragment || stmt.argument.type === import_utils.AST_NODE_TYPES.ConditionalExpression && hasJSXInConditional(stmt.argument) || stmt.argument.type === import_utils.AST_NODE_TYPES.LogicalExpression && hasJSXInLogical(stmt.argument);
188
+ }
189
+ return false;
190
+ });
191
+ }
192
+ function isReactComponent(node) {
193
+ if (node.type === import_utils.AST_NODE_TYPES.ArrowFunctionExpression) {
194
+ if (node.body.type === import_utils.AST_NODE_TYPES.JSXElement || node.body.type === import_utils.AST_NODE_TYPES.JSXFragment) {
195
+ return true;
196
+ }
197
+ if (node.body.type === import_utils.AST_NODE_TYPES.BlockStatement) {
198
+ return hasJSXReturn(node.body);
199
+ }
200
+ } else if (node.type === import_utils.AST_NODE_TYPES.FunctionExpression || node.type === import_utils.AST_NODE_TYPES.FunctionDeclaration) {
201
+ if (node.body && node.body.type === import_utils.AST_NODE_TYPES.BlockStatement) {
202
+ return hasJSXReturn(node.body);
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+ function isNamedType(node) {
208
+ return node.type === import_utils.AST_NODE_TYPES.TSTypeReference;
209
+ }
210
+ function isAlreadyReadonly(node) {
211
+ if (node.type === import_utils.AST_NODE_TYPES.TSTypeReference && node.typeName) {
212
+ if (node.typeName.type === import_utils.AST_NODE_TYPES.Identifier && node.typeName.name === "Readonly") {
213
+ return true;
214
+ }
215
+ }
216
+ return false;
217
+ }
218
+ function checkFunction(node) {
219
+ if (!isReactComponent(node)) {
220
+ return;
221
+ }
222
+ if (node.params.length !== 1) {
223
+ return;
224
+ }
225
+ const param = node.params[0];
226
+ if (param.type === import_utils.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
227
+ const { typeAnnotation } = param.typeAnnotation;
228
+ if (isNamedType(typeAnnotation) && !isAlreadyReadonly(typeAnnotation)) {
229
+ const { sourceCode } = context;
230
+ const typeText = sourceCode.getText(typeAnnotation);
231
+ context.report({
232
+ node: param.typeAnnotation,
233
+ messageId: "useReadonly",
234
+ fix(fixer) {
235
+ return fixer.replaceText(typeAnnotation, `Readonly<${typeText}>`);
236
+ }
237
+ });
238
+ }
239
+ }
240
+ }
241
+ return {
242
+ ArrowFunctionExpression: checkFunction,
243
+ FunctionExpression: checkFunction,
244
+ FunctionDeclaration: checkFunction
245
+ };
246
+ }
247
+ });
248
+ var enforce_readonly_component_props_default = enforceReadonlyComponentProps;
249
+
250
+ // src/rules/file-kebab-case.ts
251
+ var import_path = __toESM(require("path"), 1);
252
+ var import_utils2 = require("@typescript-eslint/utils");
253
+ var createRule2 = import_utils2.ESLintUtils.RuleCreator(
254
+ (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
255
+ );
164
256
  var isKebabCase = (str) => {
165
257
  if (/\.(config|rc|setup|spec|test)$/.test(str)) {
166
258
  return /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+)*$/.test(str);
167
259
  }
168
260
  return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(str);
169
261
  };
170
- var fileKebabCase = createRule({
262
+ var fileKebabCase = createRule2({
171
263
  name: "file-kebab-case",
172
264
  meta: {
173
265
  type: "problem",
@@ -203,12 +295,12 @@ var file_kebab_case_default = fileKebabCase;
203
295
 
204
296
  // src/rules/jsx-pascal-case.ts
205
297
  var import_path2 = __toESM(require("path"), 1);
206
- var import_utils2 = require("@typescript-eslint/utils");
207
- var createRule2 = import_utils2.ESLintUtils.RuleCreator(
298
+ var import_utils3 = require("@typescript-eslint/utils");
299
+ var createRule3 = import_utils3.ESLintUtils.RuleCreator(
208
300
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
209
301
  );
210
302
  var isPascalCase = (str) => /^[A-Z][a-zA-Z0-9]*$/.test(str) && !/^[A-Z]+$/.test(str);
211
- var jsxPascalCase = createRule2({
303
+ var jsxPascalCase = createRule3({
212
304
  name: "jsx-pascal-case",
213
305
  meta: {
214
306
  type: "problem",
@@ -244,11 +336,11 @@ var jsx_pascal_case_default = jsxPascalCase;
244
336
 
245
337
  // src/rules/md-filename-case-restriction.ts
246
338
  var import_path3 = __toESM(require("path"), 1);
247
- var import_utils3 = require("@typescript-eslint/utils");
248
- var createRule3 = import_utils3.ESLintUtils.RuleCreator(
339
+ var import_utils4 = require("@typescript-eslint/utils");
340
+ var createRule4 = import_utils4.ESLintUtils.RuleCreator(
249
341
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
250
342
  );
251
- var mdFilenameCaseRestriction = createRule3({
343
+ var mdFilenameCaseRestriction = createRule4({
252
344
  name: "md-filename-case-restriction",
253
345
  meta: {
254
346
  type: "problem",
@@ -290,11 +382,11 @@ var md_filename_case_restriction_default = mdFilenameCaseRestriction;
290
382
 
291
383
  // src/rules/no-emoji.ts
292
384
  var import_emoji_regex = __toESM(require("emoji-regex"), 1);
293
- var import_utils4 = require("@typescript-eslint/utils");
294
- var createRule4 = import_utils4.ESLintUtils.RuleCreator(
385
+ var import_utils5 = require("@typescript-eslint/utils");
386
+ var createRule5 = import_utils5.ESLintUtils.RuleCreator(
295
387
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
296
388
  );
297
- var noEmoji = createRule4({
389
+ var noEmoji = createRule5({
298
390
  name: "no-emoji",
299
391
  meta: {
300
392
  type: "problem",
@@ -328,11 +420,11 @@ var noEmoji = createRule4({
328
420
  var no_emoji_default = noEmoji;
329
421
 
330
422
  // src/rules/no-explicit-return-type.ts
331
- var import_utils5 = require("@typescript-eslint/utils");
332
- var createRule5 = import_utils5.ESLintUtils.RuleCreator(
423
+ var import_utils6 = require("@typescript-eslint/utils");
424
+ var createRule6 = import_utils6.ESLintUtils.RuleCreator(
333
425
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
334
426
  );
335
- var noExplicitReturnType = createRule5({
427
+ var noExplicitReturnType = createRule6({
336
428
  name: "no-explicit-return-type",
337
429
  meta: {
338
430
  type: "suggestion",
@@ -372,11 +464,11 @@ var noExplicitReturnType = createRule5({
372
464
  var no_explicit_return_type_default = noExplicitReturnType;
373
465
 
374
466
  // src/rules/prefer-destructuring-params.ts
375
- var import_utils6 = require("@typescript-eslint/utils");
376
- var createRule6 = import_utils6.ESLintUtils.RuleCreator(
467
+ var import_utils7 = require("@typescript-eslint/utils");
468
+ var createRule7 = import_utils7.ESLintUtils.RuleCreator(
377
469
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
378
470
  );
379
- var preferDestructuringParams = createRule6({
471
+ var preferDestructuringParams = createRule7({
380
472
  name: "prefer-destructuring-params",
381
473
  meta: {
382
474
  type: "suggestion",
@@ -395,7 +487,7 @@ var preferDestructuringParams = createRule6({
395
487
  return;
396
488
  }
397
489
  const hasNonDestructuredParams = node.params.some(
398
- (param) => param.type !== import_utils6.AST_NODE_TYPES.ObjectPattern && param.type !== import_utils6.AST_NODE_TYPES.RestElement
490
+ (param) => param.type !== import_utils7.AST_NODE_TYPES.ObjectPattern && param.type !== import_utils7.AST_NODE_TYPES.RestElement
399
491
  );
400
492
  if (hasNonDestructuredParams) {
401
493
  context.report({
@@ -414,11 +506,11 @@ var preferDestructuringParams = createRule6({
414
506
  var prefer_destructuring_params_default = preferDestructuringParams;
415
507
 
416
508
  // src/rules/prefer-import-type.ts
417
- var import_utils7 = require("@typescript-eslint/utils");
418
- var createRule7 = import_utils7.ESLintUtils.RuleCreator(
509
+ var import_utils8 = require("@typescript-eslint/utils");
510
+ var createRule8 = import_utils8.ESLintUtils.RuleCreator(
419
511
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
420
512
  );
421
- var preferImportType = createRule7({
513
+ var preferImportType = createRule8({
422
514
  name: "prefer-import-type",
423
515
  meta: {
424
516
  type: "suggestion",
@@ -449,14 +541,14 @@ var preferImportType = createRule7({
449
541
  return;
450
542
  }
451
543
  const isTypeOnlyImport = node.specifiers.every((specifier) => {
452
- if (specifier.type === import_utils7.AST_NODE_TYPES.ImportDefaultSpecifier) {
544
+ if (specifier.type === import_utils8.AST_NODE_TYPES.ImportDefaultSpecifier) {
453
545
  return false;
454
546
  }
455
- if (specifier.type === import_utils7.AST_NODE_TYPES.ImportNamespaceSpecifier) {
547
+ if (specifier.type === import_utils8.AST_NODE_TYPES.ImportNamespaceSpecifier) {
456
548
  return false;
457
549
  }
458
- if (specifier.type === import_utils7.AST_NODE_TYPES.ImportSpecifier) {
459
- const importedName = specifier.imported.type === import_utils7.AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value;
550
+ if (specifier.type === import_utils8.AST_NODE_TYPES.ImportSpecifier) {
551
+ const importedName = specifier.imported.type === import_utils8.AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value;
460
552
  const isKnownTypeOnly = node.source.value === "@typescript-eslint/utils" && ["TSESTree", "RuleContext"].includes(importedName) || node.source.value === "react" && ["Component", "ComponentProps", "ReactNode", "FC", "JSX", "ReactElement", "PropsWithChildren"].includes(
461
553
  importedName
462
554
  ) || importedName.endsWith("Type") || importedName.endsWith("Interface") || importedName.endsWith("Props");
@@ -484,11 +576,11 @@ var preferImportType = createRule7({
484
576
  var prefer_import_type_default = preferImportType;
485
577
 
486
578
  // src/rules/prefer-interface-over-inline-types.ts
487
- var import_utils8 = require("@typescript-eslint/utils");
488
- var createRule8 = import_utils8.ESLintUtils.RuleCreator(
579
+ var import_utils9 = require("@typescript-eslint/utils");
580
+ var createRule9 = import_utils9.ESLintUtils.RuleCreator(
489
581
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
490
582
  );
491
- var preferInterfaceOverInlineTypes = createRule8({
583
+ var preferInterfaceOverInlineTypes = createRule9({
492
584
  name: "prefer-interface-over-inline-types",
493
585
  meta: {
494
586
  type: "suggestion",
@@ -504,54 +596,54 @@ var preferInterfaceOverInlineTypes = createRule8({
504
596
  defaultOptions: [],
505
597
  create(context) {
506
598
  function hasJSXInConditional(node) {
507
- return node.consequent.type === import_utils8.AST_NODE_TYPES.JSXElement || node.consequent.type === import_utils8.AST_NODE_TYPES.JSXFragment || node.alternate.type === import_utils8.AST_NODE_TYPES.JSXElement || node.alternate.type === import_utils8.AST_NODE_TYPES.JSXFragment;
599
+ return node.consequent.type === import_utils9.AST_NODE_TYPES.JSXElement || node.consequent.type === import_utils9.AST_NODE_TYPES.JSXFragment || node.alternate.type === import_utils9.AST_NODE_TYPES.JSXElement || node.alternate.type === import_utils9.AST_NODE_TYPES.JSXFragment;
508
600
  }
509
601
  function hasJSXInLogical(node) {
510
- return node.right.type === import_utils8.AST_NODE_TYPES.JSXElement || node.right.type === import_utils8.AST_NODE_TYPES.JSXFragment;
602
+ return node.right.type === import_utils9.AST_NODE_TYPES.JSXElement || node.right.type === import_utils9.AST_NODE_TYPES.JSXFragment;
511
603
  }
512
604
  function hasJSXReturn(block) {
513
605
  return block.body.some((stmt) => {
514
- if (stmt.type === import_utils8.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
515
- return stmt.argument.type === import_utils8.AST_NODE_TYPES.JSXElement || stmt.argument.type === import_utils8.AST_NODE_TYPES.JSXFragment || stmt.argument.type === import_utils8.AST_NODE_TYPES.ConditionalExpression && hasJSXInConditional(stmt.argument) || stmt.argument.type === import_utils8.AST_NODE_TYPES.LogicalExpression && hasJSXInLogical(stmt.argument);
606
+ if (stmt.type === import_utils9.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
607
+ return stmt.argument.type === import_utils9.AST_NODE_TYPES.JSXElement || stmt.argument.type === import_utils9.AST_NODE_TYPES.JSXFragment || stmt.argument.type === import_utils9.AST_NODE_TYPES.ConditionalExpression && hasJSXInConditional(stmt.argument) || stmt.argument.type === import_utils9.AST_NODE_TYPES.LogicalExpression && hasJSXInLogical(stmt.argument);
516
608
  }
517
609
  return false;
518
610
  });
519
611
  }
520
612
  function isReactComponent(node) {
521
- if (node.type === import_utils8.AST_NODE_TYPES.ArrowFunctionExpression) {
522
- if (node.body.type === import_utils8.AST_NODE_TYPES.JSXElement || node.body.type === import_utils8.AST_NODE_TYPES.JSXFragment) {
613
+ if (node.type === import_utils9.AST_NODE_TYPES.ArrowFunctionExpression) {
614
+ if (node.body.type === import_utils9.AST_NODE_TYPES.JSXElement || node.body.type === import_utils9.AST_NODE_TYPES.JSXFragment) {
523
615
  return true;
524
616
  }
525
- if (node.body.type === import_utils8.AST_NODE_TYPES.BlockStatement) {
617
+ if (node.body.type === import_utils9.AST_NODE_TYPES.BlockStatement) {
526
618
  return hasJSXReturn(node.body);
527
619
  }
528
- } else if (node.type === import_utils8.AST_NODE_TYPES.FunctionExpression || node.type === import_utils8.AST_NODE_TYPES.FunctionDeclaration) {
529
- if (node.body && node.body.type === import_utils8.AST_NODE_TYPES.BlockStatement) {
620
+ } else if (node.type === import_utils9.AST_NODE_TYPES.FunctionExpression || node.type === import_utils9.AST_NODE_TYPES.FunctionDeclaration) {
621
+ if (node.body && node.body.type === import_utils9.AST_NODE_TYPES.BlockStatement) {
530
622
  return hasJSXReturn(node.body);
531
623
  }
532
624
  }
533
625
  return false;
534
626
  }
535
627
  function isInlineTypeAnnotation(node) {
536
- if (node.type === import_utils8.AST_NODE_TYPES.TSTypeLiteral) {
628
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral) {
537
629
  return true;
538
630
  }
539
- if (node.type === import_utils8.AST_NODE_TYPES.TSTypeReference && node.typeArguments) {
540
- return node.typeArguments.params.some((param) => param.type === import_utils8.AST_NODE_TYPES.TSTypeLiteral);
631
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeReference && node.typeArguments) {
632
+ return node.typeArguments.params.some((param) => param.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral);
541
633
  }
542
- if (node.type === import_utils8.AST_NODE_TYPES.TSUnionType) {
634
+ if (node.type === import_utils9.AST_NODE_TYPES.TSUnionType) {
543
635
  return node.types.some((type) => isInlineTypeAnnotation(type));
544
636
  }
545
637
  return false;
546
638
  }
547
639
  function hasInlineObjectType(node) {
548
- if (node.type === import_utils8.AST_NODE_TYPES.TSTypeLiteral) {
640
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral) {
549
641
  return true;
550
642
  }
551
- if (node.type === import_utils8.AST_NODE_TYPES.TSTypeReference && node.typeArguments) {
552
- return node.typeArguments.params.some((param) => param.type === import_utils8.AST_NODE_TYPES.TSTypeLiteral);
643
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeReference && node.typeArguments) {
644
+ return node.typeArguments.params.some((param) => param.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral);
553
645
  }
554
- if (node.type === import_utils8.AST_NODE_TYPES.TSUnionType) {
646
+ if (node.type === import_utils9.AST_NODE_TYPES.TSUnionType) {
555
647
  return node.types.some((type) => hasInlineObjectType(type));
556
648
  }
557
649
  return false;
@@ -564,7 +656,7 @@ var preferInterfaceOverInlineTypes = createRule8({
564
656
  return;
565
657
  }
566
658
  const param = node.params[0];
567
- if (param.type === import_utils8.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
659
+ if (param.type === import_utils9.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
568
660
  const { typeAnnotation } = param.typeAnnotation;
569
661
  if (isInlineTypeAnnotation(typeAnnotation) && hasInlineObjectType(typeAnnotation)) {
570
662
  context.report({
@@ -584,11 +676,11 @@ var preferInterfaceOverInlineTypes = createRule8({
584
676
  var prefer_interface_over_inline_types_default = preferInterfaceOverInlineTypes;
585
677
 
586
678
  // src/rules/prefer-react-import-types.ts
587
- var import_utils9 = require("@typescript-eslint/utils");
588
- var createRule9 = import_utils9.ESLintUtils.RuleCreator(
679
+ var import_utils10 = require("@typescript-eslint/utils");
680
+ var createRule10 = import_utils10.ESLintUtils.RuleCreator(
589
681
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
590
682
  );
591
- var preferReactImportTypes = createRule9({
683
+ var preferReactImportTypes = createRule10({
592
684
  name: "prefer-react-import-types",
593
685
  meta: {
594
686
  type: "suggestion",
@@ -664,7 +756,7 @@ var preferReactImportTypes = createRule9({
664
756
  ]);
665
757
  const allReactExports = /* @__PURE__ */ new Set([...reactTypes, ...reactRuntimeExports]);
666
758
  function checkMemberExpression(node) {
667
- if (node.object.type === import_utils9.AST_NODE_TYPES.Identifier && node.object.name === "React" && node.property.type === import_utils9.AST_NODE_TYPES.Identifier && allReactExports.has(node.property.name)) {
759
+ if (node.object.type === import_utils10.AST_NODE_TYPES.Identifier && node.object.name === "React" && node.property.type === import_utils10.AST_NODE_TYPES.Identifier && allReactExports.has(node.property.name)) {
668
760
  const typeName = node.property.name;
669
761
  const isType = reactTypes.has(typeName);
670
762
  const importStatement = isType ? `import type { ${typeName} } from "react"` : `import { ${typeName} } from "react"`;
@@ -681,7 +773,7 @@ var preferReactImportTypes = createRule9({
681
773
  return {
682
774
  MemberExpression: checkMemberExpression,
683
775
  "TSTypeReference > TSQualifiedName": (node) => {
684
- if (node.left.type === import_utils9.AST_NODE_TYPES.Identifier && node.left.name === "React" && node.right.type === import_utils9.AST_NODE_TYPES.Identifier && allReactExports.has(node.right.name)) {
776
+ if (node.left.type === import_utils10.AST_NODE_TYPES.Identifier && node.left.name === "React" && node.right.type === import_utils10.AST_NODE_TYPES.Identifier && allReactExports.has(node.right.name)) {
685
777
  const typeName = node.right.name;
686
778
  const isType = reactTypes.has(typeName);
687
779
  const importStatement = isType ? `import type { ${typeName} } from "react"` : `import { ${typeName} } from "react"`;
@@ -701,11 +793,11 @@ var preferReactImportTypes = createRule9({
701
793
  var prefer_react_import_types_default = preferReactImportTypes;
702
794
 
703
795
  // src/rules/react-props-destructure.ts
704
- var import_utils10 = require("@typescript-eslint/utils");
705
- var createRule10 = import_utils10.ESLintUtils.RuleCreator(
796
+ var import_utils11 = require("@typescript-eslint/utils");
797
+ var createRule11 = import_utils11.ESLintUtils.RuleCreator(
706
798
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
707
799
  );
708
- var reactPropsDestructure = createRule10({
800
+ var reactPropsDestructure = createRule11({
709
801
  name: "react-props-destructure",
710
802
  meta: {
711
803
  type: "suggestion",
@@ -721,29 +813,29 @@ var reactPropsDestructure = createRule10({
721
813
  defaultOptions: [],
722
814
  create(context) {
723
815
  function hasJSXInConditional(node) {
724
- return node.consequent.type === import_utils10.AST_NODE_TYPES.JSXElement || node.consequent.type === import_utils10.AST_NODE_TYPES.JSXFragment || node.alternate.type === import_utils10.AST_NODE_TYPES.JSXElement || node.alternate.type === import_utils10.AST_NODE_TYPES.JSXFragment;
816
+ return node.consequent.type === import_utils11.AST_NODE_TYPES.JSXElement || node.consequent.type === import_utils11.AST_NODE_TYPES.JSXFragment || node.alternate.type === import_utils11.AST_NODE_TYPES.JSXElement || node.alternate.type === import_utils11.AST_NODE_TYPES.JSXFragment;
725
817
  }
726
818
  function hasJSXInLogical(node) {
727
- return node.right.type === import_utils10.AST_NODE_TYPES.JSXElement || node.right.type === import_utils10.AST_NODE_TYPES.JSXFragment;
819
+ return node.right.type === import_utils11.AST_NODE_TYPES.JSXElement || node.right.type === import_utils11.AST_NODE_TYPES.JSXFragment;
728
820
  }
729
821
  function hasJSXReturn(block) {
730
822
  return block.body.some((stmt) => {
731
- if (stmt.type === import_utils10.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
732
- return stmt.argument.type === import_utils10.AST_NODE_TYPES.JSXElement || stmt.argument.type === import_utils10.AST_NODE_TYPES.JSXFragment || stmt.argument.type === import_utils10.AST_NODE_TYPES.ConditionalExpression && hasJSXInConditional(stmt.argument) || stmt.argument.type === import_utils10.AST_NODE_TYPES.LogicalExpression && hasJSXInLogical(stmt.argument);
823
+ if (stmt.type === import_utils11.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
824
+ return stmt.argument.type === import_utils11.AST_NODE_TYPES.JSXElement || stmt.argument.type === import_utils11.AST_NODE_TYPES.JSXFragment || stmt.argument.type === import_utils11.AST_NODE_TYPES.ConditionalExpression && hasJSXInConditional(stmt.argument) || stmt.argument.type === import_utils11.AST_NODE_TYPES.LogicalExpression && hasJSXInLogical(stmt.argument);
733
825
  }
734
826
  return false;
735
827
  });
736
828
  }
737
829
  function isReactComponent(node) {
738
- if (node.type === import_utils10.AST_NODE_TYPES.ArrowFunctionExpression) {
739
- if (node.body.type === import_utils10.AST_NODE_TYPES.JSXElement || node.body.type === import_utils10.AST_NODE_TYPES.JSXFragment) {
830
+ if (node.type === import_utils11.AST_NODE_TYPES.ArrowFunctionExpression) {
831
+ if (node.body.type === import_utils11.AST_NODE_TYPES.JSXElement || node.body.type === import_utils11.AST_NODE_TYPES.JSXFragment) {
740
832
  return true;
741
833
  }
742
- if (node.body.type === import_utils10.AST_NODE_TYPES.BlockStatement) {
834
+ if (node.body.type === import_utils11.AST_NODE_TYPES.BlockStatement) {
743
835
  return hasJSXReturn(node.body);
744
836
  }
745
- } else if (node.type === import_utils10.AST_NODE_TYPES.FunctionExpression || node.type === import_utils10.AST_NODE_TYPES.FunctionDeclaration) {
746
- if (node.body && node.body.type === import_utils10.AST_NODE_TYPES.BlockStatement) {
837
+ } else if (node.type === import_utils11.AST_NODE_TYPES.FunctionExpression || node.type === import_utils11.AST_NODE_TYPES.FunctionDeclaration) {
838
+ if (node.body && node.body.type === import_utils11.AST_NODE_TYPES.BlockStatement) {
747
839
  return hasJSXReturn(node.body);
748
840
  }
749
841
  }
@@ -757,9 +849,9 @@ var reactPropsDestructure = createRule10({
757
849
  return;
758
850
  }
759
851
  const param = node.params[0];
760
- if (param.type === import_utils10.AST_NODE_TYPES.ObjectPattern) {
761
- const properties = param.properties.filter((prop) => prop.type === import_utils10.AST_NODE_TYPES.Property).map((prop) => {
762
- if (prop.key.type === import_utils10.AST_NODE_TYPES.Identifier) {
852
+ if (param.type === import_utils11.AST_NODE_TYPES.ObjectPattern) {
853
+ const properties = param.properties.filter((prop) => prop.type === import_utils11.AST_NODE_TYPES.Property).map((prop) => {
854
+ if (prop.key.type === import_utils11.AST_NODE_TYPES.Identifier) {
763
855
  return prop.key.name;
764
856
  }
765
857
  return null;
@@ -791,6 +883,7 @@ var meta = {
791
883
  version: package_default.version
792
884
  };
793
885
  var rules = {
886
+ "enforce-readonly-component-props": enforce_readonly_component_props_default,
794
887
  "file-kebab-case": file_kebab_case_default,
795
888
  "jsx-pascal-case": jsx_pascal_case_default,
796
889
  "md-filename-case-restriction": md_filename_case_restriction_default,
@@ -827,12 +920,14 @@ var baseRecommendedRules = {
827
920
  var jsxRules = {
828
921
  "nextfriday/jsx-pascal-case": "warn",
829
922
  "nextfriday/prefer-interface-over-inline-types": "warn",
830
- "nextfriday/react-props-destructure": "warn"
923
+ "nextfriday/react-props-destructure": "warn",
924
+ "nextfriday/enforce-readonly-component-props": "warn"
831
925
  };
832
926
  var jsxRecommendedRules = {
833
927
  "nextfriday/jsx-pascal-case": "error",
834
928
  "nextfriday/prefer-interface-over-inline-types": "error",
835
- "nextfriday/react-props-destructure": "error"
929
+ "nextfriday/react-props-destructure": "error",
930
+ "nextfriday/enforce-readonly-component-props": "error"
836
931
  };
837
932
  var createConfig = (configRules) => ({
838
933
  plugins: {