eslint-plugin-nextfriday 1.1.1 → 1.2.1

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,17 @@
1
1
  # eslint-plugin-nextfriday
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#18](https://github.com/next-friday/eslint-plugin-nextfriday/pull/18) [`ddfad3d`](https://github.com/next-friday/eslint-plugin-nextfriday/commit/ddfad3d5a112c429ee084c134517dad8cac01c7c) Thanks [@nextfridaydeveloper](https://github.com/nextfridaydeveloper)! - Fixed prefer-destructuring-params rule to only target user-defined functions and skip built-in/library/third-party functions.
8
+
9
+ ## 1.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#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.
14
+
3
15
  ## 1.1.1
4
16
 
5
17
  ### 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.1",
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",
@@ -391,11 +483,21 @@ var preferDestructuringParams = createRule6({
391
483
  defaultOptions: [],
392
484
  create(context) {
393
485
  const checkFunction = (node) => {
486
+ const { filename } = context;
487
+ if (filename.includes("node_modules") || filename.includes(".d.ts")) {
488
+ return;
489
+ }
490
+ if (node.type === import_utils7.AST_NODE_TYPES.FunctionDeclaration && node.id) {
491
+ const functionName = node.id.name;
492
+ if (functionName.startsWith("_") || functionName.includes("$") || /^[A-Z][a-zA-Z]*$/.test(functionName)) {
493
+ return;
494
+ }
495
+ }
394
496
  if (node.params.length <= 1) {
395
497
  return;
396
498
  }
397
499
  const hasNonDestructuredParams = node.params.some(
398
- (param) => param.type !== import_utils6.AST_NODE_TYPES.ObjectPattern && param.type !== import_utils6.AST_NODE_TYPES.RestElement
500
+ (param) => param.type !== import_utils7.AST_NODE_TYPES.ObjectPattern && param.type !== import_utils7.AST_NODE_TYPES.RestElement
399
501
  );
400
502
  if (hasNonDestructuredParams) {
401
503
  context.report({
@@ -414,11 +516,11 @@ var preferDestructuringParams = createRule6({
414
516
  var prefer_destructuring_params_default = preferDestructuringParams;
415
517
 
416
518
  // src/rules/prefer-import-type.ts
417
- var import_utils7 = require("@typescript-eslint/utils");
418
- var createRule7 = import_utils7.ESLintUtils.RuleCreator(
519
+ var import_utils8 = require("@typescript-eslint/utils");
520
+ var createRule8 = import_utils8.ESLintUtils.RuleCreator(
419
521
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
420
522
  );
421
- var preferImportType = createRule7({
523
+ var preferImportType = createRule8({
422
524
  name: "prefer-import-type",
423
525
  meta: {
424
526
  type: "suggestion",
@@ -449,14 +551,14 @@ var preferImportType = createRule7({
449
551
  return;
450
552
  }
451
553
  const isTypeOnlyImport = node.specifiers.every((specifier) => {
452
- if (specifier.type === import_utils7.AST_NODE_TYPES.ImportDefaultSpecifier) {
554
+ if (specifier.type === import_utils8.AST_NODE_TYPES.ImportDefaultSpecifier) {
453
555
  return false;
454
556
  }
455
- if (specifier.type === import_utils7.AST_NODE_TYPES.ImportNamespaceSpecifier) {
557
+ if (specifier.type === import_utils8.AST_NODE_TYPES.ImportNamespaceSpecifier) {
456
558
  return false;
457
559
  }
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;
560
+ if (specifier.type === import_utils8.AST_NODE_TYPES.ImportSpecifier) {
561
+ const importedName = specifier.imported.type === import_utils8.AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value;
460
562
  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
563
  importedName
462
564
  ) || importedName.endsWith("Type") || importedName.endsWith("Interface") || importedName.endsWith("Props");
@@ -484,11 +586,11 @@ var preferImportType = createRule7({
484
586
  var prefer_import_type_default = preferImportType;
485
587
 
486
588
  // src/rules/prefer-interface-over-inline-types.ts
487
- var import_utils8 = require("@typescript-eslint/utils");
488
- var createRule8 = import_utils8.ESLintUtils.RuleCreator(
589
+ var import_utils9 = require("@typescript-eslint/utils");
590
+ var createRule9 = import_utils9.ESLintUtils.RuleCreator(
489
591
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
490
592
  );
491
- var preferInterfaceOverInlineTypes = createRule8({
593
+ var preferInterfaceOverInlineTypes = createRule9({
492
594
  name: "prefer-interface-over-inline-types",
493
595
  meta: {
494
596
  type: "suggestion",
@@ -504,54 +606,54 @@ var preferInterfaceOverInlineTypes = createRule8({
504
606
  defaultOptions: [],
505
607
  create(context) {
506
608
  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;
609
+ 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
610
  }
509
611
  function hasJSXInLogical(node) {
510
- return node.right.type === import_utils8.AST_NODE_TYPES.JSXElement || node.right.type === import_utils8.AST_NODE_TYPES.JSXFragment;
612
+ return node.right.type === import_utils9.AST_NODE_TYPES.JSXElement || node.right.type === import_utils9.AST_NODE_TYPES.JSXFragment;
511
613
  }
512
614
  function hasJSXReturn(block) {
513
615
  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);
616
+ if (stmt.type === import_utils9.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
617
+ 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
618
  }
517
619
  return false;
518
620
  });
519
621
  }
520
622
  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) {
623
+ if (node.type === import_utils9.AST_NODE_TYPES.ArrowFunctionExpression) {
624
+ if (node.body.type === import_utils9.AST_NODE_TYPES.JSXElement || node.body.type === import_utils9.AST_NODE_TYPES.JSXFragment) {
523
625
  return true;
524
626
  }
525
- if (node.body.type === import_utils8.AST_NODE_TYPES.BlockStatement) {
627
+ if (node.body.type === import_utils9.AST_NODE_TYPES.BlockStatement) {
526
628
  return hasJSXReturn(node.body);
527
629
  }
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) {
630
+ } else if (node.type === import_utils9.AST_NODE_TYPES.FunctionExpression || node.type === import_utils9.AST_NODE_TYPES.FunctionDeclaration) {
631
+ if (node.body && node.body.type === import_utils9.AST_NODE_TYPES.BlockStatement) {
530
632
  return hasJSXReturn(node.body);
531
633
  }
532
634
  }
533
635
  return false;
534
636
  }
535
637
  function isInlineTypeAnnotation(node) {
536
- if (node.type === import_utils8.AST_NODE_TYPES.TSTypeLiteral) {
638
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral) {
537
639
  return true;
538
640
  }
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);
641
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeReference && node.typeArguments) {
642
+ return node.typeArguments.params.some((param) => param.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral);
541
643
  }
542
- if (node.type === import_utils8.AST_NODE_TYPES.TSUnionType) {
644
+ if (node.type === import_utils9.AST_NODE_TYPES.TSUnionType) {
543
645
  return node.types.some((type) => isInlineTypeAnnotation(type));
544
646
  }
545
647
  return false;
546
648
  }
547
649
  function hasInlineObjectType(node) {
548
- if (node.type === import_utils8.AST_NODE_TYPES.TSTypeLiteral) {
650
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral) {
549
651
  return true;
550
652
  }
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);
653
+ if (node.type === import_utils9.AST_NODE_TYPES.TSTypeReference && node.typeArguments) {
654
+ return node.typeArguments.params.some((param) => param.type === import_utils9.AST_NODE_TYPES.TSTypeLiteral);
553
655
  }
554
- if (node.type === import_utils8.AST_NODE_TYPES.TSUnionType) {
656
+ if (node.type === import_utils9.AST_NODE_TYPES.TSUnionType) {
555
657
  return node.types.some((type) => hasInlineObjectType(type));
556
658
  }
557
659
  return false;
@@ -564,7 +666,7 @@ var preferInterfaceOverInlineTypes = createRule8({
564
666
  return;
565
667
  }
566
668
  const param = node.params[0];
567
- if (param.type === import_utils8.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
669
+ if (param.type === import_utils9.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
568
670
  const { typeAnnotation } = param.typeAnnotation;
569
671
  if (isInlineTypeAnnotation(typeAnnotation) && hasInlineObjectType(typeAnnotation)) {
570
672
  context.report({
@@ -584,11 +686,11 @@ var preferInterfaceOverInlineTypes = createRule8({
584
686
  var prefer_interface_over_inline_types_default = preferInterfaceOverInlineTypes;
585
687
 
586
688
  // src/rules/prefer-react-import-types.ts
587
- var import_utils9 = require("@typescript-eslint/utils");
588
- var createRule9 = import_utils9.ESLintUtils.RuleCreator(
689
+ var import_utils10 = require("@typescript-eslint/utils");
690
+ var createRule10 = import_utils10.ESLintUtils.RuleCreator(
589
691
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
590
692
  );
591
- var preferReactImportTypes = createRule9({
693
+ var preferReactImportTypes = createRule10({
592
694
  name: "prefer-react-import-types",
593
695
  meta: {
594
696
  type: "suggestion",
@@ -664,7 +766,7 @@ var preferReactImportTypes = createRule9({
664
766
  ]);
665
767
  const allReactExports = /* @__PURE__ */ new Set([...reactTypes, ...reactRuntimeExports]);
666
768
  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)) {
769
+ 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
770
  const typeName = node.property.name;
669
771
  const isType = reactTypes.has(typeName);
670
772
  const importStatement = isType ? `import type { ${typeName} } from "react"` : `import { ${typeName} } from "react"`;
@@ -681,7 +783,7 @@ var preferReactImportTypes = createRule9({
681
783
  return {
682
784
  MemberExpression: checkMemberExpression,
683
785
  "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)) {
786
+ 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
787
  const typeName = node.right.name;
686
788
  const isType = reactTypes.has(typeName);
687
789
  const importStatement = isType ? `import type { ${typeName} } from "react"` : `import { ${typeName} } from "react"`;
@@ -701,11 +803,11 @@ var preferReactImportTypes = createRule9({
701
803
  var prefer_react_import_types_default = preferReactImportTypes;
702
804
 
703
805
  // src/rules/react-props-destructure.ts
704
- var import_utils10 = require("@typescript-eslint/utils");
705
- var createRule10 = import_utils10.ESLintUtils.RuleCreator(
806
+ var import_utils11 = require("@typescript-eslint/utils");
807
+ var createRule11 = import_utils11.ESLintUtils.RuleCreator(
706
808
  (name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name}.md`
707
809
  );
708
- var reactPropsDestructure = createRule10({
810
+ var reactPropsDestructure = createRule11({
709
811
  name: "react-props-destructure",
710
812
  meta: {
711
813
  type: "suggestion",
@@ -721,29 +823,29 @@ var reactPropsDestructure = createRule10({
721
823
  defaultOptions: [],
722
824
  create(context) {
723
825
  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;
826
+ 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
827
  }
726
828
  function hasJSXInLogical(node) {
727
- return node.right.type === import_utils10.AST_NODE_TYPES.JSXElement || node.right.type === import_utils10.AST_NODE_TYPES.JSXFragment;
829
+ return node.right.type === import_utils11.AST_NODE_TYPES.JSXElement || node.right.type === import_utils11.AST_NODE_TYPES.JSXFragment;
728
830
  }
729
831
  function hasJSXReturn(block) {
730
832
  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);
833
+ if (stmt.type === import_utils11.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
834
+ 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
835
  }
734
836
  return false;
735
837
  });
736
838
  }
737
839
  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) {
840
+ if (node.type === import_utils11.AST_NODE_TYPES.ArrowFunctionExpression) {
841
+ if (node.body.type === import_utils11.AST_NODE_TYPES.JSXElement || node.body.type === import_utils11.AST_NODE_TYPES.JSXFragment) {
740
842
  return true;
741
843
  }
742
- if (node.body.type === import_utils10.AST_NODE_TYPES.BlockStatement) {
844
+ if (node.body.type === import_utils11.AST_NODE_TYPES.BlockStatement) {
743
845
  return hasJSXReturn(node.body);
744
846
  }
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) {
847
+ } else if (node.type === import_utils11.AST_NODE_TYPES.FunctionExpression || node.type === import_utils11.AST_NODE_TYPES.FunctionDeclaration) {
848
+ if (node.body && node.body.type === import_utils11.AST_NODE_TYPES.BlockStatement) {
747
849
  return hasJSXReturn(node.body);
748
850
  }
749
851
  }
@@ -757,9 +859,9 @@ var reactPropsDestructure = createRule10({
757
859
  return;
758
860
  }
759
861
  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) {
862
+ if (param.type === import_utils11.AST_NODE_TYPES.ObjectPattern) {
863
+ const properties = param.properties.filter((prop) => prop.type === import_utils11.AST_NODE_TYPES.Property).map((prop) => {
864
+ if (prop.key.type === import_utils11.AST_NODE_TYPES.Identifier) {
763
865
  return prop.key.name;
764
866
  }
765
867
  return null;
@@ -791,6 +893,7 @@ var meta = {
791
893
  version: package_default.version
792
894
  };
793
895
  var rules = {
896
+ "enforce-readonly-component-props": enforce_readonly_component_props_default,
794
897
  "file-kebab-case": file_kebab_case_default,
795
898
  "jsx-pascal-case": jsx_pascal_case_default,
796
899
  "md-filename-case-restriction": md_filename_case_restriction_default,
@@ -827,12 +930,14 @@ var baseRecommendedRules = {
827
930
  var jsxRules = {
828
931
  "nextfriday/jsx-pascal-case": "warn",
829
932
  "nextfriday/prefer-interface-over-inline-types": "warn",
830
- "nextfriday/react-props-destructure": "warn"
933
+ "nextfriday/react-props-destructure": "warn",
934
+ "nextfriday/enforce-readonly-component-props": "warn"
831
935
  };
832
936
  var jsxRecommendedRules = {
833
937
  "nextfriday/jsx-pascal-case": "error",
834
938
  "nextfriday/prefer-interface-over-inline-types": "error",
835
- "nextfriday/react-props-destructure": "error"
939
+ "nextfriday/react-props-destructure": "error",
940
+ "nextfriday/enforce-readonly-component-props": "error"
836
941
  };
837
942
  var createConfig = (configRules) => ({
838
943
  plugins: {