@mui/internal-code-infra 0.0.3-canary.6 → 0.0.3-canary.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +55 -0
  2. package/build/babel-config.d.mts +40 -0
  3. package/build/brokenLinksChecker/index.d.mts +138 -0
  4. package/build/cli/cmdArgosPush.d.mts +13 -0
  5. package/build/cli/cmdBuild.d.mts +56 -0
  6. package/build/cli/cmdCopyFiles.d.mts +20 -0
  7. package/build/cli/cmdExtractErrorCodes.d.mts +3 -0
  8. package/build/cli/cmdGithubAuth.d.mts +6 -0
  9. package/build/cli/cmdListWorkspaces.d.mts +18 -0
  10. package/build/cli/cmdPublish.d.mts +27 -0
  11. package/build/cli/cmdPublishCanary.d.mts +30 -0
  12. package/build/cli/cmdPublishNewPackage.d.mts +8 -0
  13. package/build/cli/cmdSetVersionOverrides.d.mts +9 -0
  14. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -0
  15. package/build/cli/index.d.mts +1 -0
  16. package/build/eslint/baseConfig.d.mts +10 -0
  17. package/build/eslint/docsConfig.d.mts +4 -0
  18. package/build/eslint/extensions.d.mts +8 -0
  19. package/build/eslint/index.d.mts +4 -0
  20. package/build/eslint/jsonConfig.d.mts +4 -0
  21. package/build/eslint/material-ui/config.d.mts +8 -0
  22. package/build/eslint/material-ui/index.d.mts +2 -0
  23. package/build/eslint/material-ui/rules/disallow-active-element-as-key-event-target.d.mts +5 -0
  24. package/build/eslint/material-ui/rules/disallow-react-api-in-server-components.d.mts +2 -0
  25. package/build/eslint/material-ui/rules/docgen-ignore-before-comment.d.mts +2 -0
  26. package/build/eslint/material-ui/rules/mui-name-matches-component-name.d.mts +5 -0
  27. package/build/eslint/material-ui/rules/no-empty-box.d.mts +5 -0
  28. package/build/eslint/material-ui/rules/no-restricted-resolved-imports.d.mts +12 -0
  29. package/build/eslint/material-ui/rules/no-styled-box.d.mts +5 -0
  30. package/build/eslint/material-ui/rules/rules-of-use-theme-variants.d.mts +9 -0
  31. package/build/eslint/material-ui/rules/straight-quotes.d.mts +5 -0
  32. package/build/eslint/testConfig.d.mts +14 -0
  33. package/build/markdownlint/duplicate-h1.d.mts +27 -0
  34. package/build/markdownlint/git-diff.d.mts +8 -0
  35. package/build/markdownlint/index.d.mts +56 -0
  36. package/build/markdownlint/straight-quotes.d.mts +8 -0
  37. package/build/markdownlint/table-alignment.d.mts +8 -0
  38. package/build/markdownlint/terminal-language.d.mts +8 -0
  39. package/build/prettier.d.mts +20 -0
  40. package/build/stylelint/index.d.mts +32 -0
  41. package/build/utils/babel.d.mts +71 -0
  42. package/build/utils/build.d.mts +50 -0
  43. package/build/utils/changelog.d.mts +64 -0
  44. package/build/utils/credentials.d.mts +17 -0
  45. package/build/utils/extractErrorCodes.d.mts +19 -0
  46. package/build/utils/git.d.mts +26 -0
  47. package/build/utils/github.d.mts +41 -0
  48. package/build/utils/pnpm.d.mts +238 -0
  49. package/build/utils/typescript.d.mts +35 -0
  50. package/package.json +92 -42
  51. package/src/babel-config.mjs +52 -8
  52. package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +20 -0
  53. package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +22 -0
  54. package/src/brokenLinksChecker/__fixtures__/static-site/example.md +9 -0
  55. package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +21 -0
  56. package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +17 -0
  57. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +26 -0
  58. package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +5 -0
  59. package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +19 -0
  60. package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +20 -0
  61. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +20 -0
  62. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +24 -0
  63. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +28 -0
  64. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +19 -0
  65. package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +20 -0
  66. package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +31 -0
  67. package/src/brokenLinksChecker/index.mjs +641 -0
  68. package/src/brokenLinksChecker/index.test.ts +178 -0
  69. package/src/cli/cmdArgosPush.mjs +13 -2
  70. package/src/cli/cmdBuild.mjs +228 -31
  71. package/src/cli/cmdGithubAuth.mjs +36 -0
  72. package/src/cli/cmdListWorkspaces.mjs +2 -2
  73. package/src/cli/cmdPublish.mjs +203 -49
  74. package/src/cli/cmdPublishCanary.mjs +404 -46
  75. package/src/cli/cmdPublishNewPackage.mjs +86 -0
  76. package/src/cli/cmdSetVersionOverrides.mjs +17 -1
  77. package/src/cli/cmdValidateBuiltTypes.mjs +49 -0
  78. package/src/cli/index.mjs +6 -2
  79. package/src/cli/packageJson.d.ts +729 -0
  80. package/src/eslint/baseConfig.mjs +96 -78
  81. package/src/eslint/docsConfig.mjs +26 -13
  82. package/src/eslint/extensions.mjs +8 -8
  83. package/src/eslint/jsonConfig.mjs +40 -0
  84. package/src/eslint/material-ui/config.mjs +8 -9
  85. package/src/eslint/material-ui/rules/mui-name-matches-component-name.mjs +4 -2
  86. package/src/eslint/material-ui/rules/rules-of-use-theme-variants.mjs +2 -1
  87. package/src/eslint/testConfig.mjs +72 -66
  88. package/src/stylelint/index.mjs +46 -0
  89. package/src/untyped-plugins.d.ts +13 -0
  90. package/src/{cli → utils}/babel.mjs +10 -3
  91. package/src/utils/build.mjs +27 -1
  92. package/src/utils/changelog.mjs +157 -0
  93. package/src/utils/credentials.mjs +71 -0
  94. package/src/utils/extractErrorCodes.mjs +2 -2
  95. package/src/utils/git.mjs +67 -0
  96. package/src/utils/github.mjs +263 -0
  97. package/src/{cli → utils}/pnpm.mjs +23 -13
  98. package/src/{cli → utils}/typescript.mjs +13 -7
  99. package/src/cli/cmdJsonLint.mjs +0 -69
