@mui/internal-code-infra 0.0.4-canary.1 → 0.0.4-canary.100

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 (146) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +22 -8
  3. package/build/babel-config.d.mts +11 -3
  4. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  5. package/build/brokenLinksChecker/index.d.mts +192 -2
  6. package/build/changelog/fetchChangelogs.d.mts +3 -0
  7. package/build/changelog/types.d.ts +3 -1
  8. package/build/cli/cmdArgosPush.d.mts +8 -2
  9. package/build/cli/cmdBuild.d.mts +48 -2
  10. package/build/cli/cmdCopyFiles.d.mts +14 -2
  11. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  12. package/build/cli/cmdGenerateChangelog.d.mts +17 -2
  13. package/build/cli/cmdGithubAuth.d.mts +2 -2
  14. package/build/cli/cmdListWorkspaces.d.mts +16 -2
  15. package/build/cli/cmdNetlifyIgnore.d.mts +12 -2
  16. package/build/cli/cmdPublish.d.mts +24 -4
  17. package/build/cli/cmdPublishCanary.d.mts +21 -2
  18. package/build/cli/cmdPublishNewPackage.d.mts +10 -2
  19. package/build/cli/cmdRenameScope.d.mts +18 -0
  20. package/build/cli/cmdSetVersionOverrides.d.mts +5 -2
  21. package/build/cli/cmdVale.d.mts +46 -0
  22. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  23. package/build/eslint/baseConfig.d.mts +3 -1
  24. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  25. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  26. package/build/eslint/mui/rules/no-floating-cleanup.d.mts +5 -0
  27. package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
  28. package/build/eslint/mui/rules/no-presentation-role.d.mts +5 -0
  29. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +8 -2
  30. package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
  31. package/build/remark/config.d.mts +43 -0
  32. package/build/remark/createLintTester.d.mts +10 -0
  33. package/build/remark/firstBlockHeading.d.mts +4 -0
  34. package/build/remark/gitDiff.d.mts +2 -0
  35. package/build/remark/noSpaceInLinks.d.mts +2 -0
  36. package/build/remark/straightQuotes.d.mts +2 -0
  37. package/build/remark/tableAlignment.d.mts +2 -0
  38. package/build/remark/terminalLanguage.d.mts +2 -0
  39. package/build/utils/babel.d.mts +7 -1
  40. package/build/utils/build.d.mts +59 -37
  41. package/build/utils/extractErrorCodes.d.mts +9 -0
  42. package/build/utils/git.d.mts +16 -0
  43. package/build/utils/github.d.mts +13 -1
  44. package/build/utils/pnpm.d.mts +200 -7
  45. package/build/utils/testUtils.d.mts +28 -0
  46. package/build/utils/typescript.d.mts +6 -16
  47. package/package.json +83 -56
  48. package/src/babel-config.mjs +9 -3
  49. package/src/brokenLinksChecker/crawlWorker.mjs +240 -0
  50. package/src/brokenLinksChecker/index.mjs +263 -188
  51. package/src/build-env.d.ts +13 -0
  52. package/src/changelog/fetchChangelogs.mjs +8 -2
  53. package/src/changelog/renderChangelog.mjs +1 -1
  54. package/src/changelog/types.ts +3 -1
  55. package/src/cli/cmdBuild.mjs +51 -85
  56. package/src/cli/cmdListWorkspaces.mjs +16 -3
  57. package/src/cli/cmdNetlifyIgnore.mjs +34 -93
  58. package/src/cli/cmdPublish.mjs +105 -36
  59. package/src/cli/cmdPublishCanary.mjs +145 -109
  60. package/src/cli/cmdPublishNewPackage.mjs +31 -8
  61. package/src/cli/cmdRenameScope.mjs +76 -0
  62. package/src/cli/cmdSetVersionOverrides.mjs +8 -9
  63. package/src/cli/cmdVale.mjs +513 -0
  64. package/src/cli/index.mjs +4 -0
  65. package/src/cli/packageJson.d.ts +2 -3
  66. package/src/eslint/baseConfig.mjs +51 -19
  67. package/src/eslint/docsConfig.mjs +2 -1
  68. package/src/eslint/mui/config.mjs +24 -4
  69. package/src/eslint/mui/index.mjs +6 -0
  70. package/src/eslint/mui/rules/no-floating-cleanup.mjs +187 -0
  71. package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
  72. package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
  73. package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
  74. package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
  75. package/src/estree-typescript.d.ts +1 -1
  76. package/src/remark/config.mjs +157 -0
  77. package/src/remark/createLintTester.mjs +19 -0
  78. package/src/remark/firstBlockHeading.mjs +87 -0
  79. package/src/remark/gitDiff.mjs +43 -0
  80. package/src/remark/noSpaceInLinks.mjs +42 -0
  81. package/src/remark/straightQuotes.mjs +31 -0
  82. package/src/remark/tableAlignment.mjs +23 -0
  83. package/src/remark/terminalLanguage.mjs +19 -0
  84. package/src/stylelint/index.mjs +2 -2
  85. package/src/untyped-plugins.d.ts +14 -14
  86. package/src/utils/build.mjs +604 -270
  87. package/src/utils/git.mjs +27 -7
  88. package/src/utils/pnpm.mjs +549 -26
  89. package/src/utils/testUtils.mjs +53 -0
  90. package/src/utils/typescript.mjs +23 -42
  91. package/vale/.vale.ini +1 -0
  92. package/vale/styles/MUI/CorrectReferenceAllCases.yml +42 -0
  93. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  94. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  95. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  96. package/vale/styles/MUI/NoBritish.yml +112 -0
  97. package/vale/styles/MUI/NoCompanyName.yml +17 -0
  98. package/build/markdownlint/duplicate-h1.d.mts +0 -56
  99. package/build/markdownlint/git-diff.d.mts +0 -11
  100. package/build/markdownlint/index.d.mts +0 -52
  101. package/build/markdownlint/straight-quotes.d.mts +0 -11
  102. package/build/markdownlint/table-alignment.d.mts +0 -11
  103. package/build/markdownlint/terminal-language.d.mts +0 -11
  104. package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +0 -20
  105. package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +0 -22
  106. package/src/brokenLinksChecker/__fixtures__/static-site/example.md +0 -20
  107. package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +0 -21
  108. package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +0 -17
  109. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +0 -28
  110. package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +0 -5
  111. package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +0 -21
  112. package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +0 -20
  113. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +0 -20
  114. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +0 -24
  115. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +0 -28
  116. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +0 -19
  117. package/src/brokenLinksChecker/__fixtures__/static-site/unclosed-tags.html +0 -1
  118. package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +0 -20
  119. package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +0 -31
  120. package/src/brokenLinksChecker/index.test.ts +0 -261
  121. package/src/changelog/categorizeCommits.test.ts +0 -319
  122. package/src/changelog/filterCommits.test.ts +0 -363
  123. package/src/changelog/parseCommitLabels.test.ts +0 -509
  124. package/src/changelog/renderChangelog.test.ts +0 -378
  125. package/src/changelog/sortSections.test.ts +0 -199
  126. package/src/eslint/mui/rules/add-undef-to-optional.test.mjs +0 -361
  127. package/src/eslint/mui/rules/consistent-production-guard.test.mjs +0 -162
  128. package/src/eslint/mui/rules/disallow-active-elements-as-key-event-target.test.mjs +0 -66
  129. package/src/eslint/mui/rules/disallow-react-api-in-server-components.test.mjs +0 -305
  130. package/src/eslint/mui/rules/docgen-ignore-before-comment.test.mjs +0 -52
  131. package/src/eslint/mui/rules/flatten-parentheses.test.mjs +0 -245
  132. package/src/eslint/mui/rules/mui-name-matches-component-name.test.mjs +0 -247
  133. package/src/eslint/mui/rules/no-empty-box.test.mjs +0 -40
  134. package/src/eslint/mui/rules/no-styled-box.test.mjs +0 -73
  135. package/src/eslint/mui/rules/require-dev-wrapper.test.mjs +0 -265
  136. package/src/eslint/mui/rules/rules-of-use-theme-variants.test.mjs +0 -149
  137. package/src/eslint/mui/rules/straight-quotes.test.mjs +0 -67
  138. package/src/markdownlint/duplicate-h1.mjs +0 -69
  139. package/src/markdownlint/git-diff.mjs +0 -31
  140. package/src/markdownlint/index.mjs +0 -62
  141. package/src/markdownlint/straight-quotes.mjs +0 -26
  142. package/src/markdownlint/table-alignment.mjs +0 -42
  143. package/src/markdownlint/terminal-language.mjs +0 -19
  144. package/src/utils/build.test.mjs +0 -705
  145. package/src/utils/template.test.mjs +0 -133
  146. package/src/utils/typescript.test.mjs +0 -380
