@mui/internal-code-infra 0.1.1-canary.1 → 0.1.1-canary.10
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/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/eslint/mui/rules/no-constructor-assigned-field.d.mts +50 -0
- package/build/utils/git.d.mts +30 -9
- package/build/utils/github.d.mts +8 -0
- package/package.json +30 -28
- package/src/changelog/fetchChangelogs.mjs +66 -26
- package/src/changelog/index.mjs +3 -0
- package/src/cli/cmdBaseline.mjs +31 -0
- package/src/cli/cmdPublish.mjs +7 -1
- package/src/cli/index.mjs +2 -0
- 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/testConfig.mjs +1 -2
- package/src/utils/git.mjs +107 -46
- package/src/utils/github.mjs +27 -0
|
@@ -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.
|
|
@@ -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;
|
|
@@ -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;
|
package/build/utils/git.d.mts
CHANGED
|
@@ -13,22 +13,18 @@ export type RepoInfo = {
|
|
|
13
13
|
remoteName: string;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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>}
|
|
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>;
|
package/build/utils/github.d.mts
CHANGED
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.1.1-canary.
|
|
3
|
+
"version": "0.1.1-canary.10",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"code-infra": "./bin/code-infra.mjs"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@argos-ci/core": "6.8.
|
|
57
|
-
"@babel/cli": "^8.0.
|
|
58
|
-
"@babel/core": "^8.0.
|
|
56
|
+
"@argos-ci/core": "6.8.6",
|
|
57
|
+
"@babel/cli": "^8.0.6",
|
|
58
|
+
"@babel/core": "^8.0.6",
|
|
59
59
|
"@babel/plugin-syntax-jsx": "^8.0.1",
|
|
60
|
-
"@babel/plugin-syntax-typescript": "^8.0.
|
|
60
|
+
"@babel/plugin-syntax-typescript": "^8.0.3",
|
|
61
61
|
"@babel/plugin-transform-react-pure-annotations": "^8.0.1",
|
|
62
|
-
"@babel/plugin-transform-runtime": "^8.0.
|
|
63
|
-
"@babel/preset-env": "^8.0.
|
|
62
|
+
"@babel/plugin-transform-runtime": "^8.0.6",
|
|
63
|
+
"@babel/preset-env": "^8.0.6",
|
|
64
64
|
"@babel/preset-react": "^8.0.1",
|
|
65
65
|
"@babel/preset-typescript": "^8.0.1",
|
|
66
66
|
"@eslint/compat": "^2.1.1",
|
|
@@ -68,27 +68,29 @@
|
|
|
68
68
|
"@eslint/json": "^2.1.0",
|
|
69
69
|
"@inquirer/confirm": "^6.3.2",
|
|
70
70
|
"@inquirer/select": "^5.2.5",
|
|
71
|
-
"@mui/internal-babel-plugin-display-name": "2.0.1-canary.
|
|
72
|
-
"@mui/internal-babel-plugin-minify-errors": "3.0.1-canary.
|
|
73
|
-
"@mui/internal-babel-plugin-resolve-imports": "3.0.1-canary.
|
|
74
|
-
"@napi-rs/keyring": "^2.
|
|
71
|
+
"@mui/internal-babel-plugin-display-name": "2.0.1-canary.1",
|
|
72
|
+
"@mui/internal-babel-plugin-minify-errors": "3.0.1-canary.1",
|
|
73
|
+
"@mui/internal-babel-plugin-resolve-imports": "3.0.1-canary.2",
|
|
74
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
75
75
|
"@octokit/auth-action": "^6.0.3",
|
|
76
76
|
"@octokit/oauth-methods": "^6.0.5",
|
|
77
|
+
"@octokit/plugin-retry": "^8.1.1",
|
|
78
|
+
"@octokit/plugin-throttling": "^11.0.5",
|
|
77
79
|
"@octokit/rest": "^22.0.1",
|
|
78
|
-
"@pnpm/config.version-policy": "^1100.2.
|
|
80
|
+
"@pnpm/config.version-policy": "^1100.2.5",
|
|
79
81
|
"@pnpm/find-workspace-dir": "^1000.1.5",
|
|
80
82
|
"@pnpm/parse-wanted-dependency": "^1001.0.0",
|
|
81
|
-
"@typescript-eslint/types": "^8.
|
|
82
|
-
"@typescript-eslint/utils": "^8.
|
|
83
|
+
"@typescript-eslint/types": "^8.71.0",
|
|
84
|
+
"@typescript-eslint/utils": "^8.71.0",
|
|
83
85
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
84
86
|
"babel-plugin-optimize-clsx": "^2.6.2",
|
|
85
87
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
86
88
|
"babel-plugin-transform-import-meta": "^3.0.0",
|
|
87
89
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
|
88
90
|
"babel-plugin-transform-remove-imports": "^1.8.1",
|
|
89
|
-
"chalk": "^6.0.
|
|
91
|
+
"chalk": "^6.0.1",
|
|
90
92
|
"clipboardy": "^5.3.2",
|
|
91
|
-
"content-type": "^3.1.
|
|
93
|
+
"content-type": "^3.1.1",
|
|
92
94
|
"env-ci": "^11.2.0",
|
|
93
95
|
"es-toolkit": "^1.52.0",
|
|
94
96
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -104,13 +106,13 @@
|
|
|
104
106
|
"eslint-plugin-testing-library": "^7.16.2",
|
|
105
107
|
"execa": "^10.0.1",
|
|
106
108
|
"git-url-parse": "^16.1.0",
|
|
107
|
-
"globals": "^17.
|
|
109
|
+
"globals": "^17.13.0",
|
|
108
110
|
"globby": "^16.2.4",
|
|
109
|
-
"html-validate": "^11.
|
|
111
|
+
"html-validate": "^11.16.1",
|
|
110
112
|
"minimatch": "^10.2.6",
|
|
111
113
|
"node-html-parser": "^9.0.4",
|
|
112
|
-
"open": "^11.0.
|
|
113
|
-
"postcss-styled-syntax": "^0.7.
|
|
114
|
+
"open": "^11.0.4",
|
|
115
|
+
"postcss-styled-syntax": "^0.7.3",
|
|
114
116
|
"regexp.escape": "^2.0.1",
|
|
115
117
|
"rehype-slug": "^6.0.0",
|
|
116
118
|
"rehype-stringify": "^10.0.1",
|
|
@@ -133,12 +135,12 @@
|
|
|
133
135
|
"resolve-pkg-maps": "^1.0.0",
|
|
134
136
|
"semver": "^7.8.5",
|
|
135
137
|
"stylelint-config-standard": "^40.0.0",
|
|
136
|
-
"typescript-eslint": "^8.
|
|
138
|
+
"typescript-eslint": "^8.71.0",
|
|
137
139
|
"unified": "^11.0.5",
|
|
138
140
|
"unified-lint-rule": "^3.0.1",
|
|
139
141
|
"unist-util-visit": "^5.1.0",
|
|
140
142
|
"yaml": "^2.9.1",
|
|
141
|
-
"yargs": "^18.
|
|
143
|
+
"yargs": "^18.2.0"
|
|
142
144
|
},
|
|
143
145
|
"peerDependencies": {
|
|
144
146
|
"@next/eslint-plugin-next": "*",
|
|
@@ -162,17 +164,17 @@
|
|
|
162
164
|
"@types/mdast": "4.0.4",
|
|
163
165
|
"@types/regexp.escape": "2.0.0",
|
|
164
166
|
"@types/yargs": "17.0.35",
|
|
165
|
-
"@typescript-eslint/parser": "8.
|
|
166
|
-
"@typescript-eslint/rule-tester": "8.
|
|
167
|
+
"@typescript-eslint/parser": "8.71.0",
|
|
168
|
+
"@typescript-eslint/rule-tester": "8.71.0",
|
|
167
169
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
168
|
-
"eslint": "10.
|
|
170
|
+
"eslint": "10.11.0",
|
|
169
171
|
"get-port": "7.2.0",
|
|
170
|
-
"prettier": "3.9.
|
|
172
|
+
"prettier": "3.9.9",
|
|
171
173
|
"remark": "15.0.1",
|
|
172
174
|
"remark-mdx": "3.1.1",
|
|
173
175
|
"serve": "14.2.6",
|
|
174
176
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
175
|
-
"typescript-eslint": "8.
|
|
177
|
+
"typescript-eslint": "8.71.0"
|
|
176
178
|
},
|
|
177
179
|
"files": [
|
|
178
180
|
"bin",
|
|
@@ -187,7 +189,7 @@
|
|
|
187
189
|
"publishConfig": {
|
|
188
190
|
"access": "public"
|
|
189
191
|
},
|
|
190
|
-
"gitSha": "
|
|
192
|
+
"gitSha": "46d7844970a13df535908a1f3f0d353d992feb87",
|
|
191
193
|
"scripts": {
|
|
192
194
|
"build": "tsc -p tsconfig.build.json",
|
|
193
195
|
"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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
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/cmdPublish.mjs
CHANGED
|
@@ -30,7 +30,13 @@ import { getCurrentGitSha, getRepositoryInfo, remoteGitTagExists } from '../util
|
|
|
30
30
|
const isCI = envCI().isCi;
|
|
31
31
|
|
|
32
32
|
function getOctokit() {
|
|
33
|
-
|
|
33
|
+
const octokit = new Octokit({
|
|
34
|
+
authStrategy: isCI ? createActionAuth : persistentAuthStrategy,
|
|
35
|
+
});
|
|
36
|
+
octokit.hook.before('request', (requestOptions) => {
|
|
37
|
+
requestOptions.headers['x-github-api-version'] = '2026-03-10';
|
|
38
|
+
});
|
|
39
|
+
return octokit;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
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)
|
|
@@ -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
|
+
});
|
|
@@ -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/utils/git.mjs
CHANGED
|
@@ -9,68 +9,63 @@ import gitUrlParse from 'git-url-parse';
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* The repository a remote URL points at, if it is a GitHub repository under `mui`.
|
|
13
|
+
* @param {string} url
|
|
14
|
+
* @returns {{ owner: string, repo: string } | null}
|
|
15
|
+
*/
|
|
16
|
+
function muiRepositoryOf(url) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = gitUrlParse(url);
|
|
19
|
+
return parsed.source === 'github.com' && parsed.owner === 'mui'
|
|
20
|
+
? { owner: parsed.owner, repo: parsed.name }
|
|
21
|
+
: null;
|
|
22
|
+
} catch {
|
|
23
|
+
// Not a URL git-url-parse understands, such as a local path: not a mui repository either.
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The canonical repository's remote, as opposed to a fork's: `upstream`, else `origin`, each only if
|
|
30
|
+
* it points at a mui repository. Reads the configured URLs, which `insteadOf` does not rewrite.
|
|
13
31
|
* @param {string} [cwd=process.cwd()]
|
|
14
|
-
* @returns {Promise<RepoInfo>}
|
|
32
|
+
* @returns {Promise<RepoInfo>}
|
|
15
33
|
*/
|
|
16
34
|
export async function getRepositoryInfo(cwd = process.cwd()) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @type {Map<string, { owner: string, repo: string }>}
|
|
29
|
-
*/
|
|
30
|
-
const validRemotes = new Map();
|
|
31
|
-
|
|
32
|
-
for (const line of lines) {
|
|
33
|
-
// Match pattern: "remoteName url (fetch|push)"
|
|
34
|
-
const [remoteName, url, type] = line.trim().split(/\s+/, 3);
|
|
35
|
-
repoRemotes.add(remoteName);
|
|
36
|
-
if (type === '(fetch)') {
|
|
37
|
-
try {
|
|
38
|
-
const parsed = gitUrlParse(url);
|
|
39
|
-
if (parsed.source !== 'github.com' || parsed.owner !== 'mui') {
|
|
40
|
-
cause[remoteName] = `Remote is not a GitHub repository under 'mui' organization: ${url}`;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (!validRemotes.has(remoteName)) {
|
|
44
|
-
validRemotes.set(remoteName, { owner: parsed.owner, repo: parsed.name });
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
cause[remoteName] = `Failed to parse URL for remote ${remoteName}: ${url}`;
|
|
48
|
-
}
|
|
49
|
-
} else if (type !== '(push)') {
|
|
50
|
-
throw new Error(`Unexpected line format for "git remote -v": "${line}"`);
|
|
51
|
-
}
|
|
35
|
+
// Exits 1 when neither remote is configured; the error below reports that.
|
|
36
|
+
const { stdout } = await $({
|
|
37
|
+
cwd,
|
|
38
|
+
reject: false,
|
|
39
|
+
})`git config --get-regexp ${'^remote\\.(upstream|origin)\\.url$'}`;
|
|
40
|
+
/** @type {Map<string, string>} */
|
|
41
|
+
const urls = new Map();
|
|
42
|
+
for (const line of stdout.split('\n').filter(Boolean)) {
|
|
43
|
+
const [key, url] = line.split(' ', 2);
|
|
44
|
+
urls.set(key.slice('remote.'.length, -'.url'.length), url);
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
return { ...
|
|
47
|
+
for (const remoteName of ['upstream', 'origin']) {
|
|
48
|
+
const url = urls.get(remoteName);
|
|
49
|
+
const repository = url ? muiRepositoryOf(url) : null;
|
|
50
|
+
if (repository) {
|
|
51
|
+
return { ...repository, remoteName };
|
|
59
52
|
}
|
|
60
53
|
}
|
|
61
54
|
|
|
55
|
+
const found = [...urls].map(([name, url]) => `${name}: ${url}`).join(', ') || 'none';
|
|
62
56
|
throw new Error(
|
|
63
|
-
`
|
|
64
|
-
|
|
57
|
+
`No canonical remote: neither \`upstream\` nor \`origin\` points at a mui repository (${found}). ` +
|
|
58
|
+
'Add an `upstream` remote for it, or name the remote explicitly where the command allows it.',
|
|
65
59
|
);
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
/**
|
|
69
63
|
* Get current git SHA
|
|
64
|
+
* @param {string} [cwd=process.cwd()]
|
|
70
65
|
* @returns {Promise<string>} Current git commit SHA
|
|
71
66
|
*/
|
|
72
|
-
export async function getCurrentGitSha() {
|
|
73
|
-
const result = await
|
|
67
|
+
export async function getCurrentGitSha(cwd = process.cwd()) {
|
|
68
|
+
const result = await $({ cwd })`git rev-parse HEAD`;
|
|
74
69
|
return result.stdout.trim();
|
|
75
70
|
}
|
|
76
71
|
|
|
@@ -84,3 +79,69 @@ export async function remoteGitTagExists(tagName, cwd = process.cwd()) {
|
|
|
84
79
|
const { stdout } = await $({ cwd })`git ls-remote --tags origin refs/tags/${tagName}`;
|
|
85
80
|
return stdout.trim().length > 0;
|
|
86
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The default branch of `remote`, as the remote itself reports it.
|
|
85
|
+
* @param {string} remote
|
|
86
|
+
* @param {string} cwd
|
|
87
|
+
* @returns {Promise<string>}
|
|
88
|
+
*/
|
|
89
|
+
async function getDefaultBranchOf(remote, cwd) {
|
|
90
|
+
const { stdout } = await $({ cwd })`git ls-remote --symref ${remote} HEAD`;
|
|
91
|
+
const match = /^ref: refs\/heads\/(\S+)\tHEAD$/m.exec(stdout);
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new Error(`Remote "${remote}" does not report a default branch.`);
|
|
94
|
+
}
|
|
95
|
+
return match[1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The commit HEAD should be compared against: where it forked from the base branch, or its parent
|
|
100
|
+
* when HEAD is already on the base branch.
|
|
101
|
+
* @param {Object} [options]
|
|
102
|
+
* @param {string} [options.cwd]
|
|
103
|
+
* @param {string} [options.remote] - Defaults to the canonical remote.
|
|
104
|
+
* @param {string} [options.baseBranch] - Defaults to the remote's default branch.
|
|
105
|
+
* @returns {Promise<string>}
|
|
106
|
+
*/
|
|
107
|
+
export async function resolveBaseline(options = {}) {
|
|
108
|
+
const cwd = options.cwd ?? process.cwd();
|
|
109
|
+
const remote = options.remote ?? (await getRepositoryInfo(cwd)).remoteName;
|
|
110
|
+
const baseBranch = options.baseBranch ?? (await getDefaultBranchOf(remote, cwd));
|
|
111
|
+
|
|
112
|
+
await $({ cwd })`git fetch --no-tags ${remote} ${baseBranch}`;
|
|
113
|
+
const [forkPoint, head] = await Promise.all([
|
|
114
|
+
$({ cwd })`git merge-base HEAD FETCH_HEAD`.then((result) => result.stdout.trim()),
|
|
115
|
+
getCurrentGitSha(cwd),
|
|
116
|
+
]);
|
|
117
|
+
if (forkPoint !== head) {
|
|
118
|
+
return forkPoint;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parent = await $({ cwd, reject: false })`git rev-parse --verify HEAD~1`;
|
|
122
|
+
if (parent.exitCode !== 0) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'HEAD has no parent commit to compare against. A shallow clone is the usual cause.',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return parent.stdout.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The most recent version tag reachable from HEAD — where a changelog's range starts.
|
|
132
|
+
* @param {Object} opts
|
|
133
|
+
* @param {string} opts.cwd
|
|
134
|
+
* @param {boolean} [opts.fetchAll=true] Whether to fetch all tags from all remotes before finding the latest tag.
|
|
135
|
+
* @returns {Promise<string>}
|
|
136
|
+
*/
|
|
137
|
+
export async function findLatestTaggedVersion(opts) {
|
|
138
|
+
const $$ = $({ cwd: opts.cwd });
|
|
139
|
+
const fetchAll = opts.fetchAll ?? true;
|
|
140
|
+
if (fetchAll) {
|
|
141
|
+
// --force to update any existing tags that may have changed to avoid the clobering error.
|
|
142
|
+
const { remoteName } = await getRepositoryInfo(opts.cwd);
|
|
143
|
+
await $$`git fetch --tags --force ${remoteName}`;
|
|
144
|
+
}
|
|
145
|
+
const { stdout } = await $$`git describe --tags --abbrev=0 --match ${'v*'}`; // only include "version-tags"
|
|
146
|
+
return stdout.trim();
|
|
147
|
+
}
|
package/src/utils/github.mjs
CHANGED
|
@@ -261,3 +261,30 @@ export async function endToEndGhAuthGetToken({ log = false } = {}) {
|
|
|
261
261
|
export async function clearGitHubAuth() {
|
|
262
262
|
await credentials.deleteKey(GITHUB_APP_CREDENTIAL_KEY);
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Returns when a rate-limited request may be retried, or `null` for other errors.
|
|
267
|
+
* https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#exceeding-the-rate-limit
|
|
268
|
+
*
|
|
269
|
+
* @param {unknown} error
|
|
270
|
+
* @returns {Date | null}
|
|
271
|
+
*/
|
|
272
|
+
export function getRateLimitRetryTime(error) {
|
|
273
|
+
const { status, response, message } = /** @type {any} */ (error) ?? {};
|
|
274
|
+
if (status !== 403 && status !== 429) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const headers = response?.headers ?? {};
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
let retryTime;
|
|
280
|
+
if (headers['retry-after']) {
|
|
281
|
+
retryTime = now + Number(headers['retry-after']) * 1000;
|
|
282
|
+
} else if (headers['x-ratelimit-remaining'] === '0' && headers['x-ratelimit-reset']) {
|
|
283
|
+
retryTime = Number(headers['x-ratelimit-reset']) * 1000;
|
|
284
|
+
} else if (/rate limit/i.test(message ?? '')) {
|
|
285
|
+
retryTime = now;
|
|
286
|
+
} else {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return new Date(Math.max(retryTime, now + 60_000));
|
|
290
|
+
}
|