@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.
- package/README.md +19 -8
- package/build/babel-config.d.mts +11 -3
- package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
- package/build/brokenLinksChecker/index.d.mts +45 -2
- package/build/changelog/types.d.ts +1 -1
- package/build/cli/cmdArgosPush.d.mts +2 -2
- package/build/cli/cmdBuild.d.mts +2 -2
- package/build/cli/cmdCopyFiles.d.mts +2 -2
- package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
- package/build/cli/cmdGenerateChangelog.d.mts +2 -2
- package/build/cli/cmdGithubAuth.d.mts +2 -2
- package/build/cli/cmdListWorkspaces.d.mts +4 -2
- package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
- package/build/cli/cmdPublish.d.mts +4 -2
- package/build/cli/cmdPublishCanary.d.mts +3 -2
- package/build/cli/cmdPublishNewPackage.d.mts +4 -2
- package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
- package/build/cli/cmdVale.d.mts +46 -0
- package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
- package/build/eslint/baseConfig.d.mts +3 -1
- package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
- package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
- package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
- package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
- package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
- package/build/markdownlint/duplicate-h1.d.mts +1 -1
- package/build/markdownlint/git-diff.d.mts +1 -1
- package/build/markdownlint/index.d.mts +1 -1
- package/build/markdownlint/straight-quotes.d.mts +1 -1
- package/build/markdownlint/table-alignment.d.mts +1 -1
- package/build/markdownlint/terminal-language.d.mts +1 -1
- package/build/remark/config.d.mts +43 -0
- package/build/remark/createLintTester.d.mts +10 -0
- package/build/remark/firstBlockHeading.d.mts +4 -0
- package/build/remark/gitDiff.d.mts +2 -0
- package/build/remark/noSpaceInLinks.d.mts +2 -0
- package/build/remark/straightQuotes.d.mts +2 -0
- package/build/remark/tableAlignment.d.mts +2 -0
- package/build/remark/terminalLanguage.d.mts +2 -0
- package/build/utils/build.d.mts +3 -3
- package/build/utils/github.d.mts +1 -1
- package/build/utils/pnpm.d.mts +68 -2
- package/build/utils/testUtils.d.mts +7 -0
- package/package.json +59 -32
- package/src/babel-config.mjs +9 -3
- package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
- package/src/brokenLinksChecker/crawlWorker.mjs +212 -0
- package/src/brokenLinksChecker/index.mjs +215 -164
- package/src/brokenLinksChecker/index.test.ts +43 -13
- package/src/changelog/categorizeCommits.test.ts +5 -5
- package/src/changelog/fetchChangelogs.mjs +6 -2
- package/src/changelog/parseCommitLabels.test.ts +5 -5
- package/src/changelog/renderChangelog.mjs +1 -1
- package/src/changelog/types.ts +1 -1
- package/src/cli/cmdListWorkspaces.mjs +9 -2
- package/src/cli/cmdNetlifyIgnore.mjs +4 -88
- package/src/cli/cmdPublish.mjs +51 -14
- package/src/cli/cmdPublishCanary.mjs +139 -107
- package/src/cli/cmdPublishNewPackage.mjs +27 -6
- package/src/cli/cmdVale.mjs +513 -0
- package/src/cli/cmdVale.test.mjs +644 -0
- package/src/cli/index.mjs +2 -0
- package/src/eslint/baseConfig.mjs +45 -20
- package/src/eslint/docsConfig.mjs +2 -1
- package/src/eslint/jsonConfig.mjs +2 -1
- package/src/eslint/mui/config.mjs +20 -1
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
- package/src/eslint/mui/rules/no-guarded-throw.test.mjs +206 -0
- package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
- package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
- package/src/eslint/testConfig.mjs +2 -1
- package/src/estree-typescript.d.ts +1 -1
- package/src/remark/config.mjs +157 -0
- package/src/remark/createLintTester.mjs +19 -0
- package/src/remark/firstBlockHeading.mjs +87 -0
- package/src/remark/firstBlockHeading.test.mjs +107 -0
- package/src/remark/gitDiff.mjs +43 -0
- package/src/remark/gitDiff.test.mjs +45 -0
- package/src/remark/noSpaceInLinks.mjs +42 -0
- package/src/remark/noSpaceInLinks.test.mjs +22 -0
- package/src/remark/straightQuotes.mjs +31 -0
- package/src/remark/straightQuotes.test.mjs +25 -0
- package/src/remark/tableAlignment.mjs +23 -0
- package/src/remark/tableAlignment.test.mjs +28 -0
- package/src/remark/terminalLanguage.mjs +19 -0
- package/src/remark/terminalLanguage.test.mjs +17 -0
- package/src/untyped-plugins.d.ts +11 -11
- package/src/utils/build.test.mjs +546 -575
- package/src/utils/pnpm.mjs +192 -3
- package/src/utils/pnpm.test.mjs +580 -0
- package/src/utils/testUtils.mjs +18 -0
- package/src/utils/typescript.test.mjs +249 -272
- package/vale/.vale.ini +1 -0
- package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
- package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
- package/vale/styles/MUI/GoogleLatin.yml +11 -0
- package/vale/styles/MUI/MuiBrandName.yml +22 -0
- package/vale/styles/MUI/NoBritish.yml +112 -0
- 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
|
-
|
|
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: '
|
|
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
|
-
|
|
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,6 @@
|
|
|
1
|
-
|
|
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
|
}
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
62
|
-
* @param {import('estree').
|
|
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(
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|