@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.
Files changed (39) hide show
  1. package/README.md +7 -4
  2. package/build/changelog/fetchChangelogs.d.mts +9 -10
  3. package/build/changelog/index.d.mts +1 -0
  4. package/build/cli/cmdBaseline.d.mts +18 -0
  5. package/build/cli/cmdNetlifyIgnore.d.mts +1 -1
  6. package/build/eslint/mui/rules/no-constructor-assigned-field.d.mts +50 -0
  7. package/build/eslint/packageJsonConfig.d.mts +7 -0
  8. package/build/remark/config.d.mts +5 -14
  9. package/build/utils/build.d.mts +14 -0
  10. package/build/utils/credentials.d.mts +2 -2
  11. package/build/utils/git.d.mts +30 -9
  12. package/build/utils/github.d.mts +8 -0
  13. package/build/utils/pnpm.d.mts +49 -0
  14. package/package.json +38 -35
  15. package/src/changelog/fetchChangelogs.mjs +66 -26
  16. package/src/changelog/index.mjs +3 -0
  17. package/src/cli/cmdBaseline.mjs +31 -0
  18. package/src/cli/cmdBuild.mjs +2 -0
  19. package/src/cli/cmdNetlifyIgnore.mjs +14 -7
  20. package/src/cli/cmdPublish.mjs +15 -0
  21. package/src/cli/cmdPublishNewPackage.mjs +6 -2
  22. package/src/cli/index.mjs +2 -0
  23. package/src/eslint/baseConfig.mjs +2 -0
  24. package/src/eslint/docsConfig.mjs +28 -10
  25. package/src/eslint/jsonConfig.mjs +2 -1
  26. package/src/eslint/mui/config.mjs +16 -1
  27. package/src/eslint/mui/index.mjs +2 -0
  28. package/src/eslint/mui/rules/no-constructor-assigned-field.mjs +183 -0
  29. package/src/eslint/packageJsonConfig.mjs +50 -0
  30. package/src/eslint/testConfig.mjs +1 -2
  31. package/src/remark/config.mjs +60 -13
  32. package/src/utils/build.mjs +34 -0
  33. package/src/utils/credentials.mjs +4 -3
  34. package/src/utils/extractErrorCodes.mjs +1 -3
  35. package/src/utils/git.mjs +107 -46
  36. package/src/utils/github.mjs +27 -0
  37. package/src/utils/pnpm.mjs +109 -0
  38. package/vale/styles/MUI/GoogleLatin.yml +5 -2
  39. package/vale/styles/MUI/CorrectRererenceCased.yml +0 -14
@@ -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
+ });
@@ -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 - List of workspace names to process
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: 'List of workspace names to process',
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
- const workspaceMap = new Map(
139
- allWorkspaces.flatMap((workspace) =>
140
- workspace.name ? [[workspace.name, workspace.path]] : [],
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(
@@ -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 { getPackagesNeedingManualPublish, getWorkspacePackages } from '../utils/pnpm.mjs';
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: '0.0.1',
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(recommendedConfig, {
23
- settings: {
24
- next: {
25
- rootDir: 'docs',
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
- files: [`**/*${EXTENSION_TS}`],
29
- rules: {
30
- 'compat/compat': 'off',
31
- 'jsx-a11y/anchor-is-valid': 'off',
32
- 'no-irregular-whitespace': ['error', { skipJSXText: true, skipStrings: true }],
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
- ignores: ['package-lock.json'],
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}`],
@@ -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
@@ -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 [plugin, baseSettings] = /** @type {[import('unified').Plugin<any[], any, any>, any]} */ (
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
- settings: {
148
- bullet: '-',
149
- emphasis: '_',
150
- fence: '`',
151
- listItemIndent: 'one',
152
- resourceLink: true,
153
- rule: '-',
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
  }