@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
package/README.md CHANGED
@@ -14,9 +14,9 @@ This is stored in the `docs` top-level directory.
14
14
 
15
15
  1. Go to the publish action -
16
16
 
17
- - [Base UI](https://github.com/mui/base-ui/actions/workflows/publish.yml)
17
+ - [Base UI](https://github.com/mui/base-ui/actions/workflows/publish.yml)
18
18
  - [Core](https://github.com/mui/material-ui/actions/workflows/publish.yml)
19
- - [MUI X](https://github.com/mui/mui-x/actions/workflows/publish.yml)
19
+ - [MUI X](https://github.com/mui/mui-x/actions/workflows/publish.yml)
20
20
 
21
21
  2. Choose "Run workflow" dropdown
22
22
 
@@ -24,12 +24,15 @@ This is stored in the `docs` top-level directory.
24
24
  > - **Commit SHA to release from:** the commit that contains the merged release on master. This commit is linked to the GitHub release.
25
25
  > - **Run in dry-run mode:** Used for debugging.
26
26
  > - **Create GitHub release:** Keep selected if you want a GitHub release to be automatically created from the changelog.
27
- > - **npm dist tag to publish to** Use to publish legacy or canary versions.
27
+ > - **npm dist tag to publish to** Use to publish legacy or canary versions. The publish fails before it publishes anything when a dist-tag would move to a lower version. `latest` and `latest-*` also cannot move to a prerelease unless they already point to one, and `latest-*` cannot change its major.
28
28
 
29
29
  3. Click "Run workflow"
30
30
  4. Refresh the page to see the newly created workflow, and click it.
31
31
  5. The next screen shows "@username requested your review to deploy to npm-publish", click "Review deployments" and authorize your workflow run. **Never approve workflow runs you didn't initiaite.**
32
32
 
33
+ > [!NOTE]
34
+ > A CI dry run (`code-infra publish --ci --dry-run`) runs the same dist-tag check. On a branch that does not publish to `latest`, pass the dist-tag of the release to it, for example `--tag next` for prereleases or `--tag latest-v8` for a previous major.
35
+
33
36
  > [!IMPORTANT]
34
37
  > Go through the below steps if there is an error that says `The following packages are new and need to be published manually first` in the publish flow.
35
38
 
@@ -59,7 +62,7 @@ If publishing fails with npm asking for `otp`, run the command again with 6 digi
59
62
  pnpm code-infra publish-new-package --otp=123456
60
63
  ```
61
64
 
62
- 3. Go to the settings link for each package, e.g., `https://www.npmjs.com/package/<pkg-name>/access`, and setup `Trusted Publisher`.
65
+ 3. Go to the settings link for each package, for example, `https://www.npmjs.com/package/<pkg-name>/access`, and setup `Trusted Publisher`.
63
66
  4. In the `Select your publisher` step in the above link, click on the `GitHub Actions` button to configure GitHub Actions-based trusted publishing.
64
67
  5. Fill in the details of the repo -
65
68
  1. `Organization or user` as `mui`,
@@ -8,6 +8,14 @@ export type FetchCommitsOptions = {
8
8
  * - GitHub organization name, defaults to 'mui'
9
9
  */
10
10
  org?: string;
11
+ /**
12
+ * - Called as commits are listed and their pull request details are fetched
13
+ */
14
+ onProgress?: (progress: {
15
+ phase: string;
16
+ count: number;
17
+ total: number;
18
+ }) => void;
11
19
  };
12
20
  /**
13
21
  * @typedef {import('@octokit/rest').Octokit} OctokitType
@@ -21,17 +29,8 @@ export type FetchCommitsOptions = {
21
29
  * @property {string} lastRelease
22
30
  * @property {string} release
23
31
  * @property {string} [org='mui'] - GitHub organization name, defaults to 'mui'
32
+ * @property {(progress: { phase: string; count: number; total: number }) => void} [onProgress] - Called as commits are listed and their pull request details are fetched
24
33
  */
25
- /**
26
- * @param {Object} opts
27
- * @param {string} opts.cwd
28
- * @param {boolean} [opts.fetchAll=true] Whether to fetch all tags from all remotes before finding the latest tag.
29
- * @returns {Promise<string>}
30
- */
31
- export declare function findLatestTaggedVersion(opts: {
32
- cwd: string;
33
- fetchAll?: boolean;
34
- }): Promise<string>;
35
34
  /**
36
35
  * Fetches commits between two refs (lastRelease..release) including PR details.
37
36
  * Automatically handles GitHub OAuth authentication if none provided.
@@ -4,3 +4,4 @@
4
4
  export { generateChangelog } from './generateChangelog.mjs';
5
5
  export * from './loadChangelogConfig.mjs';
6
6
  export * from './fetchChangelogs.mjs';
7
+ export { findLatestTaggedVersion } from '../utils/git.mjs';
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ export type Args = {
3
+ /**
4
+ * - Branch pull requests fork from
5
+ */
6
+ baseBranch?: string;
7
+ /**
8
+ * - Remote the base branch is fetched from
9
+ */
10
+ remote?: string;
11
+ };
12
+ /**
13
+ * @typedef {Object} Args
14
+ * @property {string} [baseBranch] - Branch pull requests fork from
15
+ * @property {string} [remote] - Remote the base branch is fetched from
16
+ */
17
+ declare const _default: import('yargs').CommandModule<{}, Args>;
18
+ export default _default;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  export type Args = {
3
3
  /**
4
- * - List of workspace names to process
4
+ * - Workspace names or root-relative paths to process
5
5
  */
6
6
  workspaces: string[];
7
7
  /**
@@ -0,0 +1,50 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export type PropertyDefinition = import('@typescript-eslint/utils').TSESTree.PropertyDefinition;
3
+ export type Node = import('@typescript-eslint/utils').TSESTree.Node;
4
+ /**
5
+ * ESLint rule that disallows assigning a class field in the constructor when it is also
6
+ * declared as a (non-`declare`) class field.
7
+ *
8
+ * Babel 8 only strips TypeScript fields marked `declare` (see mui/material-ui#49241), so a
9
+ * field without an initializer is emitted as a property definition that sets it to
10
+ * `undefined` before the constructor assigns it again. Either initialize the field at its
11
+ * declaration, or mark it `declare` so it only serves as a type annotation.
12
+ *
13
+ * `declare` fields are exempt from TypeScript's strict property initialization check
14
+ * (TS2564), so prefer initializing at the declaration where the value doesn't depend on
15
+ * constructor arguments. The `declare` suggestion is only offered when every path through the
16
+ * constructor assigns the field, i.e. for an unconditional top-level assignment.
17
+ *
18
+ * Only plain `=` assignments count: compound assignments read the field rather than
19
+ * re-initialize it. A field with an initializer is only reported for an unconditional
20
+ * top-level assignment, as a conditional one overrides a default. Private (`#`) and decorated
21
+ * fields are ignored: they are runtime declarations that `declare` can't replace.
22
+ *
23
+ * @example
24
+ * // Invalid
25
+ * class ModalManager {
26
+ * private modals: Modal[];
27
+ * constructor() {
28
+ * this.modals = [];
29
+ * }
30
+ * }
31
+ *
32
+ * @example
33
+ * // Valid - initialized at the declaration
34
+ * class ModalManager {
35
+ * private modals: Modal[] = [];
36
+ * }
37
+ *
38
+ * @example
39
+ * // Valid - declared only, assigned in the constructor
40
+ * class ModalManager {
41
+ * declare private modals: Modal[];
42
+ * constructor(modals: Modal[]) {
43
+ * this.modals = modals;
44
+ * }
45
+ * }
46
+ */
47
+ declare const _default: ESLintUtils.RuleModule<"addDeclare" | "assignedInConstructor", [], unknown, ESLintUtils.RuleListener> & {
48
+ name: string;
49
+ };
50
+ export default _default;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Lints package.json files for publishing metadata. Published packages must
3
+ * declare an SPDX license, `"author": "MUI Team"` and a repository pointing at
4
+ * their own directory. Private packages must not carry publishing metadata.
5
+ * @returns {import('eslint').Linter.Config[]}
6
+ */
7
+ export declare function createPackageJsonConfig(): import('eslint').Linter.Config[];
@@ -1,5 +1,3 @@
1
- import remarkGfm from 'remark-gfm';
2
- import remarkLint from 'remark-lint';
3
1
  /**
4
2
  * Returns a remark preset wiring the MUI-authored remark-lint plugins together
5
3
  * with a curated set of community plugins. Drop this into `.remarkrc.mjs`:
@@ -23,21 +21,14 @@ import remarkLint from 'remark-lint';
23
21
  * ```
24
22
  *
25
23
  * @param {Object} [options]
24
+ * @param {boolean} [options.allowDefinitionComments=false] Allow titled `[//]` link-reference comments.
26
25
  * @param {Array<{ files: string | string[], rules: Record<string, false | unknown[]> }>} [options.overrides]
26
+ * @returns {import('unified').Preset}
27
27
  */
28
- export declare function createRemarkConfig({ overrides }?: {
28
+ export declare function createRemarkConfig({ overrides, allowDefinitionComments }?: {
29
+ allowDefinitionComments?: boolean;
29
30
  overrides?: Array<{
30
31
  files: string | string[];
31
32
  rules: Record<string, false | unknown[]>;
32
33
  }>;
33
- }): {
34
- settings: {
35
- bullet: string;
36
- emphasis: string;
37
- fence: string;
38
- listItemIndent: string;
39
- resourceLink: boolean;
40
- rule: string;
41
- };
42
- plugins: (any[] | typeof remarkGfm | typeof remarkLint)[];
43
- };
34
+ }): import('unified').Preset;
@@ -93,6 +93,20 @@ export declare function validatePkgJson(packageJson: Record<string, any>, option
93
93
  skipMainCheck?: boolean;
94
94
  enableReactCompiler?: boolean;
95
95
  }): void;
96
+ /**
97
+ * Validates that "engines.node" in package.json doesn't allow Node.js versions older than the
98
+ * "node" browserslist environment, which the CommonJS bundle is compiled for. Otherwise the
99
+ * build can emit syntax that the oldest supported Node.js version can't run.
100
+ * Targets are resolved the same way `@babel/preset-env` resolves them during the build.
101
+ * @param {{ name?: string; engines?: { node?: string } }} packageJson
102
+ * @param {string} cwd - The package directory, used to find the browserslist config.
103
+ */
104
+ export declare function validateEnginesNode(packageJson: {
105
+ name?: string;
106
+ engines?: {
107
+ node?: string;
108
+ };
109
+ }, cwd: string): void;
96
110
  /**
97
111
  * Marks the start and end of a function execution for performance measurement.
98
112
  * Uses the Performance API to create marks and measure the duration.
@@ -8,9 +8,9 @@
8
8
  export declare const KEYRING_SERVICE = "mui-code-infra";
9
9
  /**
10
10
  * @param {string} key
11
- * @returns {Promise<string | undefined>}
11
+ * @returns {Promise<string | null>}
12
12
  */
13
- export declare function getPassword(key: string): Promise<string | undefined>;
13
+ export declare function getPassword(key: string): Promise<string | null>;
14
14
  /**
15
15
  * @param {string} key
16
16
  * @param {string} password
@@ -13,22 +13,18 @@ export type RepoInfo = {
13
13
  remoteName: string;
14
14
  };
15
15
  /**
16
- * @typedef {Object} RepoInfo
17
- * @property {string} owner - Repository owner
18
- * @property {string} repo - Repository name
19
- * @property {string} remoteName - Remote name
20
- */
21
- /**
22
- * Get current repository info from git remote
16
+ * The canonical repository's remote, as opposed to a fork's: `upstream`, else `origin`, each only if
17
+ * it points at a mui repository. Reads the configured URLs, which `insteadOf` does not rewrite.
23
18
  * @param {string} [cwd=process.cwd()]
24
- * @returns {Promise<RepoInfo>} Repository owner and name
19
+ * @returns {Promise<RepoInfo>}
25
20
  */
26
21
  export declare function getRepositoryInfo(cwd?: string): Promise<RepoInfo>;
27
22
  /**
28
23
  * Get current git SHA
24
+ * @param {string} [cwd=process.cwd()]
29
25
  * @returns {Promise<string>} Current git commit SHA
30
26
  */
31
- export declare function getCurrentGitSha(): Promise<string>;
27
+ export declare function getCurrentGitSha(cwd?: string): Promise<string>;
32
28
  /**
33
29
  * Check whether a tag already exists on the origin remote
34
30
  * @param {string} tagName - Tag name to check (e.g. `v1.2.3`)
@@ -36,3 +32,28 @@ export declare function getCurrentGitSha(): Promise<string>;
36
32
  * @returns {Promise<boolean>} True if the tag exists on origin
37
33
  */
38
34
  export declare function remoteGitTagExists(tagName: string, cwd?: string): Promise<boolean>;
35
+ /**
36
+ * The commit HEAD should be compared against: where it forked from the base branch, or its parent
37
+ * when HEAD is already on the base branch.
38
+ * @param {Object} [options]
39
+ * @param {string} [options.cwd]
40
+ * @param {string} [options.remote] - Defaults to the canonical remote.
41
+ * @param {string} [options.baseBranch] - Defaults to the remote's default branch.
42
+ * @returns {Promise<string>}
43
+ */
44
+ export declare function resolveBaseline(options?: {
45
+ cwd?: string;
46
+ remote?: string;
47
+ baseBranch?: string;
48
+ }): Promise<string>;
49
+ /**
50
+ * The most recent version tag reachable from HEAD — where a changelog's range starts.
51
+ * @param {Object} opts
52
+ * @param {string} opts.cwd
53
+ * @param {boolean} [opts.fetchAll=true] Whether to fetch all tags from all remotes before finding the latest tag.
54
+ * @returns {Promise<string>}
55
+ */
56
+ export declare function findLatestTaggedVersion(opts: {
57
+ cwd: string;
58
+ fetchAll?: boolean;
59
+ }): Promise<string>;
@@ -47,3 +47,11 @@ export declare function endToEndGhAuthGetToken({ log }?: {
47
47
  * @returns {Promise<void>}
48
48
  */
49
49
  export declare function clearGitHubAuth(): Promise<void>;
50
+ /**
51
+ * Returns when a rate-limited request may be retried, or `null` for other errors.
52
+ * https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#exceeding-the-rate-limit
53
+ *
54
+ * @param {unknown} error
55
+ * @returns {Date | null}
56
+ */
57
+ export declare function getRateLimitRetryTime(error: unknown): Date | null;
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ /** Version that `publish-new-package` publishes to reserve the name of a new package. */
3
+ export declare const PLACEHOLDER_VERSION = "0.0.1";
2
4
  export type PrivatePackage = {
3
5
  /**
4
6
  * - Package name
@@ -134,6 +136,53 @@ export declare function getReleaseVersion(cwd?: string): Promise<string | null>;
134
136
  * @returns {Promise<PublicPackage[]>} The subset needing a manual first publish
135
137
  */
136
138
  export declare function getPackagesNeedingManualPublish(packages: PublicPackage[]): Promise<PublicPackage[]>;
139
+ export type RegistryPackage = {
140
+ /**
141
+ * - Dist-tags of the package
142
+ */
143
+ distTags: Record<string, string>;
144
+ /**
145
+ * - Published versions of the package
146
+ */
147
+ versions: string[];
148
+ };
149
+ /**
150
+ * @typedef {Object} RegistryPackage
151
+ * @property {Record<string, string>} distTags - Dist-tags of the package
152
+ * @property {string[]} versions - Published versions of the package
153
+ */
154
+ /**
155
+ * Read the dist-tags and the published versions of each package with `pnpm view`, so
156
+ * the lookup uses the same registry configuration and credentials as `pnpm publish`.
157
+ *
158
+ * @param {PublicPackage[]} packages - Packages to read
159
+ * @returns {Promise<Map<string, RegistryPackage | null>>} By package name, null for a package that is not on the registry
160
+ */
161
+ export declare function getRegistryPackages(packages: PublicPackage[]): Promise<Map<string, RegistryPackage | null>>;
162
+ export type DistTagConflict = {
163
+ /**
164
+ * - Package to publish
165
+ */
166
+ pkg: PublicPackage;
167
+ /**
168
+ * - Why the dist-tag would move to the wrong release line
169
+ */
170
+ reason: string;
171
+ };
172
+ /**
173
+ * @typedef {Object} DistTagConflict
174
+ * @property {PublicPackage} pkg - Package to publish
175
+ * @property {string} reason - Why the dist-tag would move to the wrong release line
176
+ */
177
+ /**
178
+ * Find the packages whose publish with `tag` would move the dist-tag to the wrong release line.
179
+ *
180
+ * @param {PublicPackage[]} packages - Packages to publish
181
+ * @param {string} tag - Dist-tag to publish with
182
+ * @param {Map<string, RegistryPackage | null>} registryPackages - Registry data from {@link getRegistryPackages}
183
+ * @returns {DistTagConflict[]}
184
+ */
185
+ export declare function getDistTagConflicts(packages: PublicPackage[], tag: string, registryPackages: Map<string, RegistryPackage | null>): DistTagConflict[];
137
186
  /**
138
187
  * Get package version info from registry.
139
188
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-code-infra",
3
- "version": "0.1.1-canary.2",
3
+ "version": "0.1.1-canary.21",
4
4
  "author": "MUI Team",
5
5
  "description": "Infra scripts and configs to be used across MUI repos.",
6
6
  "license": "MIT",
@@ -53,42 +53,45 @@
53
53
  "code-infra": "./bin/code-infra.mjs"
54
54
  },
55
55
  "dependencies": {
56
- "@argos-ci/core": "6.8.4",
57
- "@babel/cli": "^8.0.5",
58
- "@babel/core": "^8.0.5",
56
+ "@argos-ci/core": "6.8.8",
57
+ "@babel/cli": "^8.0.6",
58
+ "@babel/core": "^8.0.7",
59
+ "@babel/helper-compilation-targets": "^8.0.7",
59
60
  "@babel/plugin-syntax-jsx": "^8.0.1",
60
- "@babel/plugin-syntax-typescript": "^8.0.1",
61
+ "@babel/plugin-syntax-typescript": "^8.0.3",
61
62
  "@babel/plugin-transform-react-pure-annotations": "^8.0.1",
62
- "@babel/plugin-transform-runtime": "^8.0.1",
63
- "@babel/preset-env": "^8.0.5",
63
+ "@babel/plugin-transform-runtime": "^8.0.6",
64
+ "@babel/preset-env": "^8.0.7",
64
65
  "@babel/preset-react": "^8.0.1",
65
- "@babel/preset-typescript": "^8.0.1",
66
+ "@babel/preset-typescript": "^8.0.7",
66
67
  "@eslint/compat": "^2.1.1",
67
68
  "@eslint/js": "^10.0.1",
68
- "@eslint/json": "^2.1.0",
69
- "@inquirer/confirm": "^6.3.2",
70
- "@inquirer/select": "^5.2.5",
71
- "@mui/internal-babel-plugin-display-name": "2.0.1-canary.0",
72
- "@mui/internal-babel-plugin-minify-errors": "3.0.1-canary.0",
73
- "@mui/internal-babel-plugin-resolve-imports": "3.0.1-canary.0",
74
- "@napi-rs/keyring": "^2.0.0",
69
+ "@eslint/json": "^2.1.1",
70
+ "@inquirer/confirm": "^6.3.3",
71
+ "@inquirer/select": "^5.2.6",
72
+ "@mui/internal-babel-plugin-display-name": "2.0.1-canary.2",
73
+ "@mui/internal-babel-plugin-minify-errors": "3.0.1-canary.2",
74
+ "@mui/internal-babel-plugin-resolve-imports": "3.0.1-canary.3",
75
+ "@napi-rs/keyring": "^2.1.1",
75
76
  "@octokit/auth-action": "^6.0.3",
76
77
  "@octokit/oauth-methods": "^6.0.5",
78
+ "@octokit/plugin-retry": "^8.1.1",
79
+ "@octokit/plugin-throttling": "^11.0.5",
77
80
  "@octokit/rest": "^22.0.1",
78
- "@pnpm/config.version-policy": "^1100.2.3",
81
+ "@pnpm/config.version-policy": "^1100.2.8",
79
82
  "@pnpm/find-workspace-dir": "^1000.1.5",
80
83
  "@pnpm/parse-wanted-dependency": "^1001.0.0",
81
- "@typescript-eslint/types": "^8.70.0",
82
- "@typescript-eslint/utils": "^8.70.0",
84
+ "@typescript-eslint/types": "^8.71.2",
85
+ "@typescript-eslint/utils": "^8.71.2",
83
86
  "@vitest/eslint-plugin": "^1.6.27",
84
87
  "babel-plugin-optimize-clsx": "^2.6.2",
85
88
  "babel-plugin-react-compiler": "^1.0.0",
86
- "babel-plugin-transform-import-meta": "^3.0.0",
89
+ "babel-plugin-transform-import-meta": "^3.1.0",
87
90
  "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
88
91
  "babel-plugin-transform-remove-imports": "^1.8.1",
89
- "chalk": "^6.0.0",
92
+ "chalk": "^6.0.1",
90
93
  "clipboardy": "^5.3.2",
91
- "content-type": "^3.1.0",
94
+ "content-type": "^3.1.1",
92
95
  "env-ci": "^11.2.0",
93
96
  "es-toolkit": "^1.52.0",
94
97
  "eslint-config-prettier": "^10.1.8",
@@ -98,19 +101,20 @@
98
101
  "eslint-plugin-import": "^2.32.0",
99
102
  "eslint-plugin-jsx-a11y": "^6.10.2",
100
103
  "eslint-plugin-mdx": "^3.8.1",
104
+ "eslint-plugin-package-json": "^1.10.1",
101
105
  "eslint-plugin-react": "^7.37.5",
102
106
  "eslint-plugin-react-compiler": "^19.1.0-rc.2",
103
107
  "eslint-plugin-react-hooks": "^7.1.1",
104
108
  "eslint-plugin-testing-library": "^7.16.2",
105
- "execa": "^10.0.1",
109
+ "execa": "^10.1.1",
106
110
  "git-url-parse": "^16.1.0",
107
- "globals": "^17.12.0",
111
+ "globals": "^17.13.0",
108
112
  "globby": "^16.2.4",
109
- "html-validate": "^11.15.0",
113
+ "html-validate": "^11.16.2",
110
114
  "minimatch": "^10.2.6",
111
115
  "node-html-parser": "^9.0.4",
112
- "open": "^11.0.3",
113
- "postcss-styled-syntax": "^0.7.2",
116
+ "open": "^11.0.4",
117
+ "postcss-styled-syntax": "^0.7.4",
114
118
  "regexp.escape": "^2.0.1",
115
119
  "rehype-slug": "^6.0.0",
116
120
  "rehype-stringify": "^10.0.1",
@@ -133,12 +137,12 @@
133
137
  "resolve-pkg-maps": "^1.0.0",
134
138
  "semver": "^7.8.5",
135
139
  "stylelint-config-standard": "^40.0.0",
136
- "typescript-eslint": "^8.70.0",
140
+ "typescript-eslint": "^8.71.2",
137
141
  "unified": "^11.0.5",
138
142
  "unified-lint-rule": "^3.0.1",
139
143
  "unist-util-visit": "^5.1.0",
140
144
  "yaml": "^2.9.1",
141
- "yargs": "^18.1.0"
145
+ "yargs": "^18.2.0"
142
146
  },
143
147
  "peerDependencies": {
144
148
  "@next/eslint-plugin-next": "*",
@@ -162,17 +166,16 @@
162
166
  "@types/mdast": "4.0.4",
163
167
  "@types/regexp.escape": "2.0.0",
164
168
  "@types/yargs": "17.0.35",
165
- "@typescript-eslint/parser": "8.70.0",
166
- "@typescript-eslint/rule-tester": "8.70.0",
169
+ "@typescript-eslint/parser": "8.71.2",
170
+ "@typescript-eslint/rule-tester": "8.71.2",
167
171
  "@typescript/native": "npm:typescript@7.0.2",
168
- "eslint": "10.10.0",
172
+ "eslint": "10.12.0",
169
173
  "get-port": "7.2.0",
170
- "prettier": "3.9.6",
174
+ "prettier": "3.9.10",
171
175
  "remark": "15.0.1",
172
176
  "remark-mdx": "3.1.1",
173
177
  "serve": "14.2.6",
174
- "typescript": "npm:@typescript/typescript6@6.0.2",
175
- "typescript-eslint": "8.70.0"
178
+ "typescript": "npm:@typescript/typescript6@6.0.2"
176
179
  },
177
180
  "files": [
178
181
  "bin",
@@ -187,7 +190,7 @@
187
190
  "publishConfig": {
188
191
  "access": "public"
189
192
  },
190
- "gitSha": "342a1159c5c25a48c8ed853893146aa8701574a6",
193
+ "gitSha": "e8aec3f286672d4639af7b385ef313c3d8ec74f3",
191
194
  "scripts": {
192
195
  "build": "tsc -p tsconfig.build.json",
193
196
  "typescript": "tsc -noEmit",
@@ -1,9 +1,25 @@
1
+ import { retry } from '@octokit/plugin-retry';
2
+ import { throttling } from '@octokit/plugin-throttling';
1
3
  import { Octokit } from '@octokit/rest';
2
- import { $ } from 'execa';
4
+ import { mapAsync } from 'es-toolkit/array';
3
5
 
4
- import { getRepositoryInfo } from '../utils/git.mjs';
5
6
  import { persistentAuthStrategy } from '../utils/github.mjs';
6
7
 
8
+ const ThrottledOctokit = Octokit.plugin(throttling, retry);
9
+
10
+ /**
11
+ * Waits out short rate limits. Longer ones fail with the original error.
12
+ *
13
+ * @param {number} retryAfter - Seconds until the request may be retried
14
+ * @param {unknown} options
15
+ * @param {unknown} octokit
16
+ * @param {number} retryCount
17
+ * @returns {boolean}
18
+ */
19
+ function shouldRetryRateLimit(retryAfter, options, octokit, retryCount) {
20
+ return retryAfter <= 60 && retryCount < 3;
21
+ }
22
+
7
23
  /**
8
24
  * @typedef {import('@octokit/rest').Octokit} OctokitType
9
25
  */
@@ -18,27 +34,9 @@ import { persistentAuthStrategy } from '../utils/github.mjs';
18
34
  * @property {string} lastRelease
19
35
  * @property {string} release
20
36
  * @property {string} [org='mui'] - GitHub organization name, defaults to 'mui'
37
+ * @property {(progress: { phase: string; count: number; total: number }) => void} [onProgress] - Called as commits are listed and their pull request details are fetched
21
38
  */
22
39
 
23
- /**
24
- * @param {Object} opts
25
- * @param {string} opts.cwd
26
- * @param {boolean} [opts.fetchAll=true] Whether to fetch all tags from all remotes before finding the latest tag.
27
- * @returns {Promise<string>}
28
- */
29
- export async function findLatestTaggedVersion(opts) {
30
- const $$ = $({ cwd: opts.cwd });
31
- const fetchAll = opts.fetchAll ?? true;
32
- if (fetchAll) {
33
- const { remoteName } = await getRepositoryInfo();
34
- // Fetch all tags from the mui remote to ensure we have the latest tags.
35
- // --force to update any existing tags that may have changed to avoid the clobering error.
36
- await $$`git fetch --tags --force ${remoteName}`;
37
- }
38
- const { stdout } = await $$`git describe --tags --abbrev=0 --match ${'v*'}`; // only include "version-tags"
39
- return stdout.trim();
40
- }
41
-
42
40
  /**
43
41
  * Fetches commits between two refs (lastRelease..release) including PR details.
44
42
  * Automatically handles GitHub OAuth authentication if none provided.
@@ -50,7 +48,13 @@ export async function fetchCommitsBetweenRefs(opts) {
50
48
  const octokit =
51
49
  'octokit' in opts && opts.octokit
52
50
  ? opts.octokit
53
- : new Octokit({ authStrategy: persistentAuthStrategy });
51
+ : new ThrottledOctokit({
52
+ authStrategy: persistentAuthStrategy,
53
+ throttle: {
54
+ onRateLimit: shouldRetryRateLimit,
55
+ onSecondaryRateLimit: shouldRetryRateLimit,
56
+ },
57
+ });
54
58
 
55
59
  return fetchCommitsRest({
56
60
  octokit,
@@ -58,6 +62,7 @@ export async function fetchCommitsBetweenRefs(opts) {
58
62
  lastRelease: opts.lastRelease,
59
63
  release: opts.release,
60
64
  org: opts.org ?? 'mui',
65
+ onProgress: opts.onProgress,
61
66
  });
62
67
  }
63
68
 
@@ -70,7 +75,7 @@ export async function fetchCommitsBetweenRefs(opts) {
70
75
  *
71
76
  * @returns {Promise<FetchedCommitDetails[]>}
72
77
  */
73
- async function fetchCommitsRest({ octokit, repo, lastRelease, release, org = 'mui' }) {
78
+ async function fetchCommitsRest({ octokit, repo, lastRelease, release, org = 'mui', onProgress }) {
74
79
  /**
75
80
  * @typedef {Awaited<ReturnType<Octokit['repos']['compareCommits']>>['data']['commits']} Commits
76
81
  */
@@ -90,9 +95,19 @@ async function fetchCommitsRest({ octokit, repo, lastRelease, release, org = 'mu
90
95
  );
91
96
  for await (const response of timeline) {
92
97
  results.push(...response.data.commits);
98
+ onProgress?.({
99
+ phase: 'Listing commits',
100
+ count: results.length,
101
+ total: response.data.total_commits,
102
+ });
93
103
  }
94
104
 
95
- const promises = results.map(async (commit) => {
105
+ /**
106
+ * @param {Commits[number]} commit
107
+ * @param {AbortSignal} signal
108
+ * @returns {Promise<FetchedCommitDetails | null>}
109
+ */
110
+ const fetchCommitDetails = async (commit, signal) => {
96
111
  const matches = [...commit.commit.message.matchAll(/#(\d+)/g)];
97
112
  // The PR number is always the last match.
98
113
  // Sometimes the PR titles include an issue number like this:
@@ -111,6 +126,7 @@ async function fetchCommitsRest({ octokit, repo, lastRelease, release, org = 'mu
111
126
  headers: {
112
127
  Accept: 'application/vnd.github.text+json',
113
128
  },
129
+ request: { signal },
114
130
  });
115
131
 
116
132
  const labels = pr.data.labels.map((label) => label.name);
@@ -132,9 +148,33 @@ async function fetchCommitsRest({ octokit, repo, lastRelease, release, org = 'mu
132
148
  prTitle: pr.data.title,
133
149
  prBody: pr.data.body,
134
150
  });
135
- });
151
+ };
152
+
153
+ // `mapAsync` rejects on the first error but still runs every queued callback.
154
+ // Aborted once it settles, which cancels requests in flight and skips the ones not started yet.
155
+ const controller = new AbortController();
136
156
 
137
- return (await Promise.all(promises)).filter((entry) => entry !== null);
157
+ let fetched = 0;
158
+ try {
159
+ const commits = await mapAsync(
160
+ results,
161
+ async (commit) => {
162
+ if (controller.signal.aborted) {
163
+ return null;
164
+ }
165
+ const details = await fetchCommitDetails(commit, controller.signal);
166
+ if (!controller.signal.aborted) {
167
+ fetched += 1;
168
+ onProgress?.({ phase: 'Fetching pull requests', count: fetched, total: results.length });
169
+ }
170
+ return details;
171
+ },
172
+ { concurrency: 10 },
173
+ );
174
+ return commits.filter((entry) => entry !== null);
175
+ } finally {
176
+ controller.abort();
177
+ }
138
178
  }
139
179
 
140
180
  /**