@mui/internal-code-infra 0.0.3-canary.6 → 0.0.3-canary.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/build/babel-config.d.mts +40 -0
- package/build/brokenLinksChecker/index.d.mts +138 -0
- package/build/cli/cmdArgosPush.d.mts +13 -0
- package/build/cli/cmdBuild.d.mts +56 -0
- package/build/cli/cmdCopyFiles.d.mts +20 -0
- package/build/cli/cmdExtractErrorCodes.d.mts +3 -0
- package/build/cli/cmdGithubAuth.d.mts +6 -0
- package/build/cli/cmdListWorkspaces.d.mts +18 -0
- package/build/cli/cmdPublish.d.mts +27 -0
- package/build/cli/cmdPublishCanary.d.mts +30 -0
- package/build/cli/cmdPublishNewPackage.d.mts +8 -0
- package/build/cli/cmdSetVersionOverrides.d.mts +9 -0
- package/build/cli/cmdValidateBuiltTypes.d.mts +2 -0
- package/build/cli/index.d.mts +1 -0
- package/build/eslint/baseConfig.d.mts +10 -0
- package/build/eslint/docsConfig.d.mts +4 -0
- package/build/eslint/extensions.d.mts +8 -0
- package/build/eslint/index.d.mts +4 -0
- package/build/eslint/jsonConfig.d.mts +4 -0
- package/build/eslint/material-ui/config.d.mts +8 -0
- package/build/eslint/material-ui/index.d.mts +2 -0
- package/build/eslint/material-ui/rules/disallow-active-element-as-key-event-target.d.mts +5 -0
- package/build/eslint/material-ui/rules/disallow-react-api-in-server-components.d.mts +2 -0
- package/build/eslint/material-ui/rules/docgen-ignore-before-comment.d.mts +2 -0
- package/build/eslint/material-ui/rules/mui-name-matches-component-name.d.mts +5 -0
- package/build/eslint/material-ui/rules/no-empty-box.d.mts +5 -0
- package/build/eslint/material-ui/rules/no-restricted-resolved-imports.d.mts +12 -0
- package/build/eslint/material-ui/rules/no-styled-box.d.mts +5 -0
- package/build/eslint/material-ui/rules/rules-of-use-theme-variants.d.mts +9 -0
- package/build/eslint/material-ui/rules/straight-quotes.d.mts +5 -0
- package/build/eslint/testConfig.d.mts +14 -0
- package/build/markdownlint/duplicate-h1.d.mts +27 -0
- package/build/markdownlint/git-diff.d.mts +8 -0
- package/build/markdownlint/index.d.mts +56 -0
- package/build/markdownlint/straight-quotes.d.mts +8 -0
- package/build/markdownlint/table-alignment.d.mts +8 -0
- package/build/markdownlint/terminal-language.d.mts +8 -0
- package/build/prettier.d.mts +20 -0
- package/build/stylelint/index.d.mts +32 -0
- package/build/utils/babel.d.mts +71 -0
- package/build/utils/build.d.mts +50 -0
- package/build/utils/changelog.d.mts +64 -0
- package/build/utils/credentials.d.mts +17 -0
- package/build/utils/extractErrorCodes.d.mts +19 -0
- package/build/utils/git.d.mts +26 -0
- package/build/utils/github.d.mts +41 -0
- package/build/utils/pnpm.d.mts +238 -0
- package/build/utils/typescript.d.mts +35 -0
- package/package.json +92 -42
- package/src/babel-config.mjs +52 -8
- package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +20 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +22 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/example.md +9 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +21 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +17 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/index.html +26 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +5 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +19 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +20 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +20 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +24 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +28 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +19 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +20 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +31 -0
- package/src/brokenLinksChecker/index.mjs +641 -0
- package/src/brokenLinksChecker/index.test.ts +178 -0
- package/src/cli/cmdArgosPush.mjs +13 -2
- package/src/cli/cmdBuild.mjs +228 -31
- package/src/cli/cmdGithubAuth.mjs +36 -0
- package/src/cli/cmdListWorkspaces.mjs +2 -2
- package/src/cli/cmdPublish.mjs +203 -49
- package/src/cli/cmdPublishCanary.mjs +404 -46
- package/src/cli/cmdPublishNewPackage.mjs +86 -0
- package/src/cli/cmdSetVersionOverrides.mjs +17 -1
- package/src/cli/cmdValidateBuiltTypes.mjs +49 -0
- package/src/cli/index.mjs +6 -2
- package/src/cli/packageJson.d.ts +729 -0
- package/src/eslint/baseConfig.mjs +96 -78
- package/src/eslint/docsConfig.mjs +13 -13
- package/src/eslint/extensions.mjs +8 -8
- package/src/eslint/jsonConfig.mjs +40 -0
- package/src/eslint/material-ui/config.mjs +8 -9
- package/src/eslint/material-ui/rules/mui-name-matches-component-name.mjs +4 -2
- package/src/eslint/material-ui/rules/rules-of-use-theme-variants.mjs +2 -1
- package/src/eslint/testConfig.mjs +72 -66
- package/src/stylelint/index.mjs +46 -0
- package/src/untyped-plugins.d.ts +13 -0
- package/src/{cli → utils}/babel.mjs +10 -3
- package/src/utils/build.mjs +27 -1
- package/src/utils/changelog.mjs +157 -0
- package/src/utils/credentials.mjs +71 -0
- package/src/utils/extractErrorCodes.mjs +2 -2
- package/src/utils/git.mjs +67 -0
- package/src/utils/github.mjs +263 -0
- package/src/{cli → utils}/pnpm.mjs +23 -13
- package/src/{cli → utils}/typescript.mjs +13 -7
- package/src/cli/cmdJsonLint.mjs +0 -69
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/// <reference types="../untyped-plugins" />
|
|
3
3
|
|
|
4
4
|
import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
|
|
5
|
+
import { $ } from 'execa';
|
|
5
6
|
import { globby } from 'globby';
|
|
6
7
|
import * as fs from 'node:fs/promises';
|
|
7
8
|
import * as path from 'node:path';
|
|
8
|
-
import {
|
|
9
|
-
import { BASE_IGNORES } from '../utils/build.mjs';
|
|
9
|
+
import { BASE_IGNORES } from './build.mjs';
|
|
10
10
|
|
|
11
11
|
const TO_TRANSFORM_EXTENSIONS = ['.js', '.ts', '.tsx'];
|
|
12
12
|
|
|
@@ -70,6 +70,8 @@ export async function cjsCopy({ from, to }) {
|
|
|
70
70
|
* @param {boolean} [options.verbose=false] - Whether to enable verbose logging.
|
|
71
71
|
* @param {boolean} [options.optimizeClsx=false] - Whether to enable clsx call optimization transform.
|
|
72
72
|
* @param {boolean} [options.removePropTypes=true] - Whether to enable removal of React prop types.
|
|
73
|
+
* @param {Object} [options.reactCompiler] - Whether to use the React compiler.
|
|
74
|
+
* @param {string} [options.reactCompiler.reactVersion] - The React version to use with the React compiler.
|
|
73
75
|
* @param {string[]} [options.ignores] - The globs to be ignored by Babel.
|
|
74
76
|
* @param {string} options.cwd - The package root directory.
|
|
75
77
|
* @param {string} options.pkgVersion - The package version.
|
|
@@ -77,7 +79,7 @@ export async function cjsCopy({ from, to }) {
|
|
|
77
79
|
* @param {string} options.outDir - The output directory for the build.
|
|
78
80
|
* @param {string} options.outExtension - The output file extension for the build.
|
|
79
81
|
* @param {boolean} options.hasLargeFiles - Whether the build includes large files.
|
|
80
|
-
* @param {import('
|
|
82
|
+
* @param {import('./build.mjs').BundleType} options.bundle - The bundles to build.
|
|
81
83
|
* @param {string} options.babelRuntimeVersion - The version of @babel/runtime to use.
|
|
82
84
|
* @returns {Promise<void>}
|
|
83
85
|
*/
|
|
@@ -94,6 +96,7 @@ export async function babelBuild({
|
|
|
94
96
|
removePropTypes = false,
|
|
95
97
|
verbose = false,
|
|
96
98
|
ignores = [],
|
|
99
|
+
reactCompiler,
|
|
97
100
|
}) {
|
|
98
101
|
console.log(
|
|
99
102
|
`Transpiling files to "${path.relative(path.dirname(sourceDir), outDir)}" for "${bundle}" bundle.`,
|
|
@@ -111,6 +114,8 @@ export async function babelBuild({
|
|
|
111
114
|
) {
|
|
112
115
|
configFile = path.join(workspaceDir, 'babel.config.mjs');
|
|
113
116
|
}
|
|
117
|
+
|
|
118
|
+
const reactVersion = reactCompiler?.reactVersion;
|
|
114
119
|
const env = {
|
|
115
120
|
NODE_ENV: 'production',
|
|
116
121
|
BABEL_ENV: bundle === 'esm' ? 'stable' : 'node',
|
|
@@ -120,6 +125,8 @@ export async function babelBuild({
|
|
|
120
125
|
MUI_BABEL_RUNTIME_VERSION: babelRuntimeVersion,
|
|
121
126
|
MUI_OUT_FILE_EXTENSION: outExtension ?? '.js',
|
|
122
127
|
...getVersionEnvVariables(pkgVersion),
|
|
128
|
+
MUI_REACT_COMPILER: reactVersion ? '1' : '0',
|
|
129
|
+
MUI_REACT_COMPILER_REACT_VERSION: reactVersion,
|
|
123
130
|
};
|
|
124
131
|
const res = await $({
|
|
125
132
|
stdio: 'inherit',
|
package/src/utils/build.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as semver from 'semver';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {'esm' | 'cjs'} BundleType
|
|
3
5
|
*/
|
|
@@ -24,9 +26,10 @@ export function getOutExtension(bundle, isType = false) {
|
|
|
24
26
|
* @param {Record<string, any>} packageJson
|
|
25
27
|
* @param {Object} [options]
|
|
26
28
|
* @param {boolean} [options.skipMainCheck=false] - Whether to skip checking for main field in package.json.
|
|
29
|
+
* @param {boolean} [options.enableReactCompiler=false] - Whether to enable React compiler checks.
|
|
27
30
|
*/
|
|
28
31
|
export function validatePkgJson(packageJson, options = {}) {
|
|
29
|
-
const { skipMainCheck = false } = options;
|
|
32
|
+
const { skipMainCheck = false, enableReactCompiler = false } = options;
|
|
30
33
|
/**
|
|
31
34
|
* @type {string[]}
|
|
32
35
|
*/
|
|
@@ -63,6 +66,29 @@ export function validatePkgJson(packageJson, options = {}) {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
const reactVersion = packageJson.peerDependencies?.react;
|
|
70
|
+
if (enableReactCompiler) {
|
|
71
|
+
if (!reactVersion) {
|
|
72
|
+
errors.push(
|
|
73
|
+
'When building with React compiler, "react" must be specified as a peerDependency in package.json.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const minSupportedReactVersion = semver.minVersion(reactVersion);
|
|
77
|
+
if (!minSupportedReactVersion) {
|
|
78
|
+
errors.push(
|
|
79
|
+
`Unable to determine the minimum supported React version from the peerDependency range: "${reactVersion}".`,
|
|
80
|
+
);
|
|
81
|
+
} else if (
|
|
82
|
+
semver.lt(minSupportedReactVersion, '19.0.0') &&
|
|
83
|
+
!packageJson.peerDependencies?.['react-compiler-runtime'] &&
|
|
84
|
+
!packageJson.dependencies?.['react-compiler-runtime']
|
|
85
|
+
) {
|
|
86
|
+
errors.push(
|
|
87
|
+
'When building with React compiler for React versions below 19, "react-compiler-runtime" must be specified as a dependency or peerDependency in package.json.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
if (errors.length > 0) {
|
|
67
93
|
const error = new Error(errors.join('\n'));
|
|
68
94
|
throw error;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest';
|
|
2
|
+
import { $ } from 'execa';
|
|
3
|
+
|
|
4
|
+
import { persistentAuthStrategy } from './github.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('@octokit/rest').Octokit} OctokitType
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {'team' | 'first_timer' | 'contributor'} AuthorAssociation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} FetchedCommitDetails
|
|
16
|
+
* @property {string} sha
|
|
17
|
+
* @property {string} message
|
|
18
|
+
* @property {string[]} labels
|
|
19
|
+
* @property {number} prNumber
|
|
20
|
+
* @property {string} html_url
|
|
21
|
+
* @property {{login: string; association: AuthorAssociation} | null} author
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} FetchCommitsOptions
|
|
26
|
+
* @property {string} repo
|
|
27
|
+
* @property {string} lastRelease
|
|
28
|
+
* @property {string} release
|
|
29
|
+
* @property {string} [org='mui'] - GitHub organization name, defaults to 'mui'
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Object} opts
|
|
34
|
+
* @param {string} opts.cwd
|
|
35
|
+
* @param {boolean} [opts.fetchAll=true] Whether to fetch all tags from all remotes before finding the latest tag.
|
|
36
|
+
* @returns {Promise<string>}
|
|
37
|
+
*/
|
|
38
|
+
export async function findLatestTaggedVersion(opts) {
|
|
39
|
+
const $$ = $({ cwd: opts.cwd });
|
|
40
|
+
const fetchAll = opts.fetchAll ?? true;
|
|
41
|
+
if (fetchAll) {
|
|
42
|
+
// Fetch all tags from all remotes to ensure we have the latest tags.
|
|
43
|
+
await $$`git fetch --tags --all`;
|
|
44
|
+
}
|
|
45
|
+
const { stdout } = await $$`git describe --tags --abbrev=0 --match ${'v*'}`; // only include "version-tags"
|
|
46
|
+
return stdout.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetches commits between two refs (lastRelease..release) including PR details.
|
|
51
|
+
* Automatically handles GitHub OAuth authentication if none provided.
|
|
52
|
+
*
|
|
53
|
+
* @param {FetchCommitsOptions & {octokit?: OctokitType}} opts
|
|
54
|
+
* @returns {Promise<FetchedCommitDetails[]>}
|
|
55
|
+
*/
|
|
56
|
+
export async function fetchCommitsBetweenRefs(opts) {
|
|
57
|
+
const octokit =
|
|
58
|
+
'octokit' in opts && opts.octokit
|
|
59
|
+
? opts.octokit
|
|
60
|
+
: new Octokit({ authStrategy: persistentAuthStrategy });
|
|
61
|
+
|
|
62
|
+
return fetchCommitsRest({
|
|
63
|
+
octokit,
|
|
64
|
+
repo: opts.repo,
|
|
65
|
+
lastRelease: opts.lastRelease,
|
|
66
|
+
release: opts.release,
|
|
67
|
+
org: opts.org ?? 'mui',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fetches commits between two refs using GitHub's REST API.
|
|
73
|
+
* It is more reliable than the GraphQL API but requires multiple network calls (1 + n).
|
|
74
|
+
* One to list all commits between the two refs and then one for each commit to get the PR details.
|
|
75
|
+
*
|
|
76
|
+
* @param {FetchCommitsOptions & { octokit: OctokitType}} param0
|
|
77
|
+
*
|
|
78
|
+
* @returns {Promise<FetchedCommitDetails[]>}
|
|
79
|
+
*/
|
|
80
|
+
async function fetchCommitsRest({ octokit, repo, lastRelease, release, org = 'mui' }) {
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {Awaited<ReturnType<Octokit['repos']['compareCommits']>>['data']['commits']} Commits
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* @type {Commits}
|
|
86
|
+
*/
|
|
87
|
+
const results = [];
|
|
88
|
+
/**
|
|
89
|
+
* @type {any}
|
|
90
|
+
*/
|
|
91
|
+
const timeline = octokit.paginate.iterator(
|
|
92
|
+
octokit.repos.compareCommitsWithBasehead.endpoint.merge({
|
|
93
|
+
owner: org,
|
|
94
|
+
repo,
|
|
95
|
+
basehead: `${lastRelease}...${release}`,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
for await (const response of timeline) {
|
|
99
|
+
results.push(...response.data.commits);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const promises = results.map(async (commit) => {
|
|
103
|
+
const prMatch = commit.commit.message.match(/#(\d+)/);
|
|
104
|
+
if (prMatch === null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const prNumber = parseInt(prMatch[1], 10);
|
|
109
|
+
|
|
110
|
+
const pr = await octokit.pulls.get({
|
|
111
|
+
owner: org,
|
|
112
|
+
repo,
|
|
113
|
+
pull_number: prNumber,
|
|
114
|
+
headers: {
|
|
115
|
+
Accept: 'application/vnd.github.text+json',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const labels = pr.data.labels.map((label) => label.name);
|
|
120
|
+
|
|
121
|
+
return /** @type {FetchedCommitDetails} */ ({
|
|
122
|
+
sha: commit.sha,
|
|
123
|
+
message: commit.commit.message,
|
|
124
|
+
labels,
|
|
125
|
+
prNumber,
|
|
126
|
+
html_url: pr.data.html_url,
|
|
127
|
+
author: pr.data.user?.login
|
|
128
|
+
? {
|
|
129
|
+
login: pr.data.user.login,
|
|
130
|
+
association: getAuthorAssociation(pr.data.author_association),
|
|
131
|
+
}
|
|
132
|
+
: null,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (await Promise.all(promises)).filter((entry) => entry !== null);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
*
|
|
141
|
+
* @param {import('@octokit/rest').RestEndpointMethodTypes["pulls"]["get"]["response"]["data"]["author_association"]} input
|
|
142
|
+
* @returns {AuthorAssociation}
|
|
143
|
+
*/
|
|
144
|
+
function getAuthorAssociation(input) {
|
|
145
|
+
switch (input) {
|
|
146
|
+
case 'OWNER':
|
|
147
|
+
case 'MEMBER':
|
|
148
|
+
return 'team';
|
|
149
|
+
case 'MANNEQUIN':
|
|
150
|
+
case 'NONE':
|
|
151
|
+
case 'FIRST_TIMER':
|
|
152
|
+
case 'FIRST_TIME_CONTRIBUTOR':
|
|
153
|
+
return 'first_timer';
|
|
154
|
+
default:
|
|
155
|
+
return 'contributor';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure credential storage for CLI tools
|
|
3
|
+
*
|
|
4
|
+
* Provides secure credential storage for the MUI code infrastructure tools.
|
|
5
|
+
* It uses OS keychain/credential manager to store credentials.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AsyncEntry } from '@napi-rs/keyring';
|
|
10
|
+
|
|
11
|
+
export const KEYRING_SERVICE = 'mui-code-infra';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @type {Map<string, AsyncEntry>}
|
|
15
|
+
*/
|
|
16
|
+
const credentials = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @type {Map<string, string>} In-memory cache for credentials that have been accessed.
|
|
20
|
+
* This is used to avoid multiple prompts for the same credential during a single run.
|
|
21
|
+
*/
|
|
22
|
+
const accessedCredentials = new Map();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} key
|
|
26
|
+
* @returns {Promise<string | undefined>}
|
|
27
|
+
*/
|
|
28
|
+
export async function getPassword(key) {
|
|
29
|
+
if (accessedCredentials.has(key)) {
|
|
30
|
+
return accessedCredentials.get(key);
|
|
31
|
+
}
|
|
32
|
+
let credential = credentials.get(key);
|
|
33
|
+
if (!credential) {
|
|
34
|
+
credential = new AsyncEntry(KEYRING_SERVICE, key);
|
|
35
|
+
credentials.set(key, credential);
|
|
36
|
+
}
|
|
37
|
+
const res = await credential.getPassword();
|
|
38
|
+
if (res) {
|
|
39
|
+
accessedCredentials.set(key, res);
|
|
40
|
+
}
|
|
41
|
+
return res;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} key
|
|
46
|
+
* @param {string} password
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
export async function setPassword(key, password) {
|
|
50
|
+
accessedCredentials.set(key, password);
|
|
51
|
+
let credential = credentials.get(key);
|
|
52
|
+
if (!credential) {
|
|
53
|
+
credential = new AsyncEntry(KEYRING_SERVICE, key);
|
|
54
|
+
credentials.set(key, credential);
|
|
55
|
+
}
|
|
56
|
+
await credential.setPassword(password);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} key
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
export async function deleteKey(key) {
|
|
64
|
+
accessedCredentials.delete(key);
|
|
65
|
+
let credential = credentials.get(key);
|
|
66
|
+
credentials.delete(key);
|
|
67
|
+
if (!credential) {
|
|
68
|
+
credential = new AsyncEntry(KEYRING_SERVICE, key);
|
|
69
|
+
}
|
|
70
|
+
await credential.deletePassword();
|
|
71
|
+
}
|
|
@@ -7,7 +7,7 @@ import { globby } from 'globby';
|
|
|
7
7
|
import * as fs from 'node:fs/promises';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
|
|
10
|
-
import { getWorkspacePackages } from '
|
|
10
|
+
import { getWorkspacePackages } from './pnpm.mjs';
|
|
11
11
|
import { BASE_IGNORES, mapConcurrently } from './build.mjs';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -20,7 +20,7 @@ import { BASE_IGNORES, mapConcurrently } from './build.mjs';
|
|
|
20
20
|
/**
|
|
21
21
|
* Gets all relevant files for a package to parse.
|
|
22
22
|
*
|
|
23
|
-
* @param {import('
|
|
23
|
+
* @param {import('./pnpm.mjs').PublicPackage} pkg
|
|
24
24
|
* @returns {Promise<string[]>} An array of file paths.
|
|
25
25
|
*/
|
|
26
26
|
async function getFilesForPackage(pkg) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { $ } from 'execa';
|
|
2
|
+
import gitUrlParse from 'git-url-parse';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} RepoInfo
|
|
6
|
+
* @property {string} owner - Repository owner
|
|
7
|
+
* @property {string} repo - Repository name
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get current repository info from git remote
|
|
12
|
+
* @param {string[]} [remotes=['upstream', 'origin']] - Remote name(s) to check (default: ['upstream', 'origin'])
|
|
13
|
+
* @returns {Promise<RepoInfo>} Repository owner and name
|
|
14
|
+
*/
|
|
15
|
+
export async function getRepositoryInfo(remotes = ['upstream', 'origin']) {
|
|
16
|
+
/**
|
|
17
|
+
* @type {Record<string, string>}
|
|
18
|
+
*/
|
|
19
|
+
const cause = {};
|
|
20
|
+
const cliResult = $`git remote -v`;
|
|
21
|
+
/**
|
|
22
|
+
* @type {Map<string, string>}
|
|
23
|
+
*/
|
|
24
|
+
const repoRemotes = new Map();
|
|
25
|
+
|
|
26
|
+
for await (const line of cliResult) {
|
|
27
|
+
// Match pattern: "remoteName url (fetch|push)"
|
|
28
|
+
const [remoteName, url, type] = line.trim().split(/\s+/, 3);
|
|
29
|
+
if (type === '(fetch)') {
|
|
30
|
+
repoRemotes.set(remoteName, url);
|
|
31
|
+
} else if (type !== '(push)') {
|
|
32
|
+
throw new Error(`Unexpected line format for "git remote -v": "${line}"`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const remote of remotes) {
|
|
37
|
+
if (!repoRemotes.has(remote)) {
|
|
38
|
+
cause[remote] = 'Remote not found';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const url = /** @type {string} */ (repoRemotes.get(remote));
|
|
42
|
+
try {
|
|
43
|
+
const parsed = gitUrlParse(url);
|
|
44
|
+
if (parsed.source !== 'github.com' || parsed.owner !== 'mui') {
|
|
45
|
+
cause[remote] = `Remote is not a GitHub repository under 'mui' organization: ${url}`;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
owner: parsed.owner,
|
|
50
|
+
repo: parsed.name,
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
cause[remote] = `Failed to parse URL for remote ${remote}: ${url}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(`Failed to find remote(s): ${remotes.join(', ')}`, { cause });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get current git SHA
|
|
62
|
+
* @returns {Promise<string>} Current git commit SHA
|
|
63
|
+
*/
|
|
64
|
+
export async function getCurrentGitSha() {
|
|
65
|
+
const result = await $`git rev-parse HEAD`;
|
|
66
|
+
return result.stdout.trim();
|
|
67
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core GitHub OAuth authentication utilities
|
|
3
|
+
*
|
|
4
|
+
* This utility provides core GitHub OAuth functionality using openid-client.
|
|
5
|
+
* It handles token storage, refresh, and device flow initiation.
|
|
6
|
+
* CLI-specific user interaction is handled in cli/cmdGithubAuth.mjs.
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createDeviceCode,
|
|
12
|
+
exchangeDeviceCode,
|
|
13
|
+
refreshToken as ghRefreshToken,
|
|
14
|
+
} from '@octokit/oauth-methods';
|
|
15
|
+
import clipboardy from 'clipboardy';
|
|
16
|
+
import open from 'open';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
|
|
19
|
+
import * as credentials from './credentials.mjs';
|
|
20
|
+
|
|
21
|
+
const GITHUB_APP_CLIENT_ID = 'Iv23lilHsGU3i1tIARsT'; // MUI Code Infra Oauth App
|
|
22
|
+
// Use the client id as the key so that if it changes, we don't conflict with old tokens
|
|
23
|
+
const GITHUB_APP_CREDENTIAL_KEY = GITHUB_APP_CLIENT_ID;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} GitHubAppAuthenticationWithRefreshToken
|
|
27
|
+
* @property {string} token - The access token
|
|
28
|
+
* @property {string} [expiresAt] - ISO string when the access token expires
|
|
29
|
+
* @property {string} [refreshToken] - The refresh token
|
|
30
|
+
* @property {string} [refreshTokenExpiresAt] - ISO string when the refresh token expires
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export function persistentAuthStrategy() {
|
|
34
|
+
/**
|
|
35
|
+
* Request hook to add authentication token to requests.
|
|
36
|
+
* Automatically handles token refresh on 401 errors.
|
|
37
|
+
*
|
|
38
|
+
* @param {import('@octokit/types').RequestInterface} request
|
|
39
|
+
* @param {import('@octokit/types').Route} route
|
|
40
|
+
* @param {import('@octokit/types').RequestParameters} parameters
|
|
41
|
+
* @returns
|
|
42
|
+
*/
|
|
43
|
+
async function hook(request, route, parameters) {
|
|
44
|
+
const token = await endToEndGhAuthGetToken({ log: true });
|
|
45
|
+
const endpoint = request.endpoint.merge(route, parameters);
|
|
46
|
+
endpoint.headers.authorization = `token ${token}`;
|
|
47
|
+
try {
|
|
48
|
+
// @ts-expect-error - request.endpoint.merge doesn't return correct type
|
|
49
|
+
return await request(endpoint);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const err =
|
|
52
|
+
/** @type {import('@octokit/types').RequestError & {response: {data: {message: string}}}} */ (
|
|
53
|
+
error
|
|
54
|
+
);
|
|
55
|
+
if (err.status === 401 && err.response.data.message.toLowerCase() === 'bad credentials') {
|
|
56
|
+
// refresh token and retry again
|
|
57
|
+
await clearGitHubAuth();
|
|
58
|
+
const newToken = await endToEndGhAuthGetToken();
|
|
59
|
+
endpoint.headers.authorization = `token ${newToken}`;
|
|
60
|
+
// @ts-expect-error - request.endpoint.merge doesn't return correct type
|
|
61
|
+
return await request(endpoint);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { hook };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Object} data
|
|
72
|
+
* @param {string} data.url
|
|
73
|
+
* @param {string} data.code
|
|
74
|
+
* @param {Object} options
|
|
75
|
+
* @param {boolean} [options.openInBrowser=true] - Whether to open the URL in the default browser
|
|
76
|
+
* @param {boolean} [options.copyToClipboard=true] - Whether to copy the code to clipboard
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async function logAuthInformation(data, { openInBrowser = true, copyToClipboard = true } = {}) {
|
|
80
|
+
if (copyToClipboard) {
|
|
81
|
+
await clipboardy.write(data.code);
|
|
82
|
+
console.warn(`Pasted authentication code ${chalk.bold(data.code)} to system clipboard...`);
|
|
83
|
+
} else {
|
|
84
|
+
console.warn(`To authenticate, paste "${chalk.bold(data.code)}" when prompted.`);
|
|
85
|
+
}
|
|
86
|
+
if (openInBrowser) {
|
|
87
|
+
console.warn(`Opening ${chalk.bold(data.url)} in default browser, or goto the link manually.`);
|
|
88
|
+
await open(data.url);
|
|
89
|
+
} else {
|
|
90
|
+
console.warn(`Open ${data.url} in your browser to authenticate.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks if the stored access token is expired
|
|
96
|
+
* @returns {Promise<boolean>}
|
|
97
|
+
*/
|
|
98
|
+
async function isTokenExpired() {
|
|
99
|
+
try {
|
|
100
|
+
const tokens = await getCredentialData();
|
|
101
|
+
|
|
102
|
+
if (tokens.expiresAt) {
|
|
103
|
+
return Date.now() > new Date(tokens.expiresAt).getTime();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (tokens.refreshTokenExpiresAt) {
|
|
107
|
+
return Date.now() > new Date(tokens.refreshTokenExpiresAt).getTime();
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return true; // If we can't get expiry, assume expired
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
*
|
|
117
|
+
* @returns {Promise<GitHubAppAuthenticationWithRefreshToken>} Stored GitHub authentication tokens
|
|
118
|
+
*/
|
|
119
|
+
async function getCredentialData() {
|
|
120
|
+
const data = await credentials.getPassword(GITHUB_APP_CREDENTIAL_KEY);
|
|
121
|
+
if (!data) {
|
|
122
|
+
return {
|
|
123
|
+
token: '',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return /** @type {GitHubAppAuthenticationWithRefreshToken} */ (JSON.parse(data));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stores GitHub authentication tokens securely
|
|
131
|
+
* @param {{token: string, refreshToken?: string, expiresAt?: string; refreshTokenExpiresAt?: string}} tokens - Token response from openid-client
|
|
132
|
+
* @returns {Promise<void>}
|
|
133
|
+
*/
|
|
134
|
+
async function storeGitHubTokens(tokens) {
|
|
135
|
+
/**
|
|
136
|
+
* @type {GitHubAppAuthenticationWithRefreshToken}
|
|
137
|
+
*/
|
|
138
|
+
const newTokens = {
|
|
139
|
+
token: tokens.token,
|
|
140
|
+
expiresAt: tokens.expiresAt,
|
|
141
|
+
refreshToken: tokens.refreshToken,
|
|
142
|
+
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await credentials.setPassword(GITHUB_APP_CREDENTIAL_KEY, JSON.stringify(newTokens));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @returns {Promise<string>} Refreshed GitHub access token
|
|
150
|
+
*/
|
|
151
|
+
async function refreshAccessToken() {
|
|
152
|
+
const tokenData = await getCredentialData();
|
|
153
|
+
if (!tokenData.refreshToken) {
|
|
154
|
+
return await doGithubAuth();
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
tokenData.refreshTokenExpiresAt &&
|
|
158
|
+
Date.now() > new Date(tokenData.refreshTokenExpiresAt).getTime()
|
|
159
|
+
) {
|
|
160
|
+
// Refresh token has also expired. Need to re-authenticate
|
|
161
|
+
await clearGitHubAuth();
|
|
162
|
+
return await doGithubAuth();
|
|
163
|
+
}
|
|
164
|
+
const { authentication } = await ghRefreshToken({
|
|
165
|
+
clientId: GITHUB_APP_CLIENT_ID,
|
|
166
|
+
refreshToken: tokenData.refreshToken,
|
|
167
|
+
clientType: 'github-app',
|
|
168
|
+
clientSecret: '',
|
|
169
|
+
});
|
|
170
|
+
storeGitHubTokens(authentication);
|
|
171
|
+
return authentication.token;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @returns {Promise<{url: string, code: string; deviceCode: string}>} Device flow response with verification URI and user code
|
|
176
|
+
*/
|
|
177
|
+
async function getAuthInformation() {
|
|
178
|
+
const { data } = await createDeviceCode({
|
|
179
|
+
clientId: GITHUB_APP_CLIENT_ID,
|
|
180
|
+
clientType: 'github-app',
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
url: data.verification_uri,
|
|
184
|
+
code: data.user_code,
|
|
185
|
+
deviceCode: data.device_code,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Retries exchanging device code for tokens until success or timeout.
|
|
191
|
+
* Defaults to 12 retries with 5s delay (1 minute total).
|
|
192
|
+
* Has initial delay to allow user to enter code in browser.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} deviceCode
|
|
195
|
+
* @param {{delay?: number, retries?: number}} [options]
|
|
196
|
+
* @returns {Promise<import('@octokit/oauth-methods').GitHubAppAuthenticationWithRefreshToken>}
|
|
197
|
+
*/
|
|
198
|
+
async function exchangeDeviceCodeWithRetry(deviceCode, { delay = 5000, retries = 12 } = {}) {
|
|
199
|
+
if (delay) {
|
|
200
|
+
await new Promise((resolve) => {
|
|
201
|
+
setTimeout(resolve, delay);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const { authentication } = await exchangeDeviceCode({
|
|
206
|
+
clientId: GITHUB_APP_CLIENT_ID,
|
|
207
|
+
clientType: 'github-app',
|
|
208
|
+
code: deviceCode,
|
|
209
|
+
});
|
|
210
|
+
return /** @type {import('@octokit/oauth-methods').GitHubAppAuthenticationWithRefreshToken} */ (
|
|
211
|
+
authentication
|
|
212
|
+
);
|
|
213
|
+
} catch (/** @type {any} */ ex) {
|
|
214
|
+
if (ex.response.data.error !== 'authorization_pending') {
|
|
215
|
+
throw ex; // Some other error
|
|
216
|
+
}
|
|
217
|
+
if (retries > 0) {
|
|
218
|
+
console.warn('Retrying device code exchange...');
|
|
219
|
+
return exchangeDeviceCodeWithRetry(deviceCode, { delay, retries: retries - 1 });
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`[github-auth]: Timed out waiting for user authentication`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function doGithubAuth() {
|
|
226
|
+
const data = await getAuthInformation();
|
|
227
|
+
await logAuthInformation(data);
|
|
228
|
+
const tokens = await exchangeDeviceCodeWithRetry(data.deviceCode);
|
|
229
|
+
await storeGitHubTokens(tokens);
|
|
230
|
+
return tokens.token;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {Object} [options]
|
|
235
|
+
* @param {boolean} [options.log=false] - Whether to log progress to console
|
|
236
|
+
* @returns {Promise<string>} Valid GitHub access token
|
|
237
|
+
*/
|
|
238
|
+
export async function endToEndGhAuthGetToken({ log = false } = {}) {
|
|
239
|
+
const tokenData = await getCredentialData();
|
|
240
|
+
if (!tokenData.token) {
|
|
241
|
+
if (log) {
|
|
242
|
+
console.warn("🔍 GitHub token doesn't exist. Starting authentication flow...");
|
|
243
|
+
}
|
|
244
|
+
return await doGithubAuth();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (await isTokenExpired()) {
|
|
248
|
+
if (log) {
|
|
249
|
+
console.warn('GitHub token expired. Attempting to refresh...');
|
|
250
|
+
}
|
|
251
|
+
return refreshAccessToken();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return tokenData.token;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clears stored GitHub authentication tokens
|
|
259
|
+
* @returns {Promise<void>}
|
|
260
|
+
*/
|
|
261
|
+
export async function clearGitHubAuth() {
|
|
262
|
+
await credentials.deleteKey(GITHUB_APP_CREDENTIAL_KEY);
|
|
263
|
+
}
|