@@ -5,6 +5,7 @@
5
5
  /**
6
6
  * @typedef {import('../utils/pnpm.mjs').PublicPackage} PublicPackage
7
7
  * @typedef {import('../utils/pnpm.mjs').PublishOptions} PublishOptions
8
+ * @typedef {import('../utils/pnpm.mjs').PublishSummaryEntry} PublishSummaryEntry
8
9
  */
9
10
 
10
11
  import select from '@inquirer/select';
@@ -17,8 +18,14 @@ import * as fs from 'node:fs/promises';
17
18
  import * as semver from 'semver';
18
19
 
19
20
  import { persistentAuthStrategy } from '../utils/github.mjs';
20
- import { getWorkspacePackages, publishPackages } from '../utils/pnpm.mjs';
21
- import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
21
+ import {
22
+ getPackagesNeedingManualPublish,
23
+ getReleaseVersion,
24
+ getWorkspacePackages,
25
+ publishPackages,
26
+ validatePublishDependencies,
27
+ } from '../utils/pnpm.mjs';
28
+ import { getCurrentGitSha, getRepositoryInfo, remoteGitTagExists } from '../utils/git.mjs';
22
29
 
23
30
  const isCI = envCI().isCi;
24
31
 
@@ -33,18 +40,9 @@ function getOctokit() {
33
40
  * @property {string} tag NPM dist tag to publish to
34
41
  * @property {boolean} ci Runs in CI environment
35
42
  * @property {string} [sha] Git SHA to use for the GitHub release workflow (local only)
43
+ * @property {string[]} [filter] Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.
36
44
  */
37
45
 
38
- /**
39
- * Get the version to release from the root package.json
40
- * @returns {Promise<string | null>} Version string
41
- */
42
- async function getReleaseVersion() {
43
- const result = await $`pnpm pkg get version`;
44
- const version = JSON.parse(result.stdout.trim());
45
- return semver.valid(version);
46
- }
47
-
48
46
  /**
49
47
  * Parse changelog to extract content for a specific version
50
48
  * @param {string} changelogPath - Path to CHANGELOG.md
@@ -134,8 +132,19 @@ async function checkGitHubReleaseExists(owner, repo, version) {
134
132
  */
135
133
  async function createGitTag(version, dryRun = false) {
136
134
  const tagName = `v${version}`;
135
+ // The CI smoke-test runs with a read-only token that can't push. It sets this
136
+ // so we tolerate the push failure explicitly, instead of assuming any dry-run
137
+ // permission error is expected.
138
+ const isPublishTest = process.env.IS_PUBLISH_TEST === 'true';
137
139
 
138
140
  try {
141
+ // Skip if the tag already exists locally. Tag creation is local and fails if it exists.
142
+ const { stdout: existingTag } = await $`git tag -l ${tagName}`;
143
+ if (existingTag.trim()) {
144
+ console.log(`šŸ·ļø Git tag ${tagName} already exists, skipping`);
145
+ return;
146
+ }
147
+
139
148
  await $({
140
149
  env: {
141
150
  ...process.env,
@@ -143,10 +152,28 @@ async function createGitTag(version, dryRun = false) {
143
152
  GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
144
153
  },
145
154
  })`git tag -a ${tagName} -m ${`Version ${version}`}`;
146
- const pushArgs = dryRun ? ['--dry-run'] : [];
147
- await $({ stdio: 'inherit' })`git push origin ${tagName} ${pushArgs}`;
148
155
 
149
- console.log(`šŸ·ļø Created and pushed git tag ${tagName}${dryRun ? ' (dry-run)' : ''}`);
156
+ if (dryRun) {
157
+ // `git push --dry-run` still authenticates and needs push access. When the token
158
+ // can push, validate it for real; when it can't, only tolerate the failure if this
159
+ // is an explicit publish smoke-test and the error is a genuine permission denial.
160
+ try {
161
+ // Don't inherit stdio so stderr is captured and matchable below.
162
+ await $`git push origin ${tagName} --dry-run`;
163
+ console.log(`šŸ·ļø Created git tag ${tagName} (dry-run)`);
164
+ } catch (/** @type {any} */ pushError) {
165
+ const message = pushError.stderr || pushError.message || '';
166
+ if (isPublishTest && /permission|403|denied/i.test(message)) {
167
+ console.log(`šŸ·ļø Created git tag ${tagName} (dry-run, no push permission, skipped push)`);
168
+ return;
169
+ }
170
+ throw pushError;
171
+ }
172
+ return;
173
+ }
174
+ await $({ stdio: 'inherit' })`git push origin ${tagName}`;
175
+
176
+ console.log(`šŸ·ļø Created and pushed git tag ${tagName}`);
150
177
  } catch (/** @type {any} */ error) {
151
178
  throw new Error(`Failed to create git tag: ${error.message}`);
152
179
  }
@@ -189,18 +216,11 @@ async function validateGitHubRelease(version) {
189
216
  * Publish packages to npm
190
217
  * @param {PublicPackage[]} packages - Packages to publish
191
218
  * @param {PublishOptions} options - Publishing options
192
- * @returns {Promise<void>}
219
+ * @returns {Promise<PublishSummaryEntry[]>}
193
220
  */
194
221
  async function publishToNpm(packages, options) {
195
- console.log('\nšŸ“¦ Publishing packages to npm...');
196
- console.log(`šŸ“‹ Found ${packages.length} packages:`);
197
- packages.forEach((pkg) => {
198
- console.log(` • ${pkg.name}@${pkg.version}`);
199
- });
200
-
201
222
  // Use pnpm's built-in duplicate checking - no need to check versions ourselves
202
- await publishPackages(packages, options);
203
- console.log('āœ… Successfully published to npm');
223
+ return publishPackages(packages, options);
204
224
  }
205
225
 
206
226
  /**
@@ -260,10 +280,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
260
280
  .option('sha', {
261
281
  type: 'string',
262
282
  description: 'Git SHA to use for the GitHub release workflow (local only)',
283
+ })
284
+ .option('filter', {
285
+ type: 'string',
286
+ array: true,
287
+ description:
288
+ 'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
263
289
  });
264
290
  },
265
291
  handler: async (argv) => {
266
- const { dryRun = false, githubRelease = false, tag = 'latest', sha } = argv;
292
+ const { dryRun = false, githubRelease = false, tag = 'latest', sha, filter = [] } = argv;
267
293
 
268
294
  if (isCI && !argv.ci) {
269
295
  console.error(
@@ -290,13 +316,34 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
290
316
  // Get all packages
291
317
  console.log('šŸ” Discovering all workspace packages...');
292
318
 
293
- const allPackages = await getWorkspacePackages({ publicOnly: true });
319
+ const filteredPackages = await getWorkspacePackages({ publicOnly: true, filter });
294
320
 
295
- if (allPackages.length === 0) {
296
- console.log('āš ļø No public packages found in workspace');
321
+ if (filteredPackages.length === 0) {
322
+ console.log(
323
+ `āš ļø No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
324
+ );
297
325
  return;
298
326
  }
299
327
 
328
+ if (filter.length > 0) {
329
+ console.log('šŸ” Validating workspace dependencies for filtered packages...');
330
+
331
+ const { issues } = await validatePublishDependencies(filteredPackages);
332
+
333
+ if (issues.length > 0) {
334
+ throw new Error(
335
+ `Invalid dependencies structure of packages to be published -
336
+ ${issues.join('\n ')}
337
+ `,
338
+ {
339
+ cause: issues,
340
+ },
341
+ );
342
+ }
343
+
344
+ console.log('āœ… All workspace dependency requirements satisfied');
345
+ }
346
+
300
347
  // Get version from root package.json
301
348
  const version = await getReleaseVersion();
302
349
 
@@ -311,21 +358,41 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
311
358
  githubReleaseData = await validateGitHubRelease(version);
312
359
  }
313
360
 
314
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
361
+ const newPackages = await getPackagesNeedingManualPublish(filteredPackages);
315
362
 
316
363
  if (newPackages.length > 0) {
364
+ const newPackageNames = newPackages.map((pkg) => pkg.name).join(', ');
317
365
  throw new Error(
318
- `The following packages are new and need to be published manually first: ${newPackages.join(
319
- ', ',
320
- )}. Read more about it here: https://github.com/mui/mui-public/blob/master/packages/code-infra/README.md#adding-and-publishing-new-packages`,
366
+ `The following packages are new and need to be published to npm manually first: ${newPackageNames}. Read more about it here: https://github.com/mui/mui-public/blob/master/packages/code-infra/README.md#adding-and-publishing-new-packages`,
321
367
  );
322
368
  }
323
369
 
324
370
  // Publish to npm (pnpm handles duplicate checking automatically)
325
371
  // No git checks, we'll do our own
326
- await publishToNpm(allPackages, { dryRun, noGitChecks: true, tag });
372
+ console.log('\nšŸ“¦ Publishing packages to npm...');
373
+ const publishedPackages = await publishToNpm(filteredPackages, {
374
+ dryRun,
375
+ noGitChecks: true,
376
+ tag,
377
+ });
378
+
379
+ if (publishedPackages.length === 0) {
380
+ console.log('ā„¹ļø No packages were published (all may already be up to date on npm)');
381
+ console.log('\nšŸ Nothing to publish, skipping git tag and GitHub release.');
382
+ return;
383
+ }
327
384
 
328
- await createGitTag(version, dryRun);
385
+ publishedPackages.forEach((pkg) => {
386
+ console.log(`āœ… Published ${pkg.name}@${pkg.version}`);
387
+ });
388
+
389
+ // Tag the root version when it's new. Arbitrary package publishes that don't
390
+ // bump the root version don't create a new tag, so skip in that case.
391
+ if (await remoteGitTagExists(`v${version}`)) {
392
+ console.log(`ā„¹ļø Git tag v${version} already exists, skipping tag creation.`);
393
+ } else {
394
+ await createGitTag(version, dryRun);
395
+ }
329
396
 
330
397
  // Create GitHub release or git tag after successful npm publishing
331
398
  if (githubRelease && githubReleaseData) {
@@ -353,11 +420,13 @@ const PUBLISH_WORKFLOW_ID = `.github/${WORKFLOW_PATH}`;
353
420
  */
354
421
  async function triggerLocalGithubPublishWorkflow(opts) {
355
422
  console.log(`šŸ” Checking if there are new packages to publish in the workspace...`);
356
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
423
+ const newPackages = await getPackagesNeedingManualPublish(
424
+ await getWorkspacePackages({ publicOnly: true }),
425
+ );
357
426
  if (newPackages.length) {
358
427
  console.warn(
359
428
  `āš ļø Found new packages that should be published to npm first before triggering a release:
360
- * ${newPackages.map((pkg) => pkg.name).join(' * ')}
429
+ ${newPackages.map((pkg) => ` * ${pkg.name}`).join('\n')}
361
430
  Please run the command "${chalk.bold('pnpm code-infra publish-new-package')}" first to publish and configure npm.`,
362
431
  );
363
432
  return;
@@ -16,10 +16,12 @@ import * as semver from 'semver';
16
16
 
17
17
  import {
18
18
  getPackageVersionInfo,
19
+ getTransitiveDependencies,
19
20
  getWorkspacePackages,
20
21
  publishPackages,
21
22
  readPackageJson,
22
23
  semverMax,
24
+ validatePublishDependencies,
23
25
  writePackageJson,
24
26
  } from '../utils/pnpm.mjs';
25
27
  import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
@@ -28,6 +30,7 @@ import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
28
30
  * @typedef {Object} Args
29
31
  * @property {boolean} [dryRun] - Whether to run in dry-run mode
30
32
  * @property {boolean} [githubRelease] - Whether to create GitHub releases for canary packages
33
+ * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.
31
34
  */
32
35
 
33
36
  const CANARY_TAG = 'canary';
@@ -90,7 +93,11 @@ async function fetchCommitsForPackage({ packagePath }) {
90
93
  return results;
91
94
  }
92
95
 
93
- const AUTHOR_EXCLUDE_LIST = ['renovate[bot]', 'dependabot[bot]'];
96
+ const AUTHOR_EXCLUDE_LIST = new Set([
97
+ 'renovate[bot]',
98
+ 'dependabot[bot]',
99
+ 'code-infra-renovate[bot]',
100
+ ]);
94
101
 
95
102
  /**
96
103
  * @param {string} message
@@ -118,80 +125,6 @@ function cleanupCommitMessage(message) {
118
125
  return `${prefix}${msg}`.trim();
119
126
  }
120
127
 
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
128
  /**
196
129
  * Prepare changelog data for packages using GitHub API
197
130
  * @param {PublicPackage[]} packagesToPublish - Packages that will be published
@@ -215,7 +148,7 @@ async function prepareChangelogsFromGitCli(packagesToPublish, allPackages, canar
215
148
  // Exclude commits authored by bots
216
149
  .filter(
217
150
  // We want to allow commits from copilot or other AI tools, so only filter known bots
218
- (commit) => !AUTHOR_EXCLUDE_LIST.includes(commit.author),
151
+ (commit) => !AUTHOR_EXCLUDE_LIST.has(commit.author),
219
152
  )
220
153
  .map((commit) => `- ${cleanupCommitMessage(commit.message)} by ${commit.author}`);
221
154
 
@@ -224,14 +157,23 @@ async function prepareChangelogsFromGitCli(packagesToPublish, allPackages, canar
224
157
  }
225
158
  }),
226
159
  );
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);
160
+ // Second pass: check for dependency updates in other packages not part of git history.
161
+ const workspacePathByName = new Map(allPackages.map((pkg) => [pkg.name, pkg.path]));
162
+ const publishedNames = new Set(packagesToPublish.map((p) => p.name));
163
+
164
+ const transitiveDepSets = await Promise.all(
165
+ allPackages.map((pkg) =>
166
+ getTransitiveDependencies([pkg.name], {
167
+ includeDev: false,
168
+ workspacePathByName,
169
+ }),
170
+ ),
171
+ );
230
172
 
231
173
  for (let i = 0; i < allPackages.length; i += 1) {
232
174
  const pkg = allPackages[i];
233
- const depsToPublish = (transitiveDependencies[pkg.name] ?? []).filter((dep) =>
234
- packagesToPublish.some((p) => p.name === dep),
175
+ const depsToPublish = [...transitiveDepSets[i]].filter(
176
+ (dep) => dep !== pkg.name && publishedNames.has(dep),
235
177
  );
236
178
  if (depsToPublish.length === 0) {
237
179
  continue;
@@ -290,6 +232,41 @@ async function prepareChangelogsForPackages(packagesToPublish, allPackages, cana
290
232
  return changelogs;
291
233
  }
292
234
 
235
+ /**
236
+ * Create or replace a GitHub release. Attempts to create the release first,
237
+ * and if it already exists (422), deletes the existing one and retries.
238
+ *
239
+ * @param {InstanceType<typeof Octokit>} octokit
240
+ * @param {NonNullable<Parameters<Octokit['repos']['createRelease']>[0]>} params
241
+ */
242
+ async function upsertGitHubRelease(octokit, params) {
243
+ try {
244
+ return await octokit.repos.createRelease(params);
245
+ } catch (/** @type {any} */ error) {
246
+ const isAlreadyExists =
247
+ error.status === 422 &&
248
+ error.response?.data?.errors?.some(
249
+ (/** @type {any} */ err) => err.code === 'already_exists' && err.field === 'tag_name',
250
+ );
251
+ if (!isAlreadyExists) {
252
+ throw error;
253
+ }
254
+ }
255
+
256
+ // Release already exists — delete and recreate
257
+ const existing = await octokit.repos.getReleaseByTag({
258
+ owner: params.owner,
259
+ repo: params.repo,
260
+ tag: params.tag_name,
261
+ });
262
+ await octokit.repos.deleteRelease({
263
+ owner: params.owner,
264
+ repo: params.repo,
265
+ release_id: existing.data.id,
266
+ });
267
+ return octokit.repos.createRelease(params);
268
+ }
269
+
293
270
  /**
294
271
  * Create GitHub releases and tags for published packages
295
272
  * @param {PublicPackage[]} publishedPackages - Packages that were published
@@ -342,15 +319,20 @@ async function createGitHubReleasesForPackages(
342
319
  GIT_COMMITTER_NAME: 'Code infra',
343
320
  GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
344
321
  },
345
- })`git tag -a ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
322
+ })`git tag -fa ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
346
323
 
324
+ // Force-push to handle retries where the tag already exists from a previous
325
+ // failed publish. The npm registry is the source of truth for published
326
+ // versions, so it's safe to rewrite a tag even if it points to a different
327
+ // commit — it just means the prior publish for this version failed partway
328
+ // through and the GitHub release needs to be recreated.
347
329
  // eslint-disable-next-line no-await-in-loop
348
- await $`git push origin ${tagName}`;
330
+ await $`git push --force origin ${tagName}`;
349
331
  console.log(`āœ… Created and pushed git tag: ${tagName}`);
350
332
 
351
333
  // Create GitHub release
352
334
  // eslint-disable-next-line no-await-in-loop
353
- const res = await octokit.repos.createRelease({
335
+ const res = await upsertGitHubRelease(octokit, {
354
336
  owner: repoInfo.owner,
355
337
  repo: repoInfo.repo,
356
338
  tag_name: tagName,
@@ -380,7 +362,14 @@ async function getLastCanaryTag() {
380
362
  // Tag might not exist locally, which is fine
381
363
  }
382
364
 
383
- await $`git fetch origin tag ${CANARY_TAG}`;
365
+ try {
366
+ await $`git fetch origin tag ${CANARY_TAG}`;
367
+ } catch (err) {
368
+ // Tag might not exist on the remote yet (first canary run), which is fine
369
+ if (!(/** @type {Error} */ (err).message?.includes("couldn't find remote ref"))) {
370
+ throw err;
371
+ }
372
+ }
384
373
  const { stdout: remoteCanaryTag } = await $`git ls-remote --tags origin ${CANARY_TAG}`;
385
374
  return remoteCanaryTag.trim() ? CANARY_TAG : null;
386
375
  }
@@ -492,16 +481,23 @@ async function publishCanaryVersions(
492
481
  }
493
482
 
494
483
  // Third pass: publish only the changed packages using recursive publish
495
- let publishSuccess = false;
484
+ /** @type {Set<string>} */
485
+ const publishedNames = new Set();
496
486
  try {
497
487
  console.log(`šŸ“¤ Publishing ${packagesToPublish.length} canary versions...`);
498
- await publishPackages(packagesToPublish, { ...options, noGitChecks: true, tag: CANARY_TAG });
499
-
500
- packagesToPublish.forEach((pkg) => {
501
- const canaryVersion = canaryVersions.get(pkg.name);
502
- console.log(`āœ… Published ${pkg.name}@${canaryVersion}`);
488
+ const publishedPackages = await publishPackages(packagesToPublish, {
489
+ dryRun: options.dryRun,
490
+ noGitChecks: true,
491
+ tag: CANARY_TAG,
503
492
  });
504
- publishSuccess = true;
493
+
494
+ // Only use package names from the report summary, not versions.
495
+ // pnpm-publish-summary.json reports the version from the workspace
496
+ // package.json, which is wrong for packages with publishConfig.directory.
497
+ for (const { name } of publishedPackages) {
498
+ publishedNames.add(name);
499
+ console.log(`āœ… Published ${name}@${canaryVersions.get(name)}`);
500
+ }
505
501
  } finally {
506
502
  // Always restore original package.json files in parallel
507
503
  console.log('\nšŸ”„ Restoring original package.json files...');
@@ -515,16 +511,26 @@ async function publishCanaryVersions(
515
511
  await Promise.all(restorePromises);
516
512
  }
517
513
 
518
- if (publishSuccess) {
519
- // Create/update the canary tag after successful publish
520
- await createCanaryTag(options.dryRun);
521
-
522
- // Create GitHub releases if requested
514
+ if (publishedNames.size > 0) {
515
+ // Create GitHub releases only for actually published packages
523
516
  if (options.githubRelease) {
524
- await createGitHubReleasesForPackages(packagesToPublish, canaryVersions, changelogs, options);
517
+ const actuallyPublished = packagesToPublish.filter((pkg) => publishedNames.has(pkg.name));
518
+ await createGitHubReleasesForPackages(actuallyPublished, canaryVersions, changelogs, options);
525
519
  }
526
520
 
527
- console.log('\nšŸŽ‰ All canary versions published successfully!');
521
+ // Only advance the canary tag if all expected packages were published.
522
+ // Otherwise the tag would skip over unpublished packages and they'd
523
+ // never be retried on the next run.
524
+ const missing = packagesToPublish.filter((pkg) => !publishedNames.has(pkg.name));
525
+ if (missing.length === 0) {
526
+ await createCanaryTag(options.dryRun);
527
+ console.log('\nšŸŽ‰ All canary versions published successfully!');
528
+ } else {
529
+ const missingNames = missing.map((pkg) => pkg.name).join(', ');
530
+ console.warn(
531
+ `\nāš ļø Canary tag not advanced, some packages failed to publish: ${missingNames}`,
532
+ );
533
+ }
528
534
  }
529
535
  }
530
536
 
@@ -542,10 +548,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
542
548
  type: 'boolean',
543
549
  default: false,
544
550
  description: 'Create GitHub releases for published packages',
551
+ })
552
+ .option('filter', {
553
+ type: 'string',
554
+ array: true,
555
+ description:
556
+ 'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
545
557
  });
546
558
  },
547
559
  handler: async (argv) => {
548
- const { dryRun = false, githubRelease = false } = argv;
560
+ const { dryRun = false, githubRelease = false, filter = [] } = argv;
549
561
 
550
562
  const options = { dryRun, githubRelease };
551
563
 
@@ -557,22 +569,46 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
557
569
  console.log('šŸ“ GitHub releases will be created for published packages\n');
558
570
  }
559
571
 
560
- // Always get all packages first
572
+ // All public packages — needed by publishCanaryVersions to bump versions and update
573
+ // package.json across the entire workspace, even for packages not being published.
561
574
  console.log('šŸ” Discovering all workspace packages...');
562
- const allPackages = await getWorkspacePackages({ publicOnly: true });
575
+ const filteredPackages = await getWorkspacePackages({ publicOnly: true, filter });
563
576
 
564
- if (allPackages.length === 0) {
565
- console.log('āš ļø No public packages found in workspace');
577
+ if (filteredPackages.length === 0) {
578
+ console.log(
579
+ `āš ļø No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
580
+ );
566
581
  return;
567
582
  }
568
583
 
569
- // Check for canary tag to determine selective publishing
584
+ if (filter.length > 0) {
585
+ console.log('šŸ” Validating workspace dependencies for filtered packages...');
586
+
587
+ const { issues } = await validatePublishDependencies(filteredPackages);
588
+
589
+ if (issues.length > 0) {
590
+ throw new Error(
591
+ `Invalid dependencies structure of packages to be published -
592
+ ${issues.join('\n ')}
593
+ `,
594
+ {
595
+ cause: issues,
596
+ },
597
+ );
598
+ }
599
+
600
+ console.log('āœ… All workspace dependency requirements satisfied');
601
+ }
602
+
603
+ // Check for canary tag to determine selective publishing.
604
+ // --filter is applied on top of sinceRef: publish only packages that have
605
+ // changed since the last canary tag AND match the filter.
570
606
  const canaryTag = await getLastCanaryTag();
571
607
 
572
608
  console.log('šŸ” Checking for packages changed since canary tag...');
573
609
  const packages = canaryTag
574
- ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true })
575
- : allPackages;
610
+ ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true, filter })
611
+ : filteredPackages;
576
612
 
577
613
  console.log(`šŸ“‹ Found ${packages.length} packages(s) for canary publishing:`);
578
614
  packages.forEach((pkg) => {
@@ -581,7 +617,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
581
617
 
582
618
  // Fetch version info for all packages in parallel
583
619
  console.log('\nšŸ” Fetching package version information...');
584
- const versionInfoPromises = allPackages.map(async (pkg) => {
620
+ const versionInfoPromises = filteredPackages.map(async (pkg) => {
585
621
  const versionInfo = await getPackageVersionInfo(pkg.name, pkg.version);
586
622
  return { packageName: pkg.name, versionInfo };
587
623
  });
@@ -593,7 +629,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
593
629
  packageVersionInfo.set(packageName, versionInfo);
594
630
  }
595
631
 
596
- await publishCanaryVersions(packages, allPackages, packageVersionInfo, options);
632
+ await publishCanaryVersions(packages, filteredPackages, packageVersionInfo, options);
597
633
 
598
634
  console.log('\nšŸ Publishing complete!');
599
635
  },