@mui/internal-code-infra 0.0.4-canary.4 → 0.0.4-canary.41

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 (101) hide show
  1. package/README.md +19 -8
  2. package/build/babel-config.d.mts +11 -3
  3. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  4. package/build/brokenLinksChecker/index.d.mts +45 -2
  5. package/build/changelog/types.d.ts +1 -1
  6. package/build/cli/cmdArgosPush.d.mts +2 -2
  7. package/build/cli/cmdBuild.d.mts +2 -2
  8. package/build/cli/cmdCopyFiles.d.mts +2 -2
  9. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  10. package/build/cli/cmdGenerateChangelog.d.mts +2 -2
  11. package/build/cli/cmdGithubAuth.d.mts +2 -2
  12. package/build/cli/cmdListWorkspaces.d.mts +4 -2
  13. package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
  14. package/build/cli/cmdPublish.d.mts +4 -2
  15. package/build/cli/cmdPublishCanary.d.mts +3 -2
  16. package/build/cli/cmdPublishNewPackage.d.mts +4 -2
  17. package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
  18. package/build/cli/cmdVale.d.mts +46 -0
  19. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  20. package/build/eslint/baseConfig.d.mts +3 -1
  21. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  22. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  23. package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
  24. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
  25. package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
  26. package/build/markdownlint/duplicate-h1.d.mts +1 -1
  27. package/build/markdownlint/git-diff.d.mts +1 -1
  28. package/build/markdownlint/index.d.mts +1 -1
  29. package/build/markdownlint/straight-quotes.d.mts +1 -1
  30. package/build/markdownlint/table-alignment.d.mts +1 -1
  31. package/build/markdownlint/terminal-language.d.mts +1 -1
  32. package/build/remark/config.d.mts +43 -0
  33. package/build/remark/createLintTester.d.mts +10 -0
  34. package/build/remark/firstBlockHeading.d.mts +4 -0
  35. package/build/remark/gitDiff.d.mts +2 -0
  36. package/build/remark/noSpaceInLinks.d.mts +2 -0
  37. package/build/remark/straightQuotes.d.mts +2 -0
  38. package/build/remark/tableAlignment.d.mts +2 -0
  39. package/build/remark/terminalLanguage.d.mts +2 -0
  40. package/build/utils/build.d.mts +3 -3
  41. package/build/utils/github.d.mts +1 -1
  42. package/build/utils/pnpm.d.mts +68 -2
  43. package/build/utils/testUtils.d.mts +7 -0
  44. package/package.json +59 -32
  45. package/src/babel-config.mjs +9 -3
  46. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
  47. package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
  48. package/src/brokenLinksChecker/crawlWorker.mjs +212 -0
  49. package/src/brokenLinksChecker/index.mjs +215 -164
  50. package/src/brokenLinksChecker/index.test.ts +43 -13
  51. package/src/changelog/categorizeCommits.test.ts +5 -5
  52. package/src/changelog/fetchChangelogs.mjs +6 -2
  53. package/src/changelog/parseCommitLabels.test.ts +5 -5
  54. package/src/changelog/renderChangelog.mjs +1 -1
  55. package/src/changelog/types.ts +1 -1
  56. package/src/cli/cmdListWorkspaces.mjs +9 -2
  57. package/src/cli/cmdNetlifyIgnore.mjs +4 -88
  58. package/src/cli/cmdPublish.mjs +51 -14
  59. package/src/cli/cmdPublishCanary.mjs +139 -107
  60. package/src/cli/cmdPublishNewPackage.mjs +27 -6
  61. package/src/cli/cmdVale.mjs +513 -0
  62. package/src/cli/cmdVale.test.mjs +644 -0
  63. package/src/cli/index.mjs +2 -0
  64. package/src/eslint/baseConfig.mjs +45 -20
  65. package/src/eslint/docsConfig.mjs +2 -1
  66. package/src/eslint/jsonConfig.mjs +2 -1
  67. package/src/eslint/mui/config.mjs +20 -1
  68. package/src/eslint/mui/index.mjs +2 -0
  69. package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
  70. package/src/eslint/mui/rules/no-guarded-throw.test.mjs +206 -0
  71. package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
  72. package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
  73. package/src/eslint/testConfig.mjs +2 -1
  74. package/src/estree-typescript.d.ts +1 -1
  75. package/src/remark/config.mjs +157 -0
  76. package/src/remark/createLintTester.mjs +19 -0
  77. package/src/remark/firstBlockHeading.mjs +87 -0
  78. package/src/remark/firstBlockHeading.test.mjs +107 -0
  79. package/src/remark/gitDiff.mjs +43 -0
  80. package/src/remark/gitDiff.test.mjs +45 -0
  81. package/src/remark/noSpaceInLinks.mjs +42 -0
  82. package/src/remark/noSpaceInLinks.test.mjs +22 -0
  83. package/src/remark/straightQuotes.mjs +31 -0
  84. package/src/remark/straightQuotes.test.mjs +25 -0
  85. package/src/remark/tableAlignment.mjs +23 -0
  86. package/src/remark/tableAlignment.test.mjs +28 -0
  87. package/src/remark/terminalLanguage.mjs +19 -0
  88. package/src/remark/terminalLanguage.test.mjs +17 -0
  89. package/src/untyped-plugins.d.ts +11 -11
  90. package/src/utils/build.test.mjs +546 -575
  91. package/src/utils/pnpm.mjs +192 -3
  92. package/src/utils/pnpm.test.mjs +580 -0
  93. package/src/utils/testUtils.mjs +18 -0
  94. package/src/utils/typescript.test.mjs +249 -272
  95. package/vale/.vale.ini +1 -0
  96. package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
  97. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  98. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  99. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  100. package/vale/styles/MUI/NoBritish.yml +112 -0
  101. package/vale/styles/MUI/NoCompanyName.yml +17 -0
