@mui/internal-code-infra 0.0.4-canary.9 → 0.0.4-canary.91

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.
Files changed (140) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +19 -8
  3. package/build/babel-config.d.mts +5 -1
  4. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  5. package/build/brokenLinksChecker/index.d.mts +45 -2
  6. package/build/changelog/types.d.ts +3 -1
  7. package/build/cli/cmdArgosPush.d.mts +2 -2
  8. package/build/cli/cmdBuild.d.mts +3 -2
  9. package/build/cli/cmdCopyFiles.d.mts +2 -2
  10. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  11. package/build/cli/cmdGenerateChangelog.d.mts +2 -2
  12. package/build/cli/cmdGithubAuth.d.mts +2 -2
  13. package/build/cli/cmdListWorkspaces.d.mts +6 -4
  14. package/build/cli/cmdNetlifyIgnore.d.mts +3 -2
  15. package/build/cli/cmdPublish.d.mts +4 -2
  16. package/build/cli/cmdPublishCanary.d.mts +3 -3
  17. package/build/cli/cmdPublishNewPackage.d.mts +4 -2
  18. package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
  19. package/build/cli/cmdVale.d.mts +46 -0
  20. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  21. package/build/eslint/baseConfig.d.mts +3 -1
  22. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  23. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  24. package/build/eslint/mui/rules/no-floating-cleanup.d.mts +5 -0
  25. package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
  26. package/build/eslint/mui/rules/no-presentation-role.d.mts +5 -0
  27. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
  28. package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
  29. package/build/remark/config.d.mts +43 -0
  30. package/build/remark/createLintTester.d.mts +10 -0
  31. package/build/remark/firstBlockHeading.d.mts +4 -0
  32. package/build/remark/gitDiff.d.mts +2 -0
  33. package/build/remark/noSpaceInLinks.d.mts +2 -0
  34. package/build/remark/straightQuotes.d.mts +2 -0
  35. package/build/remark/tableAlignment.d.mts +2 -0
  36. package/build/remark/terminalLanguage.d.mts +2 -0
  37. package/build/utils/babel.d.mts +1 -1
  38. package/build/utils/build.d.mts +56 -37
  39. package/build/utils/git.d.mts +7 -0
  40. package/build/utils/github.d.mts +1 -1
  41. package/build/utils/pnpm.d.mts +81 -2
  42. package/build/utils/testUtils.d.mts +7 -0
  43. package/build/utils/typescript.d.mts +6 -16
  44. package/package.json +74 -50
  45. package/src/babel-config.mjs +3 -1
  46. package/src/brokenLinksChecker/crawlWorker.mjs +240 -0
  47. package/src/brokenLinksChecker/index.mjs +263 -188
  48. package/src/changelog/fetchChangelogs.mjs +8 -2
  49. package/src/changelog/renderChangelog.mjs +1 -1
  50. package/src/changelog/types.ts +3 -1
  51. package/src/cli/cmdBuild.mjs +51 -85
  52. package/src/cli/cmdListWorkspaces.mjs +12 -27
  53. package/src/cli/cmdNetlifyIgnore.mjs +34 -93
  54. package/src/cli/cmdPublish.mjs +95 -20
  55. package/src/cli/cmdPublishCanary.mjs +128 -132
  56. package/src/cli/cmdPublishNewPackage.mjs +27 -6
  57. package/src/cli/cmdSetVersionOverrides.mjs +8 -9
  58. package/src/cli/cmdVale.mjs +513 -0
  59. package/src/cli/index.mjs +2 -0
  60. package/src/cli/packageJson.d.ts +2 -3
  61. package/src/eslint/baseConfig.mjs +51 -19
  62. package/src/eslint/docsConfig.mjs +2 -1
  63. package/src/eslint/mui/config.mjs +24 -4
  64. package/src/eslint/mui/index.mjs +6 -0
  65. package/src/eslint/mui/rules/no-floating-cleanup.mjs +187 -0
  66. package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
  67. package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
  68. package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
  69. package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
  70. package/src/estree-typescript.d.ts +1 -1
  71. package/src/remark/config.mjs +157 -0
  72. package/src/remark/createLintTester.mjs +19 -0
  73. package/src/remark/firstBlockHeading.mjs +87 -0
  74. package/src/remark/gitDiff.mjs +43 -0
  75. package/src/remark/noSpaceInLinks.mjs +42 -0
  76. package/src/remark/straightQuotes.mjs +31 -0
  77. package/src/remark/tableAlignment.mjs +23 -0
  78. package/src/remark/terminalLanguage.mjs +19 -0
  79. package/src/untyped-plugins.d.ts +11 -11
  80. package/src/utils/build.mjs +604 -270
  81. package/src/utils/git.mjs +27 -7
  82. package/src/utils/pnpm.mjs +277 -10
  83. package/src/utils/testUtils.mjs +18 -0
  84. package/src/utils/typescript.mjs +23 -42
  85. package/vale/.vale.ini +1 -0
  86. package/vale/styles/MUI/CorrectReferenceAllCases.yml +42 -0
  87. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  88. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  89. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  90. package/vale/styles/MUI/NoBritish.yml +112 -0
  91. package/vale/styles/MUI/NoCompanyName.yml +17 -0
  92. package/build/markdownlint/duplicate-h1.d.mts +0 -56
  93. package/build/markdownlint/git-diff.d.mts +0 -11
  94. package/build/markdownlint/index.d.mts +0 -52
  95. package/build/markdownlint/straight-quotes.d.mts +0 -11
  96. package/build/markdownlint/table-alignment.d.mts +0 -11
  97. package/build/markdownlint/terminal-language.d.mts +0 -11
  98. package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +0 -20
  99. package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +0 -22
  100. package/src/brokenLinksChecker/__fixtures__/static-site/example.md +0 -20
  101. package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +0 -21
  102. package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +0 -17
  103. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +0 -28
  104. package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +0 -5
  105. package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +0 -21
  106. package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +0 -20
  107. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +0 -20
  108. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +0 -24
  109. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +0 -28
  110. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +0 -19
  111. package/src/brokenLinksChecker/__fixtures__/static-site/unclosed-tags.html +0 -1
  112. package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +0 -20
  113. package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +0 -31
  114. package/src/brokenLinksChecker/index.test.ts +0 -261
  115. package/src/changelog/categorizeCommits.test.ts +0 -319
  116. package/src/changelog/filterCommits.test.ts +0 -363
  117. package/src/changelog/parseCommitLabels.test.ts +0 -509
  118. package/src/changelog/renderChangelog.test.ts +0 -378
  119. package/src/changelog/sortSections.test.ts +0 -199
  120. package/src/eslint/mui/rules/add-undef-to-optional.test.mjs +0 -361
  121. package/src/eslint/mui/rules/consistent-production-guard.test.mjs +0 -162
  122. package/src/eslint/mui/rules/disallow-active-elements-as-key-event-target.test.mjs +0 -66
  123. package/src/eslint/mui/rules/disallow-react-api-in-server-components.test.mjs +0 -305
  124. package/src/eslint/mui/rules/docgen-ignore-before-comment.test.mjs +0 -52
  125. package/src/eslint/mui/rules/flatten-parentheses.test.mjs +0 -245
  126. package/src/eslint/mui/rules/mui-name-matches-component-name.test.mjs +0 -247
  127. package/src/eslint/mui/rules/no-empty-box.test.mjs +0 -40
  128. package/src/eslint/mui/rules/no-styled-box.test.mjs +0 -73
  129. package/src/eslint/mui/rules/require-dev-wrapper.test.mjs +0 -265
  130. package/src/eslint/mui/rules/rules-of-use-theme-variants.test.mjs +0 -149
  131. package/src/eslint/mui/rules/straight-quotes.test.mjs +0 -67
  132. package/src/markdownlint/duplicate-h1.mjs +0 -69
  133. package/src/markdownlint/git-diff.mjs +0 -31
  134. package/src/markdownlint/index.mjs +0 -62
  135. package/src/markdownlint/straight-quotes.mjs +0 -26
  136. package/src/markdownlint/table-alignment.mjs +0 -42
  137. package/src/markdownlint/terminal-language.mjs +0 -19
  138. package/src/utils/build.test.mjs +0 -705
  139. package/src/utils/template.test.mjs +0 -133
  140. package/src/utils/typescript.test.mjs +0 -380
