@mui/internal-code-infra 0.0.4-canary.8 → 0.0.4-canary.81

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