@mui/internal-code-infra 0.1.1-canary.2 → 0.1.1-canary.21
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 +7 -4
- package/build/changelog/fetchChangelogs.d.mts +9 -10
- package/build/changelog/index.d.mts +1 -0
- package/build/cli/cmdBaseline.d.mts +18 -0
- package/build/cli/cmdNetlifyIgnore.d.mts +1 -1
- package/build/eslint/mui/rules/no-constructor-assigned-field.d.mts +50 -0
- package/build/eslint/packageJsonConfig.d.mts +7 -0
- package/build/remark/config.d.mts +5 -14
- package/build/utils/build.d.mts +14 -0
- package/build/utils/credentials.d.mts +2 -2
- package/build/utils/git.d.mts +30 -9
- package/build/utils/github.d.mts +8 -0
- package/build/utils/pnpm.d.mts +49 -0
- package/package.json +38 -35
- package/src/changelog/fetchChangelogs.mjs +66 -26
- package/src/changelog/index.mjs +3 -0
- package/src/cli/cmdBaseline.mjs +31 -0
- package/src/cli/cmdBuild.mjs +2 -0
- package/src/cli/cmdNetlifyIgnore.mjs +14 -7
- package/src/cli/cmdPublish.mjs +15 -0
- package/src/cli/cmdPublishNewPackage.mjs +6 -2
- package/src/cli/index.mjs +2 -0
- package/src/eslint/baseConfig.mjs +2 -0
- package/src/eslint/docsConfig.mjs +28 -10
- package/src/eslint/jsonConfig.mjs +2 -1
- package/src/eslint/mui/config.mjs +16 -1
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-constructor-assigned-field.mjs +183 -0
- package/src/eslint/packageJsonConfig.mjs +50 -0
- package/src/eslint/testConfig.mjs +1 -2
- package/src/remark/config.mjs +60 -13
- package/src/utils/build.mjs +34 -0
- package/src/utils/credentials.mjs +4 -3
- package/src/utils/extractErrorCodes.mjs +1 -3
- package/src/utils/git.mjs +107 -46
- package/src/utils/github.mjs +27 -0
- package/src/utils/pnpm.mjs +109 -0
- package/vale/styles/MUI/GoogleLatin.yml +5 -2
- package/vale/styles/MUI/CorrectRererenceCased.yml +0 -14
package/src/changelog/index.mjs
CHANGED
|
@@ -5,3 +5,6 @@
|
|
|
5
5
|
export { generateChangelog } from './generateChangelog.mjs';
|
|
6
6
|
export * from './loadChangelogConfig.mjs';
|
|
7
7
|
export * from './fetchChangelogs.mjs';
|
|
8
|
+
// Moved to the git utilities, where the rest of the local git plumbing lives. Re-exported because
|
|
9
|
+
// `@mui/internal-code-infra/changelog` is a published entry point and this was part of it.
|
|
10
|
+
export { findLatestTaggedVersion } from '../utils/git.mjs';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
|
|
5
|
+
import { resolveBaseline } from '../utils/git.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} Args
|
|
9
|
+
* @property {string} [baseBranch] - Branch pull requests fork from
|
|
10
|
+
* @property {string} [remote] - Remote the base branch is fetched from
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
14
|
+
command: 'baseline',
|
|
15
|
+
describe: 'Print the commit HEAD should be compared against',
|
|
16
|
+
builder: (yargs) => {
|
|
17
|
+
return yargs
|
|
18
|
+
.option('base-branch', {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: "Branch pull requests fork from. Default: the remote's default branch",
|
|
21
|
+
})
|
|
22
|
+
.option('remote', {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description:
|
|
25
|
+
'Remote the base branch is fetched from. Default: upstream, else origin, whichever points at a mui repository',
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
handler: async (argv) => {
|
|
29
|
+
console.log(await resolveBaseline({ baseBranch: argv.baseBranch, remote: argv.remote }));
|
|
30
|
+
},
|
|
31
|
+
});
|
package/src/cli/cmdBuild.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
createPackageExports,
|
|
13
13
|
createPackageImports,
|
|
14
14
|
getOutExtension,
|
|
15
|
+
validateEnginesNode,
|
|
15
16
|
validatePkgJson,
|
|
16
17
|
} from '../utils/build.mjs';
|
|
17
18
|
import { readPnpmConfig } from '../utils/pnpm.mjs';
|
|
@@ -266,6 +267,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
266
267
|
const pkgJsonPath = path.join(cwd, 'package.json');
|
|
267
268
|
const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, { encoding: 'utf8' }));
|
|
268
269
|
validatePkgJson(packageJson, { skipMainCheck: args.skipMainCheck, enableReactCompiler });
|
|
270
|
+
validateEnginesNode(packageJson, cwd);
|
|
269
271
|
|
|
270
272
|
const buildDirBase = /** @type {string} */ (packageJson.publishConfig?.directory);
|
|
271
273
|
const buildDir = path.join(cwd, buildDirBase);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object} Args
|
|
7
|
-
* @property {string[]} workspaces -
|
|
7
|
+
* @property {string[]} workspaces - Workspace names or root-relative paths to process
|
|
8
8
|
* @property {boolean} [check] - Check mode - error if the generated content differs from current
|
|
9
9
|
* @property {string} [baseBranch] - Branch to compare PRs against (default: master)
|
|
10
10
|
*/
|
|
@@ -100,7 +100,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
100
100
|
.positional('workspaces', {
|
|
101
101
|
type: 'string',
|
|
102
102
|
array: true,
|
|
103
|
-
describe: '
|
|
103
|
+
describe: 'Workspace names or root-relative paths to process',
|
|
104
104
|
demandOption: true,
|
|
105
105
|
})
|
|
106
106
|
.option('check', {
|
|
@@ -115,6 +115,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
115
115
|
"Branch to compare PRs against (the site's production branch on Netlify). Production and branch deploys always rebuild regardless of this value.",
|
|
116
116
|
})
|
|
117
117
|
.example('$0 netlify-ignore @mui/internal-docs-infra', 'Update netlify.toml for a workspace')
|
|
118
|
+
.example('$0 netlify-ignore ./apps/dashboard', 'Update an unnamed workspace by path')
|
|
118
119
|
.example(
|
|
119
120
|
'$0 netlify-ignore @mui/internal-docs-infra @mui/internal-code-infra',
|
|
120
121
|
'Update netlify.toml for multiple workspaces',
|
|
@@ -135,11 +136,17 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
135
136
|
|
|
136
137
|
// Get all workspace packages and create workspace map
|
|
137
138
|
const allWorkspaces = await getWorkspacePackages({ cwd: workspaceRoot });
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
|
|
139
|
+
/** @type {Map<string, string>} */
|
|
140
|
+
const workspaceMap = new Map();
|
|
141
|
+
for (const workspace of allWorkspaces) {
|
|
142
|
+
if (workspace.name) {
|
|
143
|
+
workspaceMap.set(workspace.name, workspace.path);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Paths also identify private workspaces that have no package name.
|
|
147
|
+
const relativePath = toPosixPath(path.relative(workspaceRoot, workspace.path));
|
|
148
|
+
workspaceMap.set(`./${relativePath}`, workspace.path);
|
|
149
|
+
}
|
|
143
150
|
|
|
144
151
|
// Process each workspace concurrently
|
|
145
152
|
await Promise.all(
|
package/src/cli/cmdPublish.mjs
CHANGED
|
@@ -19,7 +19,9 @@ import * as semver from 'semver';
|
|
|
19
19
|
|
|
20
20
|
import { persistentAuthStrategy } from '../utils/github.mjs';
|
|
21
21
|
import {
|
|
22
|
+
getDistTagConflicts,
|
|
22
23
|
getPackagesNeedingManualPublish,
|
|
24
|
+
getRegistryPackages,
|
|
23
25
|
getReleaseVersion,
|
|
24
26
|
getWorkspacePackages,
|
|
25
27
|
publishPackages,
|
|
@@ -373,6 +375,19 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
373
375
|
);
|
|
374
376
|
}
|
|
375
377
|
|
|
378
|
+
const distTagConflicts = getDistTagConflicts(
|
|
379
|
+
filteredPackages,
|
|
380
|
+
tag,
|
|
381
|
+
await getRegistryPackages(filteredPackages),
|
|
382
|
+
);
|
|
383
|
+
if (distTagConflicts.length > 0) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Publishing with the "${tag}" dist-tag would move it to the wrong release line:
|
|
386
|
+
${distTagConflicts.map(({ pkg, reason }) => ` * ${pkg.name}: ${reason}`).join('\n')}
|
|
387
|
+
Publish a previous major with its "latest-vX" dist-tag and a prerelease of the next major with "next". On a release branch, also make that dist-tag the "dist-tag" default of the publish workflow. If the dist-tag on npm is outdated, fix it with "npm dist-tag" first.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
376
391
|
// Publish to npm (pnpm handles duplicate checking automatically)
|
|
377
392
|
// No git checks, we'll do our own
|
|
378
393
|
console.log('\n📦 Publishing packages to npm...');
|
|
@@ -11,7 +11,11 @@ import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
|
|
|
11
11
|
|
|
12
12
|
import { getRepositoryInfo } from '../utils/git.mjs';
|
|
13
13
|
import { toPosixPath } from '../utils/path.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getPackagesNeedingManualPublish,
|
|
16
|
+
getWorkspacePackages,
|
|
17
|
+
PLACEHOLDER_VERSION,
|
|
18
|
+
} from '../utils/pnpm.mjs';
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* @typedef {Object} Args
|
|
@@ -78,7 +82,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
78
82
|
const repo = await getRepositoryInfo();
|
|
79
83
|
const packageJson = {
|
|
80
84
|
name: pkg.name,
|
|
81
|
-
version:
|
|
85
|
+
version: PLACEHOLDER_VERSION,
|
|
82
86
|
repository: {
|
|
83
87
|
type: 'git',
|
|
84
88
|
url: `git+https://github.com/${repo.owner}/${repo.repo}.git`,
|
package/src/cli/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import yargs from 'yargs';
|
|
|
4
4
|
import { hideBin } from 'yargs/helpers';
|
|
5
5
|
|
|
6
6
|
import cmdArgosPush from './cmdArgosPush.mjs';
|
|
7
|
+
import cmdBaseline from './cmdBaseline.mjs';
|
|
7
8
|
import cmdBuild from './cmdBuild.mjs';
|
|
8
9
|
import cmdCopyFiles from './cmdCopyFiles.mjs';
|
|
9
10
|
import cmdExtractErrorCodes from './cmdExtractErrorCodes.mjs';
|
|
@@ -38,6 +39,7 @@ await yargs(hideBin(process.argv))
|
|
|
38
39
|
globalArgv = argv;
|
|
39
40
|
}, true)
|
|
40
41
|
.command(cmdArgosPush)
|
|
42
|
+
.command(cmdBaseline)
|
|
41
43
|
.command(cmdBuild)
|
|
42
44
|
.command(cmdCopyFiles)
|
|
43
45
|
.command(cmdExtractErrorCodes)
|
|
@@ -17,6 +17,7 @@ import { createCoreConfig } from './mui/config.mjs';
|
|
|
17
17
|
import muiPlugin from './mui/index.mjs';
|
|
18
18
|
import { EXTENSION_TS } from './extensions.mjs';
|
|
19
19
|
import { createJsonConfig } from './jsonConfig.mjs';
|
|
20
|
+
import { createPackageJsonConfig } from './packageJsonConfig.mjs';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* @param {string} filePath
|
|
@@ -73,6 +74,7 @@ export function createBaseConfig({
|
|
|
73
74
|
includeIgnoreIfExists(path.join(baseDirectory, '.gitignore'), `Ignore rules from .gitignore`),
|
|
74
75
|
includeIgnoreIfExists(path.join(baseDirectory, '.lintignore'), `Ignore rules from .lintignore`),
|
|
75
76
|
createJsonConfig(),
|
|
77
|
+
createPackageJsonConfig(),
|
|
76
78
|
prettier,
|
|
77
79
|
// Markdown + MDX linting via eslint-plugin-mdx. Severities for markdown
|
|
78
80
|
// quality checks live in the project's `.remarkrc` (see
|
|
@@ -19,17 +19,35 @@ export function createDocsConfig() {
|
|
|
19
19
|
? nextjsAlias.flatConfig.recommended
|
|
20
20
|
: nextjsAlias.configs.recommended;
|
|
21
21
|
|
|
22
|
-
return defineConfig(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
return defineConfig(
|
|
23
|
+
recommendedConfig,
|
|
24
|
+
{
|
|
25
|
+
settings: {
|
|
26
|
+
next: {
|
|
27
|
+
rootDir: 'docs',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
files: [`**/*${EXTENSION_TS}`],
|
|
31
|
+
rules: {
|
|
32
|
+
'compat/compat': 'off',
|
|
33
|
+
'jsx-a11y/anchor-is-valid': 'off',
|
|
34
|
+
'no-irregular-whitespace': ['error', { skipJSXText: true, skipStrings: true }],
|
|
26
35
|
},
|
|
27
36
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
{
|
|
38
|
+
// Build configuration and maintenance scripts aren't documentation examples
|
|
39
|
+
// and are often intentionally outside the docs TypeScript project.
|
|
40
|
+
files: [`**/*${EXTENSION_TS}`],
|
|
41
|
+
ignores: ['**/*.config.*', '**/scripts/**'],
|
|
42
|
+
languageOptions: {
|
|
43
|
+
parserOptions: {
|
|
44
|
+
projectService: true,
|
|
45
|
+
tsconfigRootDir: process.cwd(),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
rules: {
|
|
49
|
+
'@typescript-eslint/no-deprecated': 'error',
|
|
50
|
+
},
|
|
33
51
|
},
|
|
34
|
-
|
|
52
|
+
);
|
|
35
53
|
}
|
|
@@ -9,7 +9,8 @@ export function createJsonConfig() {
|
|
|
9
9
|
{
|
|
10
10
|
name: 'JSON files',
|
|
11
11
|
files: ['**/*.json'],
|
|
12
|
-
|
|
12
|
+
// package.json is linted by eslint-plugin-package-json, whose rules need the jsonc-eslint-parser AST.
|
|
13
|
+
ignores: ['package-lock.json', '**/package.json'],
|
|
13
14
|
plugins: { json },
|
|
14
15
|
language: 'json/json',
|
|
15
16
|
extends: [json.configs.recommended],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineConfig } from 'eslint/config';
|
|
2
|
-
import { EXTENSION_DTS } from '../extensions.mjs';
|
|
2
|
+
import { EXTENSION_DTS, EXTENSION_TS_ONLY } from '../extensions.mjs';
|
|
3
3
|
|
|
4
4
|
const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
|
|
5
5
|
|
|
@@ -184,6 +184,14 @@ const typescriptOverrides = {
|
|
|
184
184
|
// TypeScript equivalents of ESLint rules
|
|
185
185
|
'default-param-last': 'off',
|
|
186
186
|
'@typescript-eslint/default-param-last': 'error',
|
|
187
|
+
'@typescript-eslint/ban-ts-comment': [
|
|
188
|
+
'error',
|
|
189
|
+
{
|
|
190
|
+
'ts-expect-error': 'allow-with-description',
|
|
191
|
+
'ts-ignore': true,
|
|
192
|
+
minimumDescriptionLength: 10,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
187
195
|
'no-array-constructor': 'off',
|
|
188
196
|
'@typescript-eslint/no-array-constructor': 'error',
|
|
189
197
|
'@typescript-eslint/triple-slash-reference': 'off',
|
|
@@ -538,6 +546,13 @@ export function createCoreConfig(options = {}) {
|
|
|
538
546
|
'@typescript-eslint/return-await': 'off',
|
|
539
547
|
},
|
|
540
548
|
},
|
|
549
|
+
{
|
|
550
|
+
name: 'mui-base/ts',
|
|
551
|
+
files: [`**/*${EXTENSION_TS_ONLY}`],
|
|
552
|
+
rules: {
|
|
553
|
+
'mui/no-constructor-assigned-field': 'error',
|
|
554
|
+
},
|
|
555
|
+
},
|
|
541
556
|
{
|
|
542
557
|
name: 'mui-base/dts',
|
|
543
558
|
files: [`**/*${EXTENSION_DTS}`],
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import addUndefToOptional from './rules/add-undef-to-optional.mjs';
|
|
|
14
14
|
import flattenParentheses from './rules/flatten-parentheses.mjs';
|
|
15
15
|
import noPresentationRole from './rules/no-presentation-role.mjs';
|
|
16
16
|
import noFloatingCleanup from './rules/no-floating-cleanup.mjs';
|
|
17
|
+
import noConstructorAssignedField from './rules/no-constructor-assigned-field.mjs';
|
|
17
18
|
|
|
18
19
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
19
20
|
const muiPlugin = {
|
|
@@ -39,6 +40,7 @@ const muiPlugin = {
|
|
|
39
40
|
'flatten-parentheses': /** @type {any} */ (flattenParentheses),
|
|
40
41
|
'no-presentation-role': noPresentationRole,
|
|
41
42
|
'no-floating-cleanup': /** @type {any} */ (noFloatingCleanup),
|
|
43
|
+
'no-constructor-assigned-field': /** @type {any} */ (noConstructorAssignedField),
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { ASTUtils, ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@typescript-eslint/utils').TSESTree.PropertyDefinition} PropertyDefinition
|
|
5
|
+
* @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
9
|
+
(name) =>
|
|
10
|
+
`https://github.com/mui/mui-public/blob/master/packages/code-infra/src/eslint/mui/rules/${name}.mjs`,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the constructor whose own code contains `node`, or `null` when `node` is outside a
|
|
15
|
+
* constructor or inside a nested function or class.
|
|
16
|
+
* @param {Node} node
|
|
17
|
+
*/
|
|
18
|
+
function getEnclosingConstructor(node) {
|
|
19
|
+
let current = node.parent;
|
|
20
|
+
while (current && !ASTUtils.isFunction(current)) {
|
|
21
|
+
if (current.type === 'ClassBody') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
current = current.parent;
|
|
25
|
+
}
|
|
26
|
+
const method = current?.parent;
|
|
27
|
+
return method && ASTUtils.isConstructor(method) ? method : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the extra edits needed before `field` can be marked `declare`.
|
|
32
|
+
* @param {PropertyDefinition} field
|
|
33
|
+
*/
|
|
34
|
+
function getDeclareHint(field) {
|
|
35
|
+
const edits = [];
|
|
36
|
+
if (field.value) {
|
|
37
|
+
edits.push('drop its initializer');
|
|
38
|
+
}
|
|
39
|
+
if (field.definite) {
|
|
40
|
+
edits.push('drop the `!`');
|
|
41
|
+
}
|
|
42
|
+
if (field.override) {
|
|
43
|
+
edits.push('drop `override`');
|
|
44
|
+
}
|
|
45
|
+
return edits.length > 0 ? ` (${edits.join(', ')})` : '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ESLint rule that disallows assigning a class field in the constructor when it is also
|
|
50
|
+
* declared as a (non-`declare`) class field.
|
|
51
|
+
*
|
|
52
|
+
* Babel 8 only strips TypeScript fields marked `declare` (see mui/material-ui#49241), so a
|
|
53
|
+
* field without an initializer is emitted as a property definition that sets it to
|
|
54
|
+
* `undefined` before the constructor assigns it again. Either initialize the field at its
|
|
55
|
+
* declaration, or mark it `declare` so it only serves as a type annotation.
|
|
56
|
+
*
|
|
57
|
+
* `declare` fields are exempt from TypeScript's strict property initialization check
|
|
58
|
+
* (TS2564), so prefer initializing at the declaration where the value doesn't depend on
|
|
59
|
+
* constructor arguments. The `declare` suggestion is only offered when every path through the
|
|
60
|
+
* constructor assigns the field, i.e. for an unconditional top-level assignment.
|
|
61
|
+
*
|
|
62
|
+
* Only plain `=` assignments count: compound assignments read the field rather than
|
|
63
|
+
* re-initialize it. A field with an initializer is only reported for an unconditional
|
|
64
|
+
* top-level assignment, as a conditional one overrides a default. Private (`#`) and decorated
|
|
65
|
+
* fields are ignored: they are runtime declarations that `declare` can't replace.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Invalid
|
|
69
|
+
* class ModalManager {
|
|
70
|
+
* private modals: Modal[];
|
|
71
|
+
* constructor() {
|
|
72
|
+
* this.modals = [];
|
|
73
|
+
* }
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Valid - initialized at the declaration
|
|
78
|
+
* class ModalManager {
|
|
79
|
+
* private modals: Modal[] = [];
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Valid - declared only, assigned in the constructor
|
|
84
|
+
* class ModalManager {
|
|
85
|
+
* declare private modals: Modal[];
|
|
86
|
+
* constructor(modals: Modal[]) {
|
|
87
|
+
* this.modals = modals;
|
|
88
|
+
* }
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
export default createRule({
|
|
92
|
+
name: 'no-constructor-assigned-field',
|
|
93
|
+
meta: {
|
|
94
|
+
type: 'suggestion',
|
|
95
|
+
docs: {
|
|
96
|
+
description:
|
|
97
|
+
'Disallow assigning class fields in the constructor when they are also declared as class fields',
|
|
98
|
+
},
|
|
99
|
+
hasSuggestions: true,
|
|
100
|
+
messages: {
|
|
101
|
+
assignedInConstructor:
|
|
102
|
+
"Field '{{name}}' is declared as a class field and also assigned in the constructor. " +
|
|
103
|
+
'Either initialize it at the declaration and remove the constructor assignment, ' +
|
|
104
|
+
'or mark it `declare`{{declareHint}}.',
|
|
105
|
+
addDeclare: 'Mark the field `declare`.',
|
|
106
|
+
},
|
|
107
|
+
schema: [],
|
|
108
|
+
},
|
|
109
|
+
defaultOptions: [],
|
|
110
|
+
create(context) {
|
|
111
|
+
/**
|
|
112
|
+
* Fields assigned in a constructor, mapped to whether any of those assignments is an
|
|
113
|
+
* unconditional top-level statement.
|
|
114
|
+
* @type {Map<PropertyDefinition, boolean>}
|
|
115
|
+
*/
|
|
116
|
+
const assignedFields = new Map();
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
AssignmentExpression(node) {
|
|
120
|
+
if (
|
|
121
|
+
node.operator !== '=' ||
|
|
122
|
+
node.left.type !== 'MemberExpression' ||
|
|
123
|
+
node.left.object.type !== 'ThisExpression'
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const name = ASTUtils.getPropertyName(node.left);
|
|
128
|
+
if (name === null) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const constructor = getEnclosingConstructor(node);
|
|
132
|
+
if (!constructor) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const field = constructor.parent.body.find(
|
|
137
|
+
/** @returns {member is PropertyDefinition} */
|
|
138
|
+
(member) =>
|
|
139
|
+
member.type === 'PropertyDefinition' &&
|
|
140
|
+
!member.static &&
|
|
141
|
+
!member.declare &&
|
|
142
|
+
member.decorators.length === 0 &&
|
|
143
|
+
ASTUtils.getPropertyName(member) === name,
|
|
144
|
+
);
|
|
145
|
+
if (!field) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const statement = node.parent;
|
|
150
|
+
const unconditional =
|
|
151
|
+
statement.type === 'ExpressionStatement' && statement.parent === constructor.value.body;
|
|
152
|
+
assignedFields.set(field, Boolean(assignedFields.get(field)) || unconditional);
|
|
153
|
+
},
|
|
154
|
+
'ClassBody:exit'(classBody) {
|
|
155
|
+
for (const member of classBody.body) {
|
|
156
|
+
if (member.type !== 'PropertyDefinition' || !assignedFields.has(member)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const unconditional = Boolean(assignedFields.get(member));
|
|
160
|
+
if (member.value && !unconditional) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const declareHint = getDeclareHint(member);
|
|
165
|
+
context.report({
|
|
166
|
+
node: member,
|
|
167
|
+
messageId: 'assignedInConstructor',
|
|
168
|
+
data: { name: ASTUtils.getPropertyName(member), declareHint },
|
|
169
|
+
suggest:
|
|
170
|
+
unconditional && declareHint === ''
|
|
171
|
+
? [
|
|
172
|
+
{
|
|
173
|
+
messageId: 'addDeclare',
|
|
174
|
+
fix: (fixer) => fixer.insertTextBefore(member, 'declare '),
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
: [],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineConfig } from 'eslint/config';
|
|
2
|
+
import packageJson from 'eslint-plugin-package-json';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lints package.json files for publishing metadata. Published packages must
|
|
6
|
+
* declare an SPDX license, `"author": "MUI Team"` and a repository pointing at
|
|
7
|
+
* their own directory. Private packages must not carry publishing metadata.
|
|
8
|
+
* @returns {import('eslint').Linter.Config[]}
|
|
9
|
+
*/
|
|
10
|
+
export function createPackageJsonConfig() {
|
|
11
|
+
return defineConfig([
|
|
12
|
+
{
|
|
13
|
+
name: 'package.json files',
|
|
14
|
+
files: ['**/package.json'],
|
|
15
|
+
ignores: ['**/__fixtures__/**'],
|
|
16
|
+
extends: [packageJson.configs.recommended],
|
|
17
|
+
rules: {
|
|
18
|
+
'package-json/require-author': ['error', { ignorePrivate: true }],
|
|
19
|
+
'package-json/require-description': ['error', { ignorePrivate: true }],
|
|
20
|
+
'package-json/restrict-private-properties': [
|
|
21
|
+
'error',
|
|
22
|
+
// `publishConfig.directory` is allowed: `code-infra build` reads it, also for private packages.
|
|
23
|
+
{ blockedProperties: ['files', 'license'] },
|
|
24
|
+
],
|
|
25
|
+
'no-restricted-syntax': [
|
|
26
|
+
'error',
|
|
27
|
+
{
|
|
28
|
+
selector:
|
|
29
|
+
'Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value="author"][value.value!="MUI Team"]',
|
|
30
|
+
message: 'The "author" field must be "MUI Team".',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
// Condition order in "exports" is significant, and scripts are grouped by purpose.
|
|
34
|
+
'package-json/sort-collections': 'off',
|
|
35
|
+
// Packages built with `code-infra build` get these fields in the generated package.json.
|
|
36
|
+
'package-json/require-files': 'off',
|
|
37
|
+
'package-json/require-sideEffects': 'off',
|
|
38
|
+
'package-json/require-type': 'off',
|
|
39
|
+
// Peer dependencies are installed once at the workspace root.
|
|
40
|
+
'package-json/specify-peers-locally': 'off',
|
|
41
|
+
// CLI-only packages have no "exports".
|
|
42
|
+
'package-json/require-exports': 'off',
|
|
43
|
+
// Rejects the object form of "browser", which the browser field spec allows.
|
|
44
|
+
'package-json/valid-browser': 'off',
|
|
45
|
+
// npm only auto-includes "main" when it has no "./" prefix; removing it from "files" can drop the entry point.
|
|
46
|
+
'package-json/no-redundant-files': 'off',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
@@ -21,7 +21,6 @@ export const baseSpecRules = {
|
|
|
21
21
|
'@typescript-eslint/no-unused-expressions': 'off',
|
|
22
22
|
'@typescript-eslint/no-unused-vars': 'off',
|
|
23
23
|
'@typescript-eslint/no-use-before-define': 'off',
|
|
24
|
-
'@typescript-eslint/ban-ts-comment': 'off',
|
|
25
24
|
|
|
26
25
|
'import/prefer-default-export': 'off',
|
|
27
26
|
|
|
@@ -63,6 +62,7 @@ export function createTestConfig() {
|
|
|
63
62
|
'import/named': 'off',
|
|
64
63
|
'mui/disallow-active-element-as-key-event-target': 'error',
|
|
65
64
|
'mui/consistent-production-guard': 'off',
|
|
65
|
+
'mui/no-constructor-assigned-field': 'off',
|
|
66
66
|
|
|
67
67
|
// disable eslint-plugin-jsx-a11y
|
|
68
68
|
// tests are not driven by assistive technology
|
|
@@ -87,7 +87,6 @@ export function createTestConfig() {
|
|
|
87
87
|
'react/no-unused-prop-types': 'off',
|
|
88
88
|
// Part of the migration away from airbnb config. Turned of initially.
|
|
89
89
|
'@typescript-eslint/no-empty-function': 'off',
|
|
90
|
-
'@typescript-eslint/ban-ts-comment': 'off',
|
|
91
90
|
'testing-library/no-node-access': 'off',
|
|
92
91
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
|
93
92
|
// end migration
|
package/src/remark/config.mjs
CHANGED
|
@@ -97,6 +97,49 @@ function withOverrides(name, plugin, baseSettings, overrideEntries) {
|
|
|
97
97
|
return wrapper;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Allows link-reference comments while retaining diagnostics for real links and definitions.
|
|
102
|
+
* Existing docs generators use these markers in both Markdown and MDX and parse them as metadata.
|
|
103
|
+
* Authored MDX can use JSX comments without this compatibility option.
|
|
104
|
+
*
|
|
105
|
+
* @param {import('unified').Plugin<any[], any, any>} plugin
|
|
106
|
+
*/
|
|
107
|
+
function withDefinitionComments(plugin) {
|
|
108
|
+
/** @param {any[]} settings */
|
|
109
|
+
return function wrapper(...settings) {
|
|
110
|
+
const processor = unified().use(plugin, ...settings);
|
|
111
|
+
/** @type {import('unified').Transformer} */
|
|
112
|
+
return async function transformer(tree, file) {
|
|
113
|
+
const previousMessages = file.messages.length;
|
|
114
|
+
await processor.run(tree, file);
|
|
115
|
+
file.messages = file.messages.filter((message, index) => {
|
|
116
|
+
const node = message.ancestors?.at(-1);
|
|
117
|
+
return !(
|
|
118
|
+
index >= previousMessages &&
|
|
119
|
+
node?.type === 'definition' &&
|
|
120
|
+
'identifier' in node &&
|
|
121
|
+
node.identifier === '//' &&
|
|
122
|
+
'title' in node &&
|
|
123
|
+
typeof node.title === 'string' &&
|
|
124
|
+
node.title.length > 0
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Makes the processor output the source text unchanged. The preset only lints, and
|
|
133
|
+
* remark-stringify doesn't round-trip Markdown (it escapes GitHub alerts, rewrites bare
|
|
134
|
+
* URLs, ...). eslint-plugin-mdx offers the processed output as a whole-file fix for any
|
|
135
|
+
* remark message, so with this `eslint --fix` leaves Markdown alone.
|
|
136
|
+
*
|
|
137
|
+
* @this {import('unified').Processor}
|
|
138
|
+
*/
|
|
139
|
+
function remarkPreserveSource() {
|
|
140
|
+
this.compiler = (_tree, file) => String(file);
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
/**
|
|
101
144
|
* Returns a remark preset wiring the MUI-authored remark-lint plugins together
|
|
102
145
|
* with a curated set of community plugins. Drop this into `.remarkrc.mjs`:
|
|
@@ -120,9 +163,11 @@ function withOverrides(name, plugin, baseSettings, overrideEntries) {
|
|
|
120
163
|
* ```
|
|
121
164
|
*
|
|
122
165
|
* @param {Object} [options]
|
|
166
|
+
* @param {boolean} [options.allowDefinitionComments=false] Allow titled `[//]` link-reference comments.
|
|
123
167
|
* @param {Array<{ files: string | string[], rules: Record<string, false | unknown[]> }>} [options.overrides]
|
|
168
|
+
* @returns {import('unified').Preset}
|
|
124
169
|
*/
|
|
125
|
-
export function createRemarkConfig({ overrides = [] } = {}) {
|
|
170
|
+
export function createRemarkConfig({ overrides = [], allowDefinitionComments = false } = {}) {
|
|
126
171
|
for (const override of overrides) {
|
|
127
172
|
const unknown = Object.keys(override.rules).filter((ruleName) => !(ruleName in RULES));
|
|
128
173
|
if (unknown.length > 0) {
|
|
@@ -130,10 +175,14 @@ export function createRemarkConfig({ overrides = [] } = {}) {
|
|
|
130
175
|
}
|
|
131
176
|
}
|
|
132
177
|
|
|
178
|
+
/** @type {import('unified').PluggableList} */
|
|
133
179
|
const entries = Object.entries(RULES).map(([name, entry]) => {
|
|
134
|
-
const [
|
|
135
|
-
entry
|
|
136
|
-
|
|
180
|
+
const [rulePlugin, baseSettings] =
|
|
181
|
+
/** @type {[import('unified').Plugin<any[], any, any>, any]} */ (entry);
|
|
182
|
+
const plugin =
|
|
183
|
+
allowDefinitionComments && (name === 'no-empty-url' || name === 'no-unused-definitions')
|
|
184
|
+
? withDefinitionComments(rulePlugin)
|
|
185
|
+
: rulePlugin;
|
|
137
186
|
const overrideEntries = overrides
|
|
138
187
|
.filter((override) => name in override.rules)
|
|
139
188
|
.map((override) => ({ files: override.files, settings: override.rules[name] }));
|
|
@@ -144,14 +193,12 @@ export function createRemarkConfig({ overrides = [] } = {}) {
|
|
|
144
193
|
});
|
|
145
194
|
|
|
146
195
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
plugins: [[remarkFrontmatter, ['yaml', 'toml']], remarkGfm, remarkLint, ...entries],
|
|
196
|
+
plugins: [
|
|
197
|
+
[remarkFrontmatter, ['yaml', 'toml']],
|
|
198
|
+
remarkGfm,
|
|
199
|
+
remarkLint,
|
|
200
|
+
...entries,
|
|
201
|
+
remarkPreserveSource,
|
|
202
|
+
],
|
|
156
203
|
};
|
|
157
204
|
}
|