@mui/internal-code-infra 0.1.1-canary.0 → 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 +31 -28
- package/src/babel-config.mjs +12 -1
- 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,13 +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.
|
|
61
|
-
"@babel/plugin-transform-
|
|
62
|
-
"@babel/
|
|
60
|
+
"@babel/plugin-syntax-typescript": "^8.0.3",
|
|
61
|
+
"@babel/plugin-transform-react-pure-annotations": "^8.0.1",
|
|
62
|
+
"@babel/plugin-transform-runtime": "^8.0.6",
|
|
63
|
+
"@babel/preset-env": "^8.0.6",
|
|
63
64
|
"@babel/preset-react": "^8.0.1",
|
|
64
65
|
"@babel/preset-typescript": "^8.0.1",
|
|
65
66
|
"@eslint/compat": "^2.1.1",
|
|
@@ -67,27 +68,29 @@
|
|
|
67
68
|
"@eslint/json": "^2.1.0",
|
|
68
69
|
"@inquirer/confirm": "^6.3.2",
|
|
69
70
|
"@inquirer/select": "^5.2.5",
|
|
70
|
-
"@mui/internal-babel-plugin-display-name": "2.0.1-canary.
|
|
71
|
-
"@mui/internal-babel-plugin-minify-errors": "3.0.1-canary.
|
|
72
|
-
"@mui/internal-babel-plugin-resolve-imports": "3.0.1-canary.
|
|
73
|
-
"@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",
|
|
74
75
|
"@octokit/auth-action": "^6.0.3",
|
|
75
76
|
"@octokit/oauth-methods": "^6.0.5",
|
|
77
|
+
"@octokit/plugin-retry": "^8.1.1",
|
|
78
|
+
"@octokit/plugin-throttling": "^11.0.5",
|
|
76
79
|
"@octokit/rest": "^22.0.1",
|
|
77
|
-
"@pnpm/config.version-policy": "^1100.2.
|
|
80
|
+
"@pnpm/config.version-policy": "^1100.2.5",
|
|
78
81
|
"@pnpm/find-workspace-dir": "^1000.1.5",
|
|
79
82
|
"@pnpm/parse-wanted-dependency": "^1001.0.0",
|
|
80
|
-
"@typescript-eslint/types": "^8.
|
|
81
|
-
"@typescript-eslint/utils": "^8.
|
|
83
|
+
"@typescript-eslint/types": "^8.71.0",
|
|
84
|
+
"@typescript-eslint/utils": "^8.71.0",
|
|
82
85
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
83
86
|
"babel-plugin-optimize-clsx": "^2.6.2",
|
|
84
87
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
85
88
|
"babel-plugin-transform-import-meta": "^3.0.0",
|
|
86
89
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
|
87
90
|
"babel-plugin-transform-remove-imports": "^1.8.1",
|
|
88
|
-
"chalk": "^6.0.
|
|
91
|
+
"chalk": "^6.0.1",
|
|
89
92
|
"clipboardy": "^5.3.2",
|
|
90
|
-
"content-type": "^3.1.
|
|
93
|
+
"content-type": "^3.1.1",
|
|
91
94
|
"env-ci": "^11.2.0",
|
|
92
95
|
"es-toolkit": "^1.52.0",
|
|
93
96
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -103,13 +106,13 @@
|
|
|
103
106
|
"eslint-plugin-testing-library": "^7.16.2",
|
|
104
107
|
"execa": "^10.0.1",
|
|
105
108
|
"git-url-parse": "^16.1.0",
|
|
106
|
-
"globals": "^17.
|
|
109
|
+
"globals": "^17.13.0",
|
|
107
110
|
"globby": "^16.2.4",
|
|
108
|
-
"html-validate": "^11.
|
|
111
|
+
"html-validate": "^11.16.1",
|
|
109
112
|
"minimatch": "^10.2.6",
|
|
110
113
|
"node-html-parser": "^9.0.4",
|
|
111
|
-
"open": "^11.0.
|
|
112
|
-
"postcss-styled-syntax": "^0.7.
|
|
114
|
+
"open": "^11.0.4",
|
|
115
|
+
"postcss-styled-syntax": "^0.7.3",
|
|
113
116
|
"regexp.escape": "^2.0.1",
|
|
114
117
|
"rehype-slug": "^6.0.0",
|
|
115
118
|
"rehype-stringify": "^10.0.1",
|
|
@@ -132,12 +135,12 @@
|
|
|
132
135
|
"resolve-pkg-maps": "^1.0.0",
|
|
133
136
|
"semver": "^7.8.5",
|
|
134
137
|
"stylelint-config-standard": "^40.0.0",
|
|
135
|
-
"typescript-eslint": "^8.
|
|
138
|
+
"typescript-eslint": "^8.71.0",
|
|
136
139
|
"unified": "^11.0.5",
|
|
137
140
|
"unified-lint-rule": "^3.0.1",
|
|
138
141
|
"unist-util-visit": "^5.1.0",
|
|
139
142
|
"yaml": "^2.9.1",
|
|
140
|
-
"yargs": "^18.
|
|
143
|
+
"yargs": "^18.2.0"
|
|
141
144
|
},
|
|
142
145
|
"peerDependencies": {
|
|
143
146
|
"@next/eslint-plugin-next": "*",
|
|
@@ -161,17 +164,17 @@
|
|
|
161
164
|
"@types/mdast": "4.0.4",
|
|
162
165
|
"@types/regexp.escape": "2.0.0",
|
|
163
166
|
"@types/yargs": "17.0.35",
|
|
164
|
-
"@typescript-eslint/parser": "8.
|
|
165
|
-
"@typescript-eslint/rule-tester": "8.
|
|
167
|
+
"@typescript-eslint/parser": "8.71.0",
|
|
168
|
+
"@typescript-eslint/rule-tester": "8.71.0",
|
|
166
169
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
167
|
-
"eslint": "10.
|
|
170
|
+
"eslint": "10.11.0",
|
|
168
171
|
"get-port": "7.2.0",
|
|
169
|
-
"prettier": "3.9.
|
|
172
|
+
"prettier": "3.9.9",
|
|
170
173
|
"remark": "15.0.1",
|
|
171
174
|
"remark-mdx": "3.1.1",
|
|
172
175
|
"serve": "14.2.6",
|
|
173
176
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
174
|
-
"typescript-eslint": "8.
|
|
177
|
+
"typescript-eslint": "8.71.0"
|
|
175
178
|
},
|
|
176
179
|
"files": [
|
|
177
180
|
"bin",
|
|
@@ -186,7 +189,7 @@
|
|
|
186
189
|
"publishConfig": {
|
|
187
190
|
"access": "public"
|
|
188
191
|
},
|
|
189
|
-
"gitSha": "
|
|
192
|
+
"gitSha": "46d7844970a13df535908a1f3f0d353d992feb87",
|
|
190
193
|
"scripts": {
|
|
191
194
|
"build": "tsc -p tsconfig.build.json",
|
|
192
195
|
"typescript": "tsc -noEmit",
|
package/src/babel-config.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import pluginReactPureAnnotations from '@babel/plugin-transform-react-pure-annotations';
|
|
1
2
|
import pluginTransformRuntime from '@babel/plugin-transform-runtime';
|
|
2
3
|
import presetEnv from '@babel/preset-env';
|
|
3
4
|
import presetReact from '@babel/preset-react';
|
|
@@ -10,6 +11,9 @@ import pluginTransformImportMeta from 'babel-plugin-transform-import-meta';
|
|
|
10
11
|
import pluginRemovePropTypes from 'babel-plugin-transform-react-remove-prop-types';
|
|
11
12
|
import pluginTransformInlineEnvVars from './babelPluginInlineEnvironmentVariables.mjs';
|
|
12
13
|
|
|
14
|
+
// Plain TypeScript, ie. files that can't contain JSX.
|
|
15
|
+
const TS_WITHOUT_JSX = /\.[cm]?ts$/;
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* @typedef {'annotation' | 'syntax' | 'infer' | 'all'} ReactCompilationMode
|
|
15
19
|
*/
|
|
@@ -154,9 +158,16 @@ export function getBaseConfig({
|
|
|
154
158
|
{
|
|
155
159
|
// Babel 8 keeps the JSX parser on for every file the React preset touches, which
|
|
156
160
|
// misparses generic arrows like `<T = unknown>(x) => x` in plain `.ts` files.
|
|
157
|
-
exclude:
|
|
161
|
+
exclude: TS_WITHOUT_JSX,
|
|
158
162
|
presets: [[presetReact, { runtime: 'automatic' }]],
|
|
159
163
|
},
|
|
164
|
+
{
|
|
165
|
+
// The React preset is off for those files, so re-add the one part of it they still
|
|
166
|
+
// need: `/*#__PURE__*/` on `React.createContext()` and friends, without which
|
|
167
|
+
// bundlers keep every unused context.
|
|
168
|
+
include: TS_WITHOUT_JSX,
|
|
169
|
+
plugins: [pluginReactPureAnnotations],
|
|
170
|
+
},
|
|
160
171
|
],
|
|
161
172
|
};
|
|
162
173
|
}
|
|
@@ -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
|
+
}
|