@@ -3,31 +3,371 @@
3
3
  /* eslint-disable no-console */
4
4
 
5
5
  /**
6
- * @typedef {import('./pnpm.mjs').PublicPackage} PublicPackage
7
- * @typedef {import('./pnpm.mjs').VersionInfo} VersionInfo
8
- * @typedef {import('./pnpm.mjs').PublishOptions} PublishOptions
6
+ * @typedef {import('../utils/pnpm.mjs').PublicPackage} PublicPackage
7
+ * @typedef {import('../utils/pnpm.mjs').VersionInfo} VersionInfo
8
+ * @typedef {import('../utils/pnpm.mjs').PublishOptions} PublishOptions
9
9
  */
10
10
 
11
+ import path from 'node:path';
12
+ import { createActionAuth } from '@octokit/auth-action';
13
+ import { Octokit } from '@octokit/rest';
11
14
  import { $ } from 'execa';
12
15
  import * as semver from 'semver';
13
16
 
14
- /**
15
- * @typedef {Object} Args
16
- * @property {boolean} [dryRun] - Whether to run in dry-run mode
17
- */
18
-
19
17
  import {
20
- getWorkspacePackages,
21
18
  getPackageVersionInfo,
19
+ getWorkspacePackages,
22
20
  publishPackages,
23
21
  readPackageJson,
24
- writePackageJson,
25
- getCurrentGitSha,
26
22
  semverMax,
27
- } from './pnpm.mjs';
23
+ writePackageJson,
24
+ } from '../utils/pnpm.mjs';
25
+ import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
26
+
27
+ /**
28
+ * @typedef {Object} Args
29
+ * @property {boolean} [dryRun] - Whether to run in dry-run mode
30
+ * @property {boolean} [githubRelease] - Whether to create GitHub releases for canary packages
31
+ */
28
32
 
29
33
  const CANARY_TAG = 'canary';
30
34
 