@@ -1,4 +1,5 @@
1
1
  import { defineConfig } from 'eslint/config';
2
+ import { EXTENSION_DTS } from '../extensions.mjs';
2
3
 
3
4
  const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
4
5
 
@@ -414,10 +415,12 @@ export function createCoreConfig(options = {}) {
414
415
  'mui/material-ui-no-styled-box': 'error',
415
416
  }
416
417
  : {}),
418
+ 'mui/no-guarded-throw': 'error',
417
419
  'mui/straight-quotes': 'off',
418
420
  'mui/consistent-production-guard': 'error',
419
421
  'mui/add-undef-to-optional': 'off',
420
422
  'mui/flatten-parentheses': 'warn',
423
+ 'mui/no-presentation-role': 'off',
421
424
 
422
425
  'react-hooks/exhaustive-deps': [
423
426
  'error',
@@ -460,9 +463,9 @@ export function createCoreConfig(options = {}) {
460
463
  'react/state-in-constructor': 'off',
461
464
  // stylistic opinion. For conditional assignment we want it outside, otherwise as static
462
465
  'react/static-property-placement': 'off',
463
- // noopener is enough
464
- // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-target-blank.md#rule-options
465
- 'react/jsx-no-target-blank': ['error', { allowReferrer: true }],
466
+ // This rule is outdated, rel are no longer needed for security on target="_blank" links.
467
+ // See https://github.com/mui/material-ui/pull/40447 for more details.
468
+ 'react/jsx-no-target-blank': 'off',
466
469
 
467
470
  'no-restricted-syntax': [
468
471
  'error',
@@ -499,6 +502,15 @@ export function createCoreConfig(options = {}) {
499
502
  message: 'Do not call `Error(...)` without `new`. Use `new Error(...)` instead.',
500
503
  selector: "CallExpression[callee.name='Error']",
501
504
  },
505
+ {
506
+ // xmlns="http://www.w3.org/2000/svg" is only needed on standalone .svg files so the
507
+ // browser treats them as SVG instead of generic XML. Inside HTML the <svg> element is
508
+ // already recognised by the browser, so the attribute is dead weight.
509
+ // https://github.com/mui/mui-public/pull/1321
510
+ message:
511
+ 'Remove xmlns from inline <svg>. The attribute is redundant in HTML and adds unnecessary bytes.',
512
+ selector: 'JSXOpeningElement[name.name="svg"] > JSXAttribute[name.name="xmlns"]',
513
+ },
502
514
  ...restrictedSyntaxRules,
503
515
  ],
504
516
 
@@ -515,9 +527,10 @@ export function createCoreConfig(options = {}) {
515
527
  '@typescript-eslint/consistent-type-imports': [
516
528
  'error',
517
529
  {
518
- fixStyle: 'inline-type-imports',
530
+ fixStyle: 'separate-type-imports',
519
531
  },
520
532
  ],
533
+ 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
521
534
  }
522
535
  : {}),
523
536
  // Prevent the use of `e` as a shorthand for `event`, `error`, etc.
@@ -525,5 +538,12 @@ export function createCoreConfig(options = {}) {
525
538
  '@typescript-eslint/return-await': 'off',
526
539
  },
527
540
  },
541
+ {
542
+ name: 'mui-base/dts',
543
+ files: [`**/*${EXTENSION_DTS}`],
544
+ rules: {
545
+ '@typescript-eslint/consistent-type-imports': 'off',
546
+ },
547
+ },
528
548
  ]);
529
549
  }
@@ -4,6 +4,7 @@ import disallowReactApiInServerComponents from './rules/disallow-react-api-in-se
4
4
  import docgenIgnoreBeforeComment from './rules/docgen-ignore-before-comment.mjs';
5
5
  import muiNameMatchesComponentName from './rules/mui-name-matches-component-name.mjs';
6
6
  import noEmptyBox from './rules/no-empty-box.mjs';
7
+ import noGuardedThrow from './rules/no-guarded-throw.mjs';
7
8
  import noRestrictedResolvedImports from './rules/no-restricted-resolved-imports.mjs';
8
9
  import noStyledBox from './rules/no-styled-box.mjs';
9
10
  import requireDevWrapper from './rules/require-dev-wrapper.mjs';
@@ -11,6 +12,8 @@ import rulesOfUseThemeVariants from './rules/rules-of-use-theme-variants.mjs';
11
12
  import straightQuotes from './rules/straight-quotes.mjs';
12
13
  import addUndefToOptional from './rules/add-undef-to-optional.mjs';
13
14
  import flattenParentheses from './rules/flatten-parentheses.mjs';
15
+ import noPresentationRole from './rules/no-presentation-role.mjs';
16
+ import noFloatingCleanup from './rules/no-floating-cleanup.mjs';
14
17
 
15
18
  /** @type {import('eslint').ESLint.Plugin} */
16
19
  const muiPlugin = {
@@ -22,6 +25,7 @@ const muiPlugin = {
22
25
  'consistent-production-guard': consistentProductionGuard,
23
26
  'disallow-active-element-as-key-event-target': disallowActiveElementAsKeyEventTarget,
24
27
  'docgen-ignore-before-comment': docgenIgnoreBeforeComment,
28
+ 'no-guarded-throw': noGuardedThrow,
25
29
  'material-ui-name-matches-component-name': muiNameMatchesComponentName,
26
30
  'material-ui-rules-of-use-theme-variants': rulesOfUseThemeVariants,
27
31
  'material-ui-no-empty-box': noEmptyBox,
@@ -33,6 +37,8 @@ const muiPlugin = {
33
37
  // Some discrepancies between TypeScript and ESLint types - casting to any
34
38
  'add-undef-to-optional': /** @type {any} */ (addUndefToOptional),
35
39
  'flatten-parentheses': /** @type {any} */ (flattenParentheses),
40
+ 'no-presentation-role': noPresentationRole,
41
+ 'no-floating-cleanup': /** @type {any} */ (noFloatingCleanup),
36
42
  },
37
43
  };
38
44
 
@@ -0,0 +1,187 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import ts from 'typescript';
3
+
4
+ /**
5
+ * no-floating-cleanup
6
+ *
7
+ * Like `@typescript-eslint/no-floating-promises`, but for functions: reports
8
+ * call expressions used as statements whose return value is a function
9
+ * (e.g. an `unsubscribe` / cleanup callback). Ignoring such a value usually
10
+ * means the subscription can never be torn down — a leak.
11
+ *
12
+ * Requires type information (parserOptions.project / projectService).
13
+ *
14
+ * Calls whose resolved signature declares a `this` return type are never
15
+ * reported: fluent / builder APIs (e.g. d3 scales) chain by returning `this`,
16
+ * so the discarded result is callable but not a leaked cleanup function.
17
+ *
18
+ * Opt-out at a call site, same convention as no-floating-promises:
19
+ * void subscribe(cb);
20
+ */
21
+
22
+ const RULE_NAME = 'no-floating-cleanup';
23
+
24
+ const createRule = ESLintUtils.RuleCreator(
25
+ (name) =>
26
+ `https://github.com/mui/mui-public/blob/master/packages/code-infra/src/eslint/mui/rules/${name}.mjs`,
27
+ );
28
+
29
+ export default createRule({
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description:
34
+ 'Disallow ignoring returned cleanup/unsubscribe functions, which leaks subscriptions.',
35
+ },
36
+ messages: {
37
+ floatingCleanup:
38
+ "Return value of type '{{typeName}}' is ignored. This likely leaks a subscription — " +
39
+ 'store the cleanup function, or discard it explicitly with the `void` operator.',
40
+ },
41
+ schema: [],
42
+ },
43
+ name: RULE_NAME,
44
+ defaultOptions: [],
45
+ create(context) {
46
+ const services = ESLintUtils.getParserServices(context);
47
+ const checker = services.program.getTypeChecker();
48
+
49
+ /**
50
+ * Does this type (or any union member) look like a cleanup function?
51
+ * @param {import('typescript').Type} type
52
+ * @returns {boolean}
53
+ */
54
+ function isCleanupType(type) {
55
+ if (type.isUnion()) {
56
+ return type.types.some(isCleanupType);
57
+ }
58
+ // Never fire on any/unknown/never — too noisy and not provable.
59
+ // eslint-disable-next-line no-bitwise -- TypeScript type flags are a bitmask
60
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
61
+ return false;
62
+ }
63
+
64
+ return checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0;
65
+ }
66
+
67
+ /**
68
+ * @param {import('typescript').Type} type
69
+ * @returns {string}
70
+ */
71
+ function describeType(type) {
72
+ return (type.aliasSymbol && type.aliasSymbol.getName()) || checker.typeToString(type);
73
+ }
74
+
75
+ /**
76
+ * Does this call resolve to a signature that returns `this`? Fluent /
77
+ * builder APIs (e.g. d3 scales) chain by returning `this`, so the
78
+ * discarded result is callable but never a leaked cleanup function —
79
+ * those calls should not be reported. Covers both an explicit `: this`
80
+ * annotation and an inferred `this` return (no annotation).
81
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} estreeCall
82
+ * @returns {boolean}
83
+ */
84
+ function returnsThis(estreeCall) {
85
+ /** @type {import('typescript').Node | undefined} */
86
+ let tsNode = services.esTreeNodeToTSNodeMap.get(estreeCall);
87
+ // Unwrap `await (...)` / parens down to the underlying call.
88
+ while (tsNode && (ts.isAwaitExpression(tsNode) || ts.isParenthesizedExpression(tsNode))) {
89
+ tsNode = tsNode.expression;
90
+ }
91
+ if (!tsNode || !(ts.isCallExpression(tsNode) || ts.isNewExpression(tsNode))) {
92
+ return false;
93
+ }
94
+ const declaration = checker.getResolvedSignature(tsNode)?.getDeclaration();
95
+ if (!declaration) {
96
+ return false;
97
+ }
98
+ // Explicit `: this` annotation.
99
+ if (declaration.type?.kind === ts.SyntaxKind.ThisType) {
100
+ return true;
101
+ }
102
+ // Inferred `this` return: the *declared* (uninstantiated) signature's
103
+ // return type is the polymorphic `this` type, which prints as `this`.
104
+ // (The resolved signature would have it bound to the receiver type.)
105
+ const declaredSignature = checker.getSignatureFromDeclaration(declaration);
106
+ return (
107
+ !!declaredSignature &&
108
+ checker.typeToString(checker.getReturnTypeOfSignature(declaredSignature)) === 'this'
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Unwrap an expression-statement expression down to the calls whose
114
+ * results are being discarded. Mirrors the cases no-floating-promises
115
+ * handles: plain calls, optional chains, `await`, comma sequences,
116
+ * and the operand(s) of unary / logical / conditional expressions.
117
+ * A top-level `void` operator is the explicit opt-out, so we stop there.
118
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} expr
119
+ * @param {import('@typescript-eslint/utils').TSESTree.Node[]} [out]
120
+ * @returns {import('@typescript-eslint/utils').TSESTree.Node[]}
121
+ */
122
+ function findDiscardedCalls(expr, out = []) {
123
+ switch (expr.type) {
124
+ case 'CallExpression':
125
+ case 'NewExpression':
126
+ out.push(expr);
127
+ break;
128
+ case 'ChainExpression':
129
+ findDiscardedCalls(expr.expression, out);
130
+ break;
131
+ case 'AwaitExpression':
132
+ // type of the AwaitExpression node is already the awaited type
133
+ if (expr.argument.type === 'CallExpression' || expr.argument.type === 'ChainExpression') {
134
+ out.push(expr);
135
+ }
136
+ break;
137
+ case 'UnaryExpression':
138
+ // `void expr` is the explicit opt-out; any other unary operator
139
+ // (e.g. `!subscribe()`) still discards the operand's result.
140
+ if (expr.operator !== 'void') {
141
+ findDiscardedCalls(expr.argument, out);
142
+ }
143
+ break;
144
+ case 'LogicalExpression':
145
+ // `a && subscribe()`, `a || subscribe()`, `a ?? subscribe()`:
146
+ // either side may be the discarded value.
147
+ findDiscardedCalls(expr.left, out);
148
+ findDiscardedCalls(expr.right, out);
149
+ break;
150
+ case 'ConditionalExpression':
151
+ // `cond ? subscribe() : other()`: the branches are discarded,
152
+ // not the test.
153
+ findDiscardedCalls(expr.consequent, out);
154
+ findDiscardedCalls(expr.alternate, out);
155
+ break;
156
+ case 'SequenceExpression':
157
+ // every value except the last is discarded; the last one is the
158
+ // statement's value, which is also discarded here
159
+ expr.expressions.forEach((subExpr) => findDiscardedCalls(subExpr, out));
160
+ break;
161
+ default:
162
+ break;
163
+ }
164
+ return out;
165
+ }
166
+
167
+ return {
168
+ /** @param {import('@typescript-eslint/utils').TSESTree.ExpressionStatement} node */
169
+ ExpressionStatement(node) {
170
+ findDiscardedCalls(node.expression).forEach((call) => {
171
+ // Fluent/builder calls return `this` for chaining — not a leak.
172
+ if (returnsThis(call)) {
173
+ return;
174
+ }
175
+ const type = services.getTypeAtLocation(call);
176
+ if (isCleanupType(type)) {
177
+ context.report({
178
+ node: call,
179
+ messageId: 'floatingCleanup',
180
+ data: { typeName: describeType(type) },
181
+ });
182
+ }
183
+ });
184
+ },
185
+ };
186
+ },
187
+ });
@@ -0,0 +1,115 @@
1
+ import { isProcessEnvNodeEnv } from './nodeEnvUtils.mjs';
2
+
3
+ /**
4
+ * Recursively checks if process.env.NODE_ENV appears anywhere in the node tree
5
+ * @param {import('estree').Node | null | undefined} node
6
+ * @returns {boolean}
7
+ */
8
+ function containsProcessEnvNodeEnv(node) {
9
+ if (!node || typeof node !== 'object') {
10
+ return false;
11
+ }
12
+
13
+ if (isProcessEnvNodeEnv(node)) {
14
+ return true;
15
+ }
16
+
17
+ // Traverse all child nodes, skipping parent references to avoid circular traversal
18
+ for (const key of Object.keys(node)) {
19
+ if (key === 'parent') {
20
+ continue;
21
+ }
22
+ const child = /** @type {unknown} */ (/** @type {any} */ (node)[key]);
23
+ if (Array.isArray(child)) {
24
+ if (child.some(containsProcessEnvNodeEnv)) {
25
+ return true;
26
+ }
27
+ } else if (
28
+ child &&
29
+ typeof child === 'object' &&
30
+ /** @type {import('estree').Node} */ (child).type
31
+ ) {
32
+ if (containsProcessEnvNodeEnv(/** @type {import('estree').Node} */ (child))) {
33
+ return true;
34
+ }
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * ESLint rule that disallows throw statements guarded by process.env.NODE_ENV checks.
42
+ *
43
+ * NODE_ENV guards cause throw statements to only execute in certain environments,
44
+ * leading to inconsistent control flow between development and production. Whether
45
+ * the guard excludes production (tree-shaking the throw away) or targets production
46
+ * specifically, the result is environment-dependent behavior that should be avoided.
47
+ *
48
+ * The rule stops at function boundaries, so throws inside functions defined within
49
+ * a NODE_ENV guard are not flagged, as the function may be called from other contexts.
50
+ *
51
+ * @example
52
+ * // Invalid - throw only in development, removed in production
53
+ * if (process.env.NODE_ENV !== 'production') {
54
+ * throw new Error('Missing required prop');
55
+ * }
56
+ *
57
+ * @example
58
+ * // Invalid - throw only in production
59
+ * if (process.env.NODE_ENV === 'production') {
60
+ * throw new Error('Production-only error');
61
+ * }
62
+ *
63
+ * @example
64
+ * // Valid - unconditional throw
65
+ * throw new Error('Something went wrong');
66
+ *
67
+ * @type {import('eslint').Rule.RuleModule}
68
+ */
69
+ const rule = {
70
+ meta: {
71
+ type: 'problem',
72
+ docs: {
73
+ description:
74
+ 'Disallow throw statements guarded by process.env.NODE_ENV checks, as they cause environment-dependent control flow',
75
+ },
76
+ messages: {
77
+ guardedThrow:
78
+ 'Do not guard `throw` statements with `process.env.NODE_ENV` checks. Guarded throws execute only in certain environments, causing inconsistent control flow between development and production.',
79
+ },
80
+ schema: [],
81
+ },
82
+ create(context) {
83
+ return {
84
+ ThrowStatement(node) {
85
+ /** @type {import('eslint').Rule.Node | null} */
86
+ let current = node.parent;
87
+ /** @type {import('eslint').Rule.Node} */
88
+ let currentChild = node;
89
+
90
+ while (current) {
91
+ if (
92
+ current.type === 'FunctionDeclaration' ||
93
+ current.type === 'FunctionExpression' ||
94
+ current.type === 'ArrowFunctionExpression'
95
+ ) {
96
+ break;
97
+ }
98
+ if (current.type === 'IfStatement') {
99
+ const isInConsequent = current.consequent === currentChild;
100
+ const isInAlternate = current.alternate === currentChild;
101
+
102
+ if ((isInConsequent || isInAlternate) && containsProcessEnvNodeEnv(current.test)) {
103
+ context.report({ node, messageId: 'guardedThrow' });
104
+ return;
105
+ }
106
+ }
107
+ currentChild = current;
108
+ current = current.parent;
109
+ }
110
+ },
111
+ };
112
+ },
113
+ };
114
+
115
+ export default rule;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ const rule = {
5
+ meta: {
6
+ docs: {
7
+ description:
8
+ 'Disallow role="presentation" in favor of role="none". Both are equivalent, but role="none" is clearer and shorter.',
9
+ },
10
+ messages: {
11
+ noPresentation:
12
+ 'Use role="none" instead of role="presentation". They are equivalent, but role="none" is preferred.',
13
+ },
14
+ fixable: 'code',
15
+ type: 'suggestion',
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ return {
20
+ /** @param {import('estree-jsx').JSXAttribute} node */
21
+ JSXAttribute(node) {
22
+ if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'role') {
23
+ return;
24
+ }
25
+
26
+ const { value } = node;
27
+
28
+ // role="presentation"
29
+ if (value !== null && value.type === 'Literal' && value.value === 'presentation') {
30
+ context.report({
31
+ node,
32
+ messageId: 'noPresentation',
33
+ fix(fixer) {
34
+ return fixer.replaceText(value, '"none"');
35
+ },
36
+ });
37
+ return;
38
+ }
39
+
40
+ // role={'presentation'}
41
+ if (
42
+ value !== null &&
43
+ value.type === 'JSXExpressionContainer' &&
44
+ value.expression.type === 'Literal' &&
45
+ value.expression.value === 'presentation'
46
+ ) {
47
+ context.report({
48
+ node,
49
+ messageId: 'noPresentation',
50
+ fix(fixer) {
51
+ return fixer.replaceText(value, '"none"');
52
+ },
53
+ });
54
+ }
55
+ },
56
+ };
57
+ },
58
+ };
59
+
60
+ export default rule;
@@ -39,3 +39,55 @@ export function isLiteralEq(node, value) {
39
39
  export function isLiteralNeq(node, value) {
40
40
  return node.type === 'Literal' && node.value !== value;
41
41
  }
42
+
43
+ /**
44
+ * Checks if a BinaryExpression compares process.env.NODE_ENV with === or !==
45
+ * @param {import('estree').Node} node
46
+ * @returns {boolean}
47
+ */
48
+ export function isNodeEnvBinaryComparison(node) {
49
+ return (
50
+ node.type === 'BinaryExpression' &&
51
+ (node.operator === '===' || node.operator === '!==') &&
52
+ (isProcessEnvNodeEnv(node.left) || isProcessEnvNodeEnv(node.right))
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Walks up the parent chain and checks if the node is inside an IfStatement
58
+ * whose test is a NODE_ENV binary comparison.
59
+ * If a callback is provided, it is called with the IfStatement and the direct
60
+ * child that leads to the node. The function returns true only when the callback
61
+ * returns true. Without a callback the function returns true when the node is
62
+ * inside any branch (consequent or alternate) of such an IfStatement.
63
+ * @param {import('eslint').Rule.Node} node
64
+ * @param {(ifStatement: import('estree').IfStatement & import('eslint').Rule.NodeParentExtension, child: import('eslint').Rule.Node) => boolean} [callback]
65
+ * @returns {boolean}
66
+ */
67
+ export function isInsideNodeEnvCheck(node, callback) {
68
+ /** @type {import('eslint').Rule.Node | null} */
69
+ let current = node.parent;
70
+ /** @type {import('eslint').Rule.Node} */
71
+ let currentChild = node;
72
+
73
+ while (current) {
74
+ if (current.type === 'IfStatement' && isNodeEnvBinaryComparison(current.test)) {
75
+ if (callback) {
76
+ if (callback(current, currentChild)) {
77
+ return true;
78
+ }
79
+ } else {
80
+ const isInConsequent = current.consequent === currentChild;
81
+ const isInAlternate = current.alternate === currentChild;
82
+ if (isInConsequent || isInAlternate) {
83
+ return true;
84
+ }
85
+ }
86
+ }
87
+
88
+ currentChild = current;
89
+ current = current.parent;
90
+ }
91
+
92
+ return false;
93
+ }
@@ -1,4 +1,9 @@
1
- import { isProcessEnvNodeEnv, isLiteralEq, isLiteralNeq } from './nodeEnvUtils.mjs';
1
+ import {
2
+ isProcessEnvNodeEnv,
3
+ isLiteralEq,
4
+ isLiteralNeq,
5
+ isInsideNodeEnvCheck,
6
+ } from './nodeEnvUtils.mjs';
2
7
 
3
8
  /**
4
9
  * ESLint rule that enforces certain function calls to be wrapped with
@@ -58,18 +63,21 @@ const rule = {
58
63
  const functionNames = options.functionNames || ['warnOnce', 'warn', 'checkSlot'];
59
64
 
60
65
  /**
61
- * Checks if a binary expression is comparing process.env.NODE_ENV appropriately
62
- * @param {import('estree').BinaryExpression} binaryExpression - The binary expression to check
66
+ * Checks if an expression is comparing process.env.NODE_ENV appropriately
67
+ * @param {import('estree').Expression} expression - The expression to check
63
68
  * @param {string} operator - The expected comparison operator (===, !==, etc.)
64
69
  * @param {string} value - The value to compare with
65
70
  * @returns {boolean}
66
71
  */
67
- function isNodeEnvComparison(binaryExpression, operator, value) {
68
- const { left, right } = binaryExpression;
72
+ function isNodeEnvComparison(expression, operator, value) {
73
+ if (expression.type !== 'BinaryExpression') {
74
+ return false;
75
+ }
76
+ const { left, right } = expression;
69
77
 
70
78
  // Check for exact match with the specified value
71
79
  if (
72
- binaryExpression.operator === operator &&
80
+ expression.operator === operator &&
73
81
  ((isProcessEnvNodeEnv(left) && isLiteralEq(right, value)) ||
74
82
  (isProcessEnvNodeEnv(right) && isLiteralEq(left, value)))
75
83
  ) {
@@ -79,7 +87,7 @@ const rule = {
79
87
  // For !== operator also allow === with any literal value that's NOT 'production'
80
88
  if (
81
89
  operator === '!==' &&
82
- binaryExpression.operator === '===' &&
90
+ expression.operator === '===' &&
83
91
  ((isProcessEnvNodeEnv(left) && isLiteralNeq(right, value)) ||
84
92
  (isProcessEnvNodeEnv(right) && isLiteralNeq(left, value)))
85
93
  ) {
@@ -95,41 +103,18 @@ const rule = {
95
103
  * @returns {boolean}
96
104
  */
97
105
  function isWrappedInProductionCheck(node) {
98
- /** @type {import('eslint').Rule.Node | null} */
99
- let current = node.parent;
100
- /** @type {import('eslint').Rule.Node} */
101
- let currentChild = node;
102
-
103
- while (current) {
104
- // Check if we're inside an if statement
105
- if (current.type === 'IfStatement') {
106
- // Determine which branch we're in
107
- const isInConsequent = current.consequent === currentChild;
108
- const isInAlternate = current.alternate === currentChild;
109
-
110
- // Skip if not in a branch
111
- if (isInConsequent || isInAlternate) {
112
- const test = current.test;
106
+ return isInsideNodeEnvCheck(node, (ifStatement, child) => {
107
+ const isInConsequent = ifStatement.consequent === child;
108
+ const isInAlternate = ifStatement.alternate === child;
113
109
 
114
- // If we're in the consequent, we need !==
115
- // If we're in the alternate (else), we need ===
116
- const operator = isInConsequent ? '!==' : '===';
117
-
118
- // Check for the specific pattern with the right operator
119
- if (
120
- test.type === 'BinaryExpression' &&
121
- isNodeEnvComparison(test, operator, 'production')
122
- ) {
123
- return true;
124
- }
125
- }
110
+ if (isInConsequent || isInAlternate) {
111
+ // If we're in the consequent, we need !==
112
+ // If we're in the alternate (else), we need ===
113
+ const operator = isInConsequent ? '!==' : '===';
114
+ return isNodeEnvComparison(ifStatement.test, operator, 'production');
126
115
  }
127
-
128
- currentChild = current;
129
- current = current.parent;
130
- }
131
-
132
- return false;
116
+ return false;
117
+ });
133
118
  }
134
119
 
135
120
  return {
@@ -1,4 +1,4 @@
1
- import { BaseExpression, Expression } from 'estree';
1
+ import type { BaseExpression, Expression } from 'estree';
2
2
 
3
3
  export * from 'estree';
4
4