@@ -1,11 +1,13 @@
1
1
  import { includeIgnoreFile, fixupConfigRules } from '@eslint/compat';
2
2
  import eslintJs from '@eslint/js';
3
- import { defineConfig } from 'eslint/config';
3
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
4
+ import { defineConfig } from '@eslint/config-helpers';
4
5
  import prettier from 'eslint-config-prettier/flat';
5
6
  import compatPlugin from 'eslint-plugin-compat';
6
7
  import importPlugin from 'eslint-plugin-import';
7
8
  import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
8
9
  import reactPlugin from 'eslint-plugin-react';
10
+ import * as mdx from 'eslint-plugin-mdx';
9
11
  import { configs as reactCompilerPluginConfigs } from 'eslint-plugin-react-compiler';
10
12
  import reactHooks from 'eslint-plugin-react-hooks';
11
13
  import globals from 'globals';
@@ -33,6 +35,7 @@ function includeIgnoreIfExists(filePath, description) {
33
35
  * @param {boolean} [params.enableReactCompiler] - Whether to enable React Compiler.
34
36
  * @param {boolean} [params.consistentTypeImports] - Whether to enforce consistent type imports.
35
37
  * @param {boolean} [params.materialUi] - Whether to enable Material UI specific rules (mui/material-ui-*).
38
+ * @param {boolean} [params.markdown] - Whether to enable markdown/MDX linting via `eslint-plugin-mdx`. Opt-in so dependents can adopt on their own schedule.
36
39
  * @param {string} [params.baseDirectory] - The base directory for the configuration.
37
40
  * @returns {import('eslint').Linter.Config[]}
38
41
  */
@@ -40,13 +43,53 @@ export function createBaseConfig({
40
43
  enableReactCompiler = false,
41
44
  consistentTypeImports = false,
42
45
  materialUi = false,
46
+ markdown = false,
43
47
  baseDirectory = process.cwd(),
44
48
  } = {}) {
45
49
  return defineConfig([
50
+ {
51
+ name: 'settings',
52
+ languageOptions: {
53
+ ecmaVersion: 7,
54
+ globals: {
55
+ ...globals.es2020,
56
+ ...globals.browser,
57
+ ...globals.node,
58
+ },
59
+ },
60
+ plugins: {
61
+ mui: muiPlugin,
62
+ },
63
+ settings: {
64
+ react: {
65
+ version: 'detect',
66
+ },
67
+ browserslistOpts: {
68
+ config: path.join(baseDirectory, '.browserslistrc'),
69
+ env: 'stable',
70
+ ignoreUnknownVersions: true,
71
+ },
72
+ },
73
+ },
46
74
  includeIgnoreIfExists(path.join(baseDirectory, '.gitignore'), `Ignore rules from .gitignore`),
47
75
  includeIgnoreIfExists(path.join(baseDirectory, '.lintignore'), `Ignore rules from .lintignore`),
48
76
  createJsonConfig(),
49
77
  prettier,
78
+ // Markdown + MDX linting via eslint-plugin-mdx. Severities for markdown
79
+ // quality checks live in the project's `.remarkrc` (see
80
+ // `@mui/internal-code-infra/remark`), not here.
81
+ markdown
82
+ ? [
83
+ {
84
+ ...mdx.flat,
85
+ rules: {
86
+ ...mdx.flat.rules,
87
+ 'mdx/remark': 'error',
88
+ },
89
+ },
90
+ mdx.flatCodeBlocks,
91
+ ]
92
+ : [],
50
93
  {
51
94
  name: 'Base config',
52
95
  files: [`**/*${EXTENSION_TS}`],
@@ -62,25 +105,7 @@ export function createBaseConfig({
62
105
  enableReactCompiler ? reactCompilerPluginConfigs.recommended : {},
63
106
  compatPlugin.configs['flat/recommended'],
64
107
  {
65
- name: 'typescript-eslint-parser',
66
- languageOptions: {
67
- ecmaVersion: 7,
68
- globals: {
69
- ...globals.es2020,
70
- ...globals.browser,
71
- ...globals.node,
72
- },
73
- },
74
- plugins: {
75
- mui: muiPlugin,
76
- },
77
- settings: {
78
- browserslistOpts: {
79
- config: path.join(baseDirectory, '.browserslistrc'),
80
- env: 'stable',
81
- ignoreUnknownVersions: true,
82
- },
83
- },
108
+ name: 'core',
84
109
  extends: createCoreConfig({ enableReactCompiler, consistentTypeImports, materialUi }),
85
110
  },
86
111
  // Lint rule to disallow usage of typescript namespaces.We've seen at least two problems with them:
@@ -1,5 +1,6 @@
1
1
  import nextjs from '@next/eslint-plugin-next';
2
- import { defineConfig } from 'eslint/config';
2
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
3
+ import { defineConfig } from '@eslint/config-helpers';
3
4
 
4
5
  /**
5
6
  * @returns {import('eslint').Linter.Config[]}
@@ -1,4 +1,5 @@
1
- import { defineConfig } from 'eslint/config';
1
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
2
+ import { defineConfig } from '@eslint/config-helpers';
2
3
  import json from '@eslint/json';
3
4
 
4
5
  /**
@@ -1,4 +1,6 @@
1
- import { defineConfig } from 'eslint/config';
1
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
2
+ import { defineConfig } from '@eslint/config-helpers';
3
+ import { EXTENSION_DTS } from '../extensions.mjs';
2
4
 
3
5
  const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
4
6
 
@@ -414,6 +416,7 @@ export function createCoreConfig(options = {}) {
414
416
  'mui/material-ui-no-styled-box': 'error',
415
417
  }
416
418
  : {}),
419
+ 'mui/no-guarded-throw': 'error',
417
420
  'mui/straight-quotes': 'off',
418
421
  'mui/consistent-production-guard': 'error',
419
422
  'mui/add-undef-to-optional': 'off',
@@ -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
 
@@ -525,5 +537,12 @@ export function createCoreConfig(options = {}) {
525
537
  '@typescript-eslint/return-await': 'off',
526
538
  },
527
539
  },
540
+ {
541
+ name: 'mui-base/dts',
542
+ files: [`**/*${EXTENSION_DTS}`],
543
+ rules: {
544
+ '@typescript-eslint/consistent-type-imports': 'off',
545
+ },
546
+ },
528
547
  ]);
529
548
  }
@@ -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';
@@ -22,6 +23,7 @@ const muiPlugin = {
22
23
  'consistent-production-guard': consistentProductionGuard,
23
24
  'disallow-active-element-as-key-event-target': disallowActiveElementAsKeyEventTarget,
24
25
  'docgen-ignore-before-comment': docgenIgnoreBeforeComment,
26
+ 'no-guarded-throw': noGuardedThrow,
25
27
  'material-ui-name-matches-component-name': muiNameMatchesComponentName,
26
28
  'material-ui-rules-of-use-theme-variants': rulesOfUseThemeVariants,
27
29
  'material-ui-no-empty-box': noEmptyBox,
@@ -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,206 @@
1
+ import eslint from 'eslint';
2
+ import parser from '@typescript-eslint/parser';
3
+ import rule from './no-guarded-throw.mjs';
4
+
5
+ const ruleTester = new eslint.RuleTester({
6
+ languageOptions: {
7
+ parser,
8
+ },
9
+ });
10
+
11
+ ruleTester.run('no-guarded-throw', rule, {
12
+ valid: [
13
+ // Should pass: Unconditional throw
14
+ {
15
+ code: `
16
+ throw new Error('Something went wrong');
17
+ `,
18
+ },
19
+ // Should pass: Throw inside a non-NODE_ENV conditional
20
+ {
21
+ code: `
22
+ if (value == null) {
23
+ throw new TypeError('value is required');
24
+ }
25
+ `,
26
+ },
27
+ // Should pass: Throw inside a catch block (no NODE_ENV guard)
28
+ {
29
+ code: `
30
+ try {
31
+ doSomething();
32
+ } catch (error) {
33
+ throw new Error('Failed');
34
+ }
35
+ `,
36
+ },
37
+ // Should pass: Throw inside arrow function inside NODE_ENV guard
38
+ {
39
+ code: `
40
+ if (process.env.NODE_ENV !== 'production') {
41
+ const fn = () => {
42
+ throw new Error('inside arrow function');
43
+ };
44
+ }
45
+ `,
46
+ },
47
+ // Should pass: Throw inside function expression inside NODE_ENV guard
48
+ {
49
+ code: `
50
+ if (process.env.NODE_ENV !== 'production') {
51
+ const fn = function () {
52
+ throw new Error('inside function expression');
53
+ };
54
+ }
55
+ `,
56
+ },
57
+ // Should pass: Throw inside function declaration inside NODE_ENV guard
58
+ {
59
+ code: `
60
+ if (process.env.NODE_ENV !== 'production') {
61
+ function validate() {
62
+ throw new Error('inside function declaration');
63
+ }
64
+ }
65
+ `,
66
+ },
67
+ ],
68
+ invalid: [
69
+ // Should fail: Throw inside !== 'production' guard
70
+ {
71
+ code: `
72
+ if (process.env.NODE_ENV !== 'production') {
73
+ throw new Error('Dev-only error');
74
+ }
75
+ `,
76
+ errors: [{ messageId: 'guardedThrow' }],
77
+ },
78
+ // Should fail: Throw inside === 'production' guard
79
+ {
80
+ code: `
81
+ if (process.env.NODE_ENV === 'production') {
82
+ throw new Error('Prod-only error');
83
+ }
84
+ `,
85
+ errors: [{ messageId: 'guardedThrow' }],
86
+ },
87
+ // Should fail: Throw in else block of === 'production' check
88
+ {
89
+ code: `
90
+ if (process.env.NODE_ENV === 'production') {
91
+ // production path
92
+ } else {
93
+ throw new Error('Non-production error');
94
+ }
95
+ `,
96
+ errors: [{ messageId: 'guardedThrow' }],
97
+ },
98
+ // Should fail: Throw nested inside NODE_ENV guard
99
+ {
100
+ code: `
101
+ if (process.env.NODE_ENV !== 'production') {
102
+ if (value == null) {
103
+ throw new TypeError('value is required');
104
+ }
105
+ }
106
+ `,
107
+ errors: [{ messageId: 'guardedThrow' }],
108
+ },
109
+ // Should fail: Reversed comparison (literal on left)
110
+ {
111
+ code: `
112
+ if ('production' !== process.env.NODE_ENV) {
113
+ throw new Error('Dev-only error');
114
+ }
115
+ `,
116
+ errors: [{ messageId: 'guardedThrow' }],
117
+ },
118
+ // Should fail: Throw in loop inside NODE_ENV guard
119
+ {
120
+ code: `
121
+ if (process.env.NODE_ENV !== 'production') {
122
+ for (const item of items) {
123
+ throw new Error('Invalid item');
124
+ }
125
+ }
126
+ `,
127
+ errors: [{ messageId: 'guardedThrow' }],
128
+ },
129
+ // Should fail: NODE_ENV combined with other conditions using &&
130
+ {
131
+ code: `
132
+ if (process.env.NODE_ENV !== 'production' && value == null) {
133
+ throw new TypeError('value is required');
134
+ }
135
+ `,
136
+ errors: [{ messageId: 'guardedThrow' }],
137
+ },
138
+ // Should fail: NODE_ENV combined with other conditions using ||
139
+ {
140
+ code: `
141
+ if (condition || process.env.NODE_ENV === 'test') {
142
+ throw new Error('Test or condition error');
143
+ }
144
+ `,
145
+ errors: [{ messageId: 'guardedThrow' }],
146
+ },
147
+ // Should fail: Unary not on process.env.NODE_ENV
148
+ {
149
+ code: `
150
+ if (!process.env.NODE_ENV) {
151
+ throw new Error('NODE_ENV not set');
152
+ }
153
+ `,
154
+ errors: [{ messageId: 'guardedThrow' }],
155
+ },
156
+ // Should fail: NODE_ENV passed to a function
157
+ {
158
+ code: `
159
+ if (fn(process.env.NODE_ENV)) {
160
+ throw new Error('Function check failed');
161
+ }
162
+ `,
163
+ errors: [{ messageId: 'guardedThrow' }],
164
+ },
165
+ // Should fail: Throw inside try/catch inside NODE_ENV guard
166
+ {
167
+ code: `
168
+ if (process.env.NODE_ENV !== 'production') {
169
+ try {
170
+ doSomething();
171
+ } catch (error) {
172
+ throw new Error('caught inside guard');
173
+ }
174
+ }
175
+ `,
176
+ errors: [{ messageId: 'guardedThrow' }],
177
+ },
178
+ // Should fail: Throw deeply nested in control flow inside NODE_ENV guard
179
+ {
180
+ code: `
181
+ if (process.env.NODE_ENV !== 'production') {
182
+ if (value == null) {
183
+ for (const item of items) {
184
+ if (!item.valid) {
185
+ throw new Error('invalid item');
186
+ }
187
+ }
188
+ }
189
+ }
190
+ `,
191
+ errors: [{ messageId: 'guardedThrow' }],
192
+ },
193
+ // Should fail: Throw inside switch/case inside NODE_ENV guard
194
+ {
195
+ code: `
196
+ if (process.env.NODE_ENV !== 'production') {
197
+ switch (type) {
198
+ case 'a':
199
+ throw new Error('invalid type a');
200
+ }
201
+ }
202
+ `,
203
+ errors: [{ messageId: 'guardedThrow' }],
204
+ },
205
+ ],
206
+ });
@@ -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,7 +1,8 @@
1
1
  import mochaPlugin from 'eslint-plugin-mocha';
2
2
  import vitestPlugin from '@vitest/eslint-plugin';
3
3
  import testingLibrary from 'eslint-plugin-testing-library';
4
- import { defineConfig } from 'eslint/config';
4
+ // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
5
+ import { defineConfig } from '@eslint/config-helpers';
5
6
  import globals from 'globals';
6
7
  import * as tseslint from 'typescript-eslint';
7
8
  import { EXTENSION_TS } from './extensions.mjs';
@@ -1,4 +1,4 @@
1
- import { BaseExpression, Expression } from 'estree';
1
+ import { type BaseExpression, type Expression } from 'estree';
2
2
 
3
3
  export * from 'estree';
4
4