35
+ /**
36
+ * Get Octokit instance with authentication
37
+ * @returns {Octokit} Authenticated Octokit instance
38
+ */
39
+ function getOctokit() {
40
+ return new Octokit({ authStrategy: createActionAuth });
41
+ }
42
+
43
+ /**
44
+ * @typedef {Object} Commit
45
+ * @property {string} sha - Commit SHA
46
+ * @property {string} message - Commit message
47
+ * @property {string} author - Commit author
48
+ */
49
+
50
+ /**
51
+ * @param {Object} param0
52
+ * @param {string} param0.packagePath
53
+ * @returns {Promise<Commit[]>} Commits between the tag and current HEAD for the package
54
+ */
55
+ async function fetchCommitsForPackage({ packagePath }) {
56
+ /**
57
+ * @type {Commit[]}
58
+ */
59
+ const results = [];
60
+ const fieldSeparator = '\u001f'; // ASCII unit separator is extremely unlikely to appear in git metadata
61
+ const formatArg = '--format=%H%x1f%s%x1f%an%x1f%ae'; // SHA, subject, author name, author email separated by unit separator
62
+ const res = $`git log --oneline --no-decorate ${
63
+ // to avoid escaping by execa
64
+ [formatArg]
65
+ } ${CANARY_TAG}..HEAD -- ${packagePath}`;
66
+ for await (const line of res) {
67
+ const commitLine = line.trimEnd();
68
+ if (!commitLine) {
69
+ continue;
70
+ }
71
+ const parts = commitLine.split(fieldSeparator);
72
+ if (parts.length < 3) {
73
+ console.error(`Failed to parse commit log line: ${commitLine}`);
74
+ continue;
75
+ }
76
+ const [sha, message, commitAuthor, commitEmail] = parts;
77
+ let author = commitAuthor;
78
+ // try to get github username from email
79
+ if (commitEmail) {
80
+ const emailUsername = commitEmail.split('@')[0];
81
+ if (emailUsername) {
82
+ const [, githubUserName] = emailUsername.split('+');
83
+ if (githubUserName) {
84
+ author = `@${githubUserName}`;
85
+ }
86
+ }
87
+ }
88
+ results.push({ sha, message, author });
89
+ }
90
+ return results;
91
+ }
92
+
93
+ const AUTHOR_EXCLUDE_LIST = ['renovate[bot]', 'dependabot[bot]'];
94
+
95
+ /**
96
+ * @param {string} message
97
+ * @returns {string}
98
+ */
99
+ function cleanupCommitMessage(message) {
100
+ // AI generated: clean up commit message by removing leading bracketed tokens except [breaking]
101
+ let msg = message || '';
102
+
103
+ // Extract and remove leading bracketed tokens like "[foo][bar] message"
104
+ const tokens = [];
105
+ const bracketRe = /^\s*\[([^\]]+)\]\s*/;
106
+ let match = msg.match(bracketRe);
107
+ while (match) {
108
+ tokens.push(match[1]);
109
+ msg = msg.slice(match[0].length);
110
+ match = msg.match(bracketRe);
111
+ }
112
+ msg = msg.trim();
113
+
114
+ // If any of the leading tokens is "breaking" keep that token (preserve original casing)
115
+ const breakingToken = tokens.find((t) => t.toLowerCase() === 'breaking');
116
+ const prefix = breakingToken ? `[${breakingToken}]${msg ? ' ' : ''}` : '';
117
+
118
+ return `${prefix}${msg}`.trim();
119
+ }
120
+
121
+ async function getPackageToDependencyMap() {
122
+ /**
123
+ * @type {(PublicPackage & { dependencies: Record<string, unknown>; private: boolean; })[]}
124
+ */
125
+ const packagesWithDeps = JSON.parse(
126
+ (await $`pnpm ls -r --json --exclude-peers --only-projects --prod`).stdout,
127
+ );
128
+ /** @type {Record<string, string[]>} */
129
+ const directPkgDependencies = packagesWithDeps
130
+ .filter((pkg) => !pkg.private)
131
+ .reduce((acc, pkg) => {
132
+ if (!pkg.name) {
133
+ return acc;
134
+ }
135
+ const deps = Object.keys(pkg.dependencies || {});
136
+ if (!deps.length) {
137
+ return acc;
138
+ }
139
+ acc[pkg.name] = deps;
140
+ return acc;
141
+ }, /** @type {Record<string, string[]>} */ ({}));
142
+ return directPkgDependencies;
143
+ }
144
+
145
+ /**
146
+ * @param {Record<string, string[]>} pkgGraph
147
+ */
148
+ function resolveTransitiveDependencies(pkgGraph = {}) {
149
+ // Compute transitive (nested) dependencies limited to workspace packages and avoid cycles.
150
+ const workspacePkgNames = new Set(Object.keys(pkgGraph));
151
+ const nestedMap = /** @type {Record<string, string[]>} */ ({});
152
+
153
+ /**
154
+ *
155
+ * @param {string} pkgName
156
+ * @returns {string[]}
157
+ */
158
+ const getTransitiveDeps = (pkgName) => {
159
+ /**
160
+ * @type {Set<string>}
161
+ */
162
+ const seen = new Set();
163
+ const stack = (pkgGraph[pkgName] || []).slice();
164
+
165
+ while (stack.length) {
166
+ const dep = stack.pop();
167
+ if (!dep || seen.has(dep)) {
168
+ continue;
169
+ }
170
+ // Only consider workspace packages for transitive expansion
171
+ if (!workspacePkgNames.has(dep)) {
172
+ // still record external deps as direct deps but don't traverse into them
173
+ seen.add(dep);
174
+ continue;
175
+ }
176
+ seen.add(dep);
177
+ const children = pkgGraph[dep] || [];
178
+ for (const c of children) {
179
+ if (!seen.has(c)) {
180
+ stack.push(c);
181
+ }
182
+ }
183
+ }
184
+
185
+ return Array.from(seen);
186
+ };
187
+
188
+ for (const name of Object.keys(pkgGraph)) {
189
+ nestedMap[name] = getTransitiveDeps(name);
190
+ }
191
+
192
+ return nestedMap;
193
+ }
194
+
195
+ /**
196
+ * Prepare changelog data for packages using GitHub API
197
+ * @param {PublicPackage[]} packagesToPublish - Packages that will be published
198
+ * @param {PublicPackage[]} allPackages - All packages in the repository
199
+ * @param {Map<string, string>} canaryVersions - Map of package names to their canary versions
200
+ * @returns {Promise<Map<string, string[]>>} Map of package names to their changelogs
201
+ */
202
+ async function prepareChangelogsFromGitCli(packagesToPublish, allPackages, canaryVersions) {
203
+ /**
204
+ * @type {Map<string, string[]>}
205
+ */
206
+ const changelogs = new Map();
207
+
208
+ await Promise.all(
209
+ packagesToPublish.map(async (pkg) => {
210
+ const commits = await fetchCommitsForPackage({ packagePath: pkg.path });
211
+ if (commits.length > 0) {
212
+ console.log(`Found ${commits.length} commits for package ${pkg.name}`);
213
+ }
214
+ const changeLogStrs = commits
215
+ // Exclude commits authored by bots
216
+ .filter(
217
+ // We want to allow commits from copilot or other AI tools, so only filter known bots
218
+ (commit) => !AUTHOR_EXCLUDE_LIST.includes(commit.author),
219
+ )
220
+ .map((commit) => `- ${cleanupCommitMessage(commit.message)} by ${commit.author}`);
221
+
222
+ if (changeLogStrs.length > 0) {
223
+ changelogs.set(pkg.name, changeLogStrs);
224
+ }
225
+ }),
226
+ );
227
+ // Second pass: check for dependency updates in other packages not part of git history
228
+ const pkgDependencies = await getPackageToDependencyMap();
229
+ const transitiveDependencies = resolveTransitiveDependencies(pkgDependencies);
230
+
231
+ for (let i = 0; i < allPackages.length; i += 1) {
232
+ const pkg = allPackages[i];
233
+ const depsToPublish = (transitiveDependencies[pkg.name] ?? []).filter((dep) =>
234
+ packagesToPublish.some((p) => p.name === dep),
235
+ );
236
+ if (depsToPublish.length === 0) {
237
+ continue;
238
+ }
239
+ const changelog = changelogs.get(pkg.name) ?? [];
240
+ changelog.push('- Updated dependencies:');
241
+ depsToPublish.forEach((dep) => {
242
+ const depVersion = canaryVersions.get(dep);
243
+ if (depVersion) {
244
+ changelog.push(` - Bumped \`${dep}@${depVersion}\``);
245
+ }
246
+ });
247
+ }
248
+ return changelogs;
249
+ }
250
+
251
+ /**
252
+ * Prepare changelog data for packages
253
+ * @param {PublicPackage[]} packagesToPublish - Packages that will be published
254
+ * @param {PublicPackage[]} allPackages - All packages in the repository
255
+ * @param {Map<string, string>} canaryVersions - Map of package names to their canary versions
256
+ * @returns {Promise<Map<string, string[]>>} Map of package names to their changelogs
257
+ */
258
+ async function prepareChangelogsForPackages(packagesToPublish, allPackages, canaryVersions) {
259
+ console.log('\n📝 Preparing changelogs for packages...');
260
+
261
+ const repoInfo = await getRepositoryInfo();
262
+ console.log(`📂 Repository: ${repoInfo.owner}/${repoInfo.repo}`);
263
+
264
+ /**
265
+ * @type {Map<string, string[]>}
266
+ */
267
+ const changelogs = await prepareChangelogsFromGitCli(
268
+ packagesToPublish,
269
+ allPackages,
270
+ canaryVersions,
271
+ );
272
+
273
+ // Log changelog content for each package
274
+ for (const pkg of packagesToPublish) {
275
+ const version = canaryVersions.get(pkg.name);
276
+ if (!version) {
277
+ continue;
278
+ }
279
+
280
+ const changelog = changelogs.get(pkg.name) || [];
281
+ console.log(`\n📦 ${pkg.name}@${version}`);
282
+ if (changelog.length > 0) {
283
+ console.log(
284
+ ` Changelog:\n${changelog.map((/** @type {string} */ line) => ` ${line}`).join('\n')}`,
285
+ );
286
+ }
287
+ }
288
+
289
+ console.log('\n✅ Changelogs prepared successfully');
290
+ return changelogs;
291
+ }
292
+
293
+ /**
294
+ * Create GitHub releases and tags for published packages
295
+ * @param {PublicPackage[]} publishedPackages - Packages that were published
296
+ * @param {Map<string, string>} canaryVersions - Map of package names to their canary versions
297
+ * @param {Map<string, string[]>} changelogs - Map of package names to their changelogs
298
+ * @param {{dryRun?: boolean}} options - Publishing options
299
+ * @returns {Promise<void>}
300
+ */
301
+ async function createGitHubReleasesForPackages(
302
+ publishedPackages,
303
+ canaryVersions,
304
+ changelogs,
305
+ options,
306
+ ) {
307
+ console.log('\n🚀 Creating GitHub releases and tags for published packages...');
308
+
309
+ const repoInfo = await getRepositoryInfo();
310
+ const gitSha = await getCurrentGitSha();
311
+ const octokit = getOctokit();
312
+
313
+ for (const pkg of publishedPackages) {
314
+ const version = canaryVersions.get(pkg.name);
315
+ if (!version) {
316
+ console.log(`⚠️ No version found for ${pkg.name}, skipping...`);
317
+ continue;
318
+ }
319
+
320
+ const changelog = changelogs.get(pkg.name);
321
+ if (!changelog) {
322
+ console.log(`⚠️ No changelog found for ${pkg.name}, skipping release creation...`);
323
+ continue;
324
+ }
325
+ const tagName = `${pkg.name}@${version}`;
326
+ const releaseName = tagName;
327
+
328
+ console.log(`\n📦 Processing ${pkg.name}@${version}...`);
329
+
330
+ // Create git tag
331
+ if (options.dryRun) {
332
+ console.log(`🏷️ Would create and push git tag: ${tagName} (dry-run)`);
333
+ console.log(`📝 Would publish a Github release:`);
334
+ console.log(` - Name: ${releaseName}`);
335
+ console.log(` - Tag: ${tagName}`);
336
+ console.log(` - Body:\n${changelog.join('\n')}`);
337
+ } else {
338
+ // eslint-disable-next-line no-await-in-loop
339
+ await $({
340
+ env: {
341
+ ...process.env,
342
+ GIT_COMMITTER_NAME: 'Code infra',
343
+ GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
344
+ },
345
+ })`git tag -a ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
346
+
347
+ // eslint-disable-next-line no-await-in-loop
348
+ await $`git push origin ${tagName}`;
349
+ console.log(`✅ Created and pushed git tag: ${tagName}`);
350
+
351
+ // Create GitHub release
352
+ // eslint-disable-next-line no-await-in-loop
353
+ const res = await octokit.repos.createRelease({
354
+ owner: repoInfo.owner,
355
+ repo: repoInfo.repo,
356
+ tag_name: tagName,
357
+ target_commitish: gitSha,
358
+ name: releaseName,
359
+ body: changelog.join('\n'),
360
+ draft: false,
361
+ prerelease: true, // Mark as prerelease since these are canary versions
362
+ });
363
+
364
+ console.log(`✅ Created GitHub release: ${releaseName} at ${res.data.html_url}`);
365
+ }
366
+ }
367
+
368
+ console.log('\n✅ Finished creating GitHub releases');
369
+ }
370
+
31
371
  /**
32
372
  * Check if the canary git tag exists
33
373
  * @returns {Promise<string|null>} Canary tag name if exists, null otherwise
@@ -66,11 +406,13 @@ async function createCanaryTag(dryRun = false) {
66
406
  }
67
407
 
68
408
  /**
69
- * Publish canary versions with updated dependencies
409
+ * Publish canary versions with updated dependencies. A big assumption here is that
410
+ * all packages are already built before calling this function.
411
+ *
70
412
  * @param {PublicPackage[]} packagesToPublish - Packages that need canary publishing
71
413
  * @param {PublicPackage[]} allPackages - All workspace packages
72
414
  * @param {Map<string, VersionInfo>} packageVersionInfo - Version info map
73
- * @param {PublishOptions} [options={}] - Publishing options
415
+ * @param {PublishOptions & {githubRelease?: boolean}} [options={}] - Publishing options
74
416
  * @returns {Promise<void>}
75
417
  */
76
418
  async function publishCanaryVersions(
@@ -90,7 +432,6 @@ async function publishCanaryVersions(
90
432
 
91
433
  const gitSha = await getCurrentGitSha();
92
434
  const canaryVersions = new Map();
93
- const originalPackageJsons = new Map();
94
435
 
95
436
  // First pass: determine canary version numbers for all packages
96
437
  const changedPackageNames = new Set(packagesToPublish.map((pkg) => pkg.name));
@@ -117,41 +458,44 @@ async function publishCanaryVersions(
117
458
  }
118
459
 
119
460
  // Second pass: read and update ALL package.json files in parallel
461
+ // Packages are already built at this point.
120
462
  const packageUpdatePromises = allPackages.map(async (pkg) => {
121
- const originalPackageJson = await readPackageJson(pkg.path);
463
+ let basePkgJson = await readPackageJson(pkg.path);
464
+ let pkgJsonDirectory = pkg.path;
465
+ if (basePkgJson.publishConfig?.directory) {
466
+ pkgJsonDirectory = path.join(pkg.path, basePkgJson.publishConfig.directory);
467
+ basePkgJson = await readPackageJson(pkgJsonDirectory);
468
+ }
122
469
 
123
470
  const canaryVersion = canaryVersions.get(pkg.name);
124
471
  if (canaryVersion) {
125
472
  const updatedPackageJson = {
126
- ...originalPackageJson,
473
+ ...basePkgJson,
127
474
  version: canaryVersion,
128
475
  gitSha,
129
476
  };
130
-
131
- await writePackageJson(pkg.path, updatedPackageJson);
477
+ await writePackageJson(pkgJsonDirectory, updatedPackageJson);
132
478
  console.log(`📝 Updated ${pkg.name} package.json to ${canaryVersion}`);
133
479
  }
134
-
135
- return { pkg, originalPackageJson };
480
+ return { pkg, basePkgJson, pkgJsonDirectory };
136
481
  });
137
482
 
138
483
  const updateResults = await Promise.all(packageUpdatePromises);
139
484
 
140
- // Build the original package.json map
141
- for (const { pkg, originalPackageJson } of updateResults) {
142
- originalPackageJsons.set(pkg.name, originalPackageJson);
485
+ // Prepare changelogs before building and publishing (so it can error out early if there are issues)
486
+ /**
487
+ * @type {Map<string, string[]>}
488
+ */
489
+ let changelogs = new Map();
490
+ if (options.githubRelease) {
491
+ changelogs = await prepareChangelogsForPackages(packagesToPublish, allPackages, canaryVersions);
143
492
  }
144
493
 
145
- // Run release build after updating package.json files
146
- console.log('\n🔨 Running release build...');
147
- await $({ stdio: 'inherit' })`pnpm release:build`;
148
- console.log('✅ Release build completed successfully');
149
-
150
494
  // Third pass: publish only the changed packages using recursive publish
151
495
  let publishSuccess = false;
152
496
  try {
153
497
  console.log(`📤 Publishing ${packagesToPublish.length} canary versions...`);
154
- await publishPackages(packagesToPublish, 'canary', { ...options, noGitChecks: true });
498
+ await publishPackages(packagesToPublish, { ...options, noGitChecks: true, tag: CANARY_TAG });
155
499
 
156
500
  packagesToPublish.forEach((pkg) => {
157
501
  const canaryVersion = canaryVersions.get(pkg.name);
@@ -161,9 +505,11 @@ async function publishCanaryVersions(
161
505
  } finally {
162
506
  // Always restore original package.json files in parallel
163
507
  console.log('\n🔄 Restoring original package.json files...');
164
- const restorePromises = allPackages.map(async (pkg) => {
165
- const originalPackageJson = originalPackageJsons.get(pkg.name);
166
- await writePackageJson(pkg.path, originalPackageJson);
508
+ const restorePromises = updateResults.map(async ({ pkg, basePkgJson, pkgJsonDirectory }) => {
509
+ // no need to restore package.json files in build directories
510
+ if (pkgJsonDirectory === pkg.path) {
511
+ await writePackageJson(pkg.path, basePkgJson);
512
+ }
167
513
  });
168
514
 
169
515
  await Promise.all(restorePromises);
@@ -172,6 +518,12 @@ async function publishCanaryVersions(
172
518
  if (publishSuccess) {
173
519
  // Create/update the canary tag after successful publish
174
520
  await createCanaryTag(options.dryRun);
521
+
522
+ // Create GitHub releases if requested
523
+ if (options.githubRelease) {
524
+ await createGitHubReleasesForPackages(packagesToPublish, canaryVersions, changelogs, options);
525
+ }
526
+
175
527
  console.log('\n🎉 All canary versions published successfully!');
176
528
  }
177
529
  }
@@ -180,21 +532,31 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
180
532
  command: 'publish-canary',
181
533
  describe: 'Publish canary packages to npm',
182
534
  builder: (yargs) => {
183
- return yargs.option('dry-run', {
184
- type: 'boolean',
185
- default: false,
186
- description: 'Run in dry-run mode without publishing',
187
- });
535
+ return yargs
536
+ .option('dry-run', {
537
+ type: 'boolean',
538
+ default: false,
539
+ description: 'Run in dry-run mode without publishing',
540
+ })
541
+ .option('github-release', {
542
+ type: 'boolean',
543
+ default: false,
544
+ description: 'Create GitHub releases for published packages',
545
+ });
188
546
  },
189
547
  handler: async (argv) => {
190
- const { dryRun = false } = argv;
548
+ const { dryRun = false, githubRelease = false } = argv;
191
549
 
192
- const options = { dryRun };
550
+ const options = { dryRun, githubRelease };
193
551
 
194
552
  if (dryRun) {
195
553
  console.log('🧪 Running in DRY RUN mode - no actual publishing will occur\n');
196
554
  }
197
555
 
556
+ if (githubRelease) {
557
+ console.log('📝 GitHub releases will be created for published packages\n');
558
+ }
559
+
198
560
  // Always get all packages first
199
561
  console.log('🔍 Discovering all workspace packages...');
200
562
  const allPackages = await getWorkspacePackages({ publicOnly: true });
@@ -207,16 +569,12 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
207
569
  // Check for canary tag to determine selective publishing
208
570
  const canaryTag = await getLastCanaryTag();
209
571
 
210
- console.log(
211
- canaryTag
212
- ? '🔍 Checking for packages changed since canary tag...'
213
- : '🔍 No canary tag found, will publish all packages',
214
- );
572
+ console.log('🔍 Checking for packages changed since canary tag...');
215
573
  const packages = canaryTag
216
574
  ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true })
217
575
  : allPackages;
218
576
 
219
- console.log(`📋 Found ${packages.length} packages that need canary publishing:`);
577
+ console.log(`📋 Found ${packages.length} packages(s) for canary publishing:`);
220
578
  packages.forEach((pkg) => {
221
579
  console.log(` • ${pkg.name}@${pkg.version}`);
222
580
  });
@@ -0,0 +1,86 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import confirm from '@inquirer/confirm';
4
+ import chalk from 'chalk';
5
+ import { $ } from 'execa';
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+
10
+ import { getWorkspacePackages } from '../utils/pnpm.mjs';
11
+
12
+ /**
13
+ * @typedef {Object} Args
14
+ * @property {boolean} [dryRun] If true, will only log the commands without executing them
15
+ */
16
+
17
+ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
18
+ command: 'publish-new-package [pkg...]',
19
+ describe: 'Publish new empty package(s) to the npm registry.',
20
+ builder: (yargs) =>
21
+ yargs.option('dryRun', {
22
+ type: 'boolean',
23
+ default: false,
24
+ description: 'If true, will only log the commands without executing them.',
25
+ }),
26
+ async handler(args) {
27
+ console.log(`🔍 Detecting new packages to publish in workspace...`);
28
+ const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
29
+
30
+ if (!newPackages.length) {
31
+ console.log('No new packages to publish.');
32
+ return;
33
+ }
34
+
35
+ console.log(`Found ${newPackages.map((pkg) => pkg.name).join(', ')} to publish.`);
36
+
37
+ const answer = await confirm({
38
+ message: `Do you want to publish ${newPackages.length} new package(s) to the npm registry?`,
39
+ });
40
+
41
+ if (!answer) {
42
+ return;
43
+ }
44
+
45
+ await Promise.all(
46
+ newPackages.map(async (pkg) => {
47
+ const newPkgDir = await fs.mkdtemp(path.join(os.tmpdir(), 'publish-new-package-'));
48
+ try {
49
+ await fs.mkdir(newPkgDir, { recursive: true });
50
+ const packageJson = {
51
+ name: pkg.name,
52
+ version: '0.0.1',
53
+ };
54
+ await fs.writeFile(
55
+ path.join(newPkgDir, 'package.json'),
56
+ `${JSON.stringify(packageJson, null, 2)}\n`,
57
+ );
58
+ /**
59
+ * @type {string[]}
60
+ */
61
+ const publishArgs = [];
62
+
63
+ if (args.dryRun) {
64
+ publishArgs.push('--dry-run');
65
+ }
66
+ await $({
67
+ cwd: newPkgDir,
68
+ })`npm publish --access public --tag=canary ${publishArgs}`;
69
+ console.log(
70
+ `✅ ${args.dryRun ? '[Dry run] ' : ''}Published ${chalk.bold(`${pkg.name}@${packageJson.version}`)} to npm registry.`,
71
+ );
72
+ } finally {
73
+ await fs.rm(newPkgDir, { recursive: true, force: true });
74
+ }
75
+ }),
76
+ );
77
+
78
+ const trustedPublisherLinks = newPackages
79
+ .map((pkg) => `https://www.npmjs.com/package/${pkg.name}/access`)
80
+ .join('\n');
81
+ console.log(`
82
+ 📝 Please ensure that the ${chalk.underline(chalk.bold('Trusted Publishers'))} settings are configured for the new package(s):
83
+ ${trustedPublisherLinks}
84
+ Read how to do that here - https://github.com/mui/mui-public/blob/master/packages/code-infra/README.md#adding-and-publishing-new-packages`);
85
+ },
86
+ });
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import * as semver from 'semver';
7
7
  import { $ } from 'execa';
8
- import { resolveVersion, findDependencyVersionFromSpec } from './pnpm.mjs';
8
+ import { resolveVersion, findDependencyVersionFromSpec } from '../utils/pnpm.mjs';
9
9
 
10
10
  /**
11
11
  * @typedef {Object} Args
@@ -51,6 +51,22 @@ async function processPackageOverride(packageSpec) {
51
51
  if (reactMajor === 17) {
52
52
  overrides['@testing-library/react'] = await resolveVersion('@testing-library/react@^12.1.0');
53
53
  }
54
+ } else if (packageName === '@mui/material') {
55
+ // Special case for MUI - also override related packages
56
+ overrides['@mui/material'] = await resolveVersion(`@mui/material@${version}`);
57
+ overrides['@mui/system'] = await resolveVersion(`@mui/system@${version}`);
58
+ overrides['@mui/icons-material'] = await resolveVersion(`@mui/icons-material@${version}`);
59
+ overrides['@mui/utils'] = await resolveVersion(`@mui/utils@${version}`);
60
+ overrides['@mui/material-nextjs'] = await resolveVersion(`@mui/material-nextjs@${version}`);
61
+
62
+ const latest = await resolveVersion(`@mui/material@latest`);
63
+ const latestMajor = semver.major(latest);
64
+ const muiMajor = semver.major(overrides['@mui/material']);
65
+ if (muiMajor < latestMajor) {
66
+ overrides['@mui/lab'] = await resolveVersion(`@mui/lab@latest-v${muiMajor}`);
67
+ } else {
68
+ overrides['@mui/lab'] = await resolveVersion(`@mui/lab@latest`);
69
+ }
54
70
  } else {
55
71
  // Generic case for other packages
56
72
  overrides[packageName] = await resolveVersion(packageSpec);