@mui/internal-code-infra 0.0.4-canary.3 → 0.0.4-canary.31

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 (69) hide show
  1. package/README.md +19 -8
  2. package/build/babel-config.d.mts +11 -3
  3. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  4. package/build/brokenLinksChecker/index.d.mts +35 -2
  5. package/build/changelog/types.d.ts +1 -1
  6. package/build/cli/cmdArgosPush.d.mts +2 -2
  7. package/build/cli/cmdBuild.d.mts +2 -2
  8. package/build/cli/cmdCopyFiles.d.mts +2 -2
  9. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  10. package/build/cli/cmdGenerateChangelog.d.mts +2 -2
  11. package/build/cli/cmdGithubAuth.d.mts +2 -2
  12. package/build/cli/cmdListWorkspaces.d.mts +4 -2
  13. package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
  14. package/build/cli/cmdPublish.d.mts +4 -2
  15. package/build/cli/cmdPublishCanary.d.mts +3 -2
  16. package/build/cli/cmdPublishNewPackage.d.mts +4 -2
  17. package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
  18. package/build/cli/cmdVale.d.mts +46 -0
  19. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  20. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  21. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  22. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
  23. package/build/markdownlint/duplicate-h1.d.mts +1 -1
  24. package/build/markdownlint/git-diff.d.mts +1 -1
  25. package/build/markdownlint/index.d.mts +1 -1
  26. package/build/markdownlint/straight-quotes.d.mts +1 -1
  27. package/build/markdownlint/table-alignment.d.mts +1 -1
  28. package/build/markdownlint/terminal-language.d.mts +1 -1
  29. package/build/utils/build.d.mts +3 -3
  30. package/build/utils/github.d.mts +1 -1
  31. package/build/utils/pnpm.d.mts +68 -2
  32. package/build/utils/testUtils.d.mts +7 -0
  33. package/package.json +38 -31
  34. package/src/babel-config.mjs +9 -3
  35. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
  36. package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
  37. package/src/brokenLinksChecker/crawlWorker.mjs +173 -0
  38. package/src/brokenLinksChecker/index.mjs +177 -164
  39. package/src/brokenLinksChecker/index.test.ts +55 -13
  40. package/src/build-env.d.ts +13 -0
  41. package/src/changelog/fetchChangelogs.mjs +6 -2
  42. package/src/changelog/types.ts +1 -1
  43. package/src/cli/cmdListWorkspaces.mjs +9 -2
  44. package/src/cli/cmdNetlifyIgnore.mjs +4 -88
  45. package/src/cli/cmdPublish.mjs +51 -14
  46. package/src/cli/cmdPublishCanary.mjs +139 -107
  47. package/src/cli/cmdPublishNewPackage.mjs +27 -6
  48. package/src/cli/cmdVale.mjs +513 -0
  49. package/src/cli/cmdVale.test.mjs +644 -0
  50. package/src/cli/index.mjs +2 -0
  51. package/src/eslint/baseConfig.mjs +2 -1
  52. package/src/eslint/docsConfig.mjs +2 -1
  53. package/src/eslint/jsonConfig.mjs +2 -1
  54. package/src/eslint/mui/config.mjs +11 -1
  55. package/src/eslint/testConfig.mjs +2 -1
  56. package/src/estree-typescript.d.ts +1 -1
  57. package/src/untyped-plugins.d.ts +11 -11
  58. package/src/utils/build.test.mjs +546 -575
  59. package/src/utils/pnpm.mjs +192 -3
  60. package/src/utils/pnpm.test.mjs +580 -0
  61. package/src/utils/testUtils.mjs +18 -0
  62. package/src/utils/typescript.test.mjs +249 -272
  63. package/vale/.vale.ini +1 -0
  64. package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
  65. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  66. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  67. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  68. package/vale/styles/MUI/NoBritish.yml +112 -0
  69. package/vale/styles/MUI/NoCompanyName.yml +17 -0
@@ -12,93 +12,7 @@ import * as fs from 'node:fs/promises';
12
12
  import * as path from 'node:path';
13
13
  import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
14
14
  import { toPosixPath } from '../utils/path.mjs';
15
- import { getWorkspacePackages } from '../utils/pnpm.mjs';
16
-
17
- /**
18
- * Get all workspace dependencies (direct and transitive) from a package
19
- * @param {string} packageName - Package name
20
- * @param {Map<string, string>} workspaceMap - Map of workspace name to path
21
- * @param {Map<string, Promise<Set<string>>>} cache - Cache of package resolution promises
22
- * @returns {Promise<Set<string>>} Set of workspace package names (dependencies only, not including the package itself)
23
- */
24
- async function getWorkspaceDependenciesRecursive(packageName, workspaceMap, cache) {
25
- // Check cache first
26
- const cached = cache.get(packageName);
27
- if (cached) {
28
- return cached;
29
- }
30
-
31
- // Create the resolution promise
32
- const promise = (async () => {
33
- const packagePath = workspaceMap.get(packageName);
34
- if (!packagePath) {
35
- throw new Error(`Workspace "${packageName}" not found in the repository`);
36
- }
37
-
38
- const packageJsonPath = path.join(packagePath, 'package.json');
39
- const content = await fs.readFile(packageJsonPath, 'utf8');
40
- const packageJson = JSON.parse(content);
41
-
42
- // Collect all dependency names
43
- /** @type {Set<string>} */
44
- const allDeps = new Set();
45
- if (packageJson.dependencies) {
46
- Object.keys(packageJson.dependencies).forEach((dep) => allDeps.add(dep));
47
- }
48
- if (packageJson.devDependencies) {
49
- Object.keys(packageJson.devDependencies).forEach((dep) => allDeps.add(dep));
50
- }
51
- if (packageJson.peerDependencies) {
52
- Object.keys(packageJson.peerDependencies).forEach((dep) => allDeps.add(dep));
53
- }
54
-
55
- // Filter to only workspace dependencies
56
- const workspaceDeps = Array.from(allDeps).filter((dep) => workspaceMap.has(dep));
57
-
58
- // Recursively process workspace dependencies in parallel
59
- const recursiveResults = await Promise.all(
60
- workspaceDeps.map(async (dep) => {
61
- return getWorkspaceDependenciesRecursive(dep, workspaceMap, cache);
62
- }),
63
- );
64
-
65
- // Merge all results using flatMap
66
- return new Set(recursiveResults.flatMap((result) => Array.from(result)).concat(workspaceDeps));
67
- })();
68
-
69
- // Store in cache before returning
70
- cache.set(packageName, promise);
71
-
72
- return promise;
73
- }
74
-
75
- /**
76
- * Get transitive workspace dependencies for a list of workspace names
77
- * @param {string[]} workspaceNames - Array of workspace names
78
- * @param {Map<string, string>} workspaceMap - Map of workspace name to path
79
- * @returns {Promise<Set<string>>} Set of workspace package names (including requested packages and all their dependencies)
80
- */
81
- async function getTransitiveDependencies(workspaceNames, workspaceMap) {
82
- // Shared cache for all workspace dependency resolution
83
- const cache = new Map();
84
-
85
- // Validate all workspace names exist
86
- for (const workspaceName of workspaceNames) {
87
- if (!workspaceMap.has(workspaceName)) {
88
- throw new Error(`Workspace "${workspaceName}" not found in the repository`);
89
- }
90
- }
91
-
92
- // Process each requested workspace in parallel
93
- const workspaceResults = await Promise.all(
94
- workspaceNames.map((workspaceName) =>
95
- getWorkspaceDependenciesRecursive(workspaceName, workspaceMap, cache),
96
- ),
97
- );
98
-
99
- // Merge all results using flatMap and add the original package names
100
- return new Set(workspaceNames.concat(workspaceResults.flatMap((result) => Array.from(result))));
101
- }
15
+ import { getTransitiveDependencies, getWorkspacePackages } from '../utils/pnpm.mjs';
102
16
 
103
17
  /**
104
18
  * Generate the ignore command string for netlify.toml
@@ -220,7 +134,9 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
220
134
  console.log(`Processing ${workspaceName}...`);
221
135
 
222
136
  // Get transitive dependencies for this specific workspace
223
- const dependencyNames = await getTransitiveDependencies([workspaceName], workspaceMap);
137
+ const dependencyNames = await getTransitiveDependencies([workspaceName], {
138
+ workspacePathByName: workspaceMap,
139
+ });
224
140
 
225
141
  // Convert package names to relative paths (normalize to POSIX separators for git)
226
142
  const relativePaths = Array.from(dependencyNames)
@@ -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,7 +18,11 @@ 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 {
22
+ getWorkspacePackages,
23
+ publishPackages,
24
+ validatePublishDependencies,
25
+ } from '../utils/pnpm.mjs';
21
26
  import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
22
27
 
23
28
  const isCI = envCI().isCi;
@@ -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
  /**
@@ -189,18 +195,11 @@ async function validateGitHubRelease(version) {
189
195
  * Publish packages to npm
190
196
  * @param {PublicPackage[]} packages - Packages to publish
191
197
  * @param {PublishOptions} options - Publishing options
192
- * @returns {Promise<void>}
198
+ * @returns {Promise<PublishSummaryEntry[]>}
193
199
  */
194
200
  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
201
  // 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');
202
+ return publishPackages(packages, options);
204
203
  }
205
204
 
206
205
  /**
@@ -260,10 +259,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
260
259
  .option('sha', {
261
260
  type: 'string',
262
261
  description: 'Git SHA to use for the GitHub release workflow (local only)',
262
+ })
263
+ .option('filter', {
264
+ type: 'string',
265
+ array: true,
266
+ description:
267
+ 'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
263
268
  });
264
269
  },
265
270
  handler: async (argv) => {
266
- const { dryRun = false, githubRelease = false, tag = 'latest', sha } = argv;
271
+ const { dryRun = false, githubRelease = false, tag = 'latest', sha, filter = [] } = argv;
267
272
 
268
273
  if (isCI && !argv.ci) {
269
274
  console.error(
@@ -290,13 +295,34 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
290
295
  // Get all packages
291
296
  console.log('šŸ” Discovering all workspace packages...');
292
297
 
293
- const allPackages = await getWorkspacePackages({ publicOnly: true });
298
+ const allPackages = await getWorkspacePackages({ publicOnly: true, filter });
294
299
 
295
300
  if (allPackages.length === 0) {
296
- console.log('āš ļø No public packages found in workspace');
301
+ console.log(
302
+ `āš ļø No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
303
+ );
297
304
  return;
298
305
  }
299
306
 
307
+ if (filter.length > 0) {
308
+ console.log('šŸ” Validating workspace dependencies for filtered packages...');
309
+
310
+ const { issues } = await validatePublishDependencies(allPackages);
311
+
312
+ if (issues.length > 0) {
313
+ throw new Error(
314
+ `Invalid dependencies structure of packages to be published -
315
+ ${issues.join('\n ')}
316
+ `,
317
+ {
318
+ cause: issues,
319
+ },
320
+ );
321
+ }
322
+
323
+ console.log('āœ… All workspace dependency requirements satisfied');
324
+ }
325
+
300
326
  // Get version from root package.json
301
327
  const version = await getReleaseVersion();
302
328
 
@@ -323,7 +349,18 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
323
349
 
324
350
  // Publish to npm (pnpm handles duplicate checking automatically)
325
351
  // No git checks, we'll do our own
326
- await publishToNpm(allPackages, { dryRun, noGitChecks: true, tag });
352
+ console.log('\nšŸ“¦ Publishing packages to npm...');
353
+ const publishedPackages = await publishToNpm(allPackages, { dryRun, noGitChecks: true, tag });
354
+
355
+ if (publishedPackages.length === 0) {
356
+ console.log('ā„¹ļø No packages were published (all may already be up to date on npm)');
357
+ console.log('\nšŸ Nothing to publish, skipping git tag and GitHub release.');
358
+ return;
359
+ }
360
+
361
+ publishedPackages.forEach((pkg) => {
362
+ console.log(`āœ… Published ${pkg.name}@${pkg.version}`);
363
+ });
327
364
 
328
365
  await createGitTag(version, dryRun);
329
366
 
@@ -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';
@@ -118,80 +121,6 @@ function cleanupCommitMessage(message) {
118
121
  return `${prefix}${msg}`.trim();
119
122
  }
120
123
 
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
124
  /**
196
125
  * Prepare changelog data for packages using GitHub API
197
126
  * @param {PublicPackage[]} packagesToPublish - Packages that will be published
@@ -224,14 +153,23 @@ async function prepareChangelogsFromGitCli(packagesToPublish, allPackages, canar
224
153
  }
225
154
  }),
226
155
  );
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);
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
+ );
230
168
 
231
169
  for (let i = 0; i < allPackages.length; i += 1) {
232
170
  const pkg = allPackages[i];
233
- const depsToPublish = (transitiveDependencies[pkg.name] ?? []).filter((dep) =>
234
- packagesToPublish.some((p) => p.name === dep),
171
+ const depsToPublish = [...transitiveDepSets[i]].filter(
172
+ (dep) => dep !== pkg.name && publishedNames.has(dep),
235
173
  );
236
174
  if (depsToPublish.length === 0) {
237
175
  continue;
@@ -290,6 +228,41 @@ async function prepareChangelogsForPackages(packagesToPublish, allPackages, cana
290
228
  return changelogs;
291
229
  }
292
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
+
293
266
  /**
294
267
  * Create GitHub releases and tags for published packages
295
268
  * @param {PublicPackage[]} publishedPackages - Packages that were published
@@ -342,15 +315,20 @@ async function createGitHubReleasesForPackages(
342
315
  GIT_COMMITTER_NAME: 'Code infra',
343
316
  GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
344
317
  },
345
- })`git tag -a ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
318
+ })`git tag -fa ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
346
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.
347
325
  // eslint-disable-next-line no-await-in-loop
348
- await $`git push origin ${tagName}`;
326
+ await $`git push --force origin ${tagName}`;
349
327
  console.log(`āœ… Created and pushed git tag: ${tagName}`);
350
328
 
351
329
  // Create GitHub release
352
330
  // eslint-disable-next-line no-await-in-loop
353
- const res = await octokit.repos.createRelease({
331
+ const res = await upsertGitHubRelease(octokit, {
354
332
  owner: repoInfo.owner,
355
333
  repo: repoInfo.repo,
356
334
  tag_name: tagName,
@@ -380,7 +358,14 @@ async function getLastCanaryTag() {
380
358
  // Tag might not exist locally, which is fine
381
359
  }
382
360
 
383
- 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
+ }
384
369
  const { stdout: remoteCanaryTag } = await $`git ls-remote --tags origin ${CANARY_TAG}`;
385
370
  return remoteCanaryTag.trim() ? CANARY_TAG : null;
386
371
  }
@@ -492,16 +477,23 @@ async function publishCanaryVersions(
492
477
  }
493
478
 
494
479
  // Third pass: publish only the changed packages using recursive publish
495
- let publishSuccess = false;
480
+ /** @type {Set<string>} */
481
+ const publishedNames = new Set();
496
482
  try {
497
483
  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}`);
484
+ const publishedPackages = await publishPackages(packagesToPublish, {
485
+ dryRun: options.dryRun,
486
+ noGitChecks: true,
487
+ tag: CANARY_TAG,
503
488
  });
504
- publishSuccess = true;
489
+
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
+ }
505
497
  } finally {
506
498
  // Always restore original package.json files in parallel
507
499
  console.log('\nšŸ”„ Restoring original package.json files...');
@@ -515,16 +507,26 @@ async function publishCanaryVersions(
515
507
  await Promise.all(restorePromises);
516
508
  }
517
509
 
518
- if (publishSuccess) {
519
- // Create/update the canary tag after successful publish
520
- await createCanaryTag(options.dryRun);
521
-
522
- // Create GitHub releases if requested
510
+ if (publishedNames.size > 0) {
511
+ // Create GitHub releases only for actually published packages
523
512
  if (options.githubRelease) {
524
- await createGitHubReleasesForPackages(packagesToPublish, canaryVersions, changelogs, options);
513
+ const actuallyPublished = packagesToPublish.filter((pkg) => publishedNames.has(pkg.name));
514
+ await createGitHubReleasesForPackages(actuallyPublished, canaryVersions, changelogs, options);
525
515
  }
526
516
 
527
- 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
+ }
528
530
  }
529
531
  }
530
532
 
@@ -542,10 +544,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
542
544
  type: 'boolean',
543
545
  default: false,
544
546
  description: 'Create GitHub releases for published packages',
547
+ })
548
+ .option('filter', {
549
+ type: 'string',
550
+ array: true,
551
+ description:
552
+ 'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
545
553
  });
546
554
  },
547
555
  handler: async (argv) => {
548
- const { dryRun = false, githubRelease = false } = argv;
556
+ const { dryRun = false, githubRelease = false, filter = [] } = argv;
549
557
 
550
558
  const options = { dryRun, githubRelease };
551
559
 
@@ -557,22 +565,46 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
557
565
  console.log('šŸ“ GitHub releases will be created for published packages\n');
558
566
  }
559
567
 
560
- // 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.
561
570
  console.log('šŸ” Discovering all workspace packages...');
562
- const allPackages = await getWorkspacePackages({ publicOnly: true });
571
+ const filteredPackages = await getWorkspacePackages({ publicOnly: true, filter });
563
572
 
564
- if (allPackages.length === 0) {
565
- 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
+ );
566
577
  return;
567
578
  }
568
579
 
569
- // Check for canary tag to determine selective publishing
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) {
586
+ throw new Error(
587
+ `Invalid dependencies structure of packages to be published -
588
+ ${issues.join('\n ')}
589
+ `,
590
+ {
591
+ cause: issues,
592
+ },
593
+ );
594
+ }
595
+
596
+ console.log('āœ… All workspace dependency requirements satisfied');
597
+ }
598
+
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.
570
602
  const canaryTag = await getLastCanaryTag();
571
603
 
572
604
  console.log('šŸ” Checking for packages changed since canary tag...');
573
605
  const packages = canaryTag
574
- ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true })
575
- : allPackages;
606
+ ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true, filter })
607
+ : filteredPackages;
576
608
 
577
609
  console.log(`šŸ“‹ Found ${packages.length} packages(s) for canary publishing:`);
578
610
  packages.forEach((pkg) => {
@@ -581,7 +613,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
581
613
 
582
614
  // Fetch version info for all packages in parallel
583
615
  console.log('\nšŸ” Fetching package version information...');
584
- const versionInfoPromises = allPackages.map(async (pkg) => {
616
+ const versionInfoPromises = filteredPackages.map(async (pkg) => {
585
617
  const versionInfo = await getPackageVersionInfo(pkg.name, pkg.version);
586
618
  return { packageName: pkg.name, versionInfo };
587
619
  });
@@ -593,7 +625,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
593
625
  packageVersionInfo.set(packageName, versionInfo);
594
626
  }
595
627
 
596
- await publishCanaryVersions(packages, allPackages, packageVersionInfo, options);
628
+ await publishCanaryVersions(packages, filteredPackages, packageVersionInfo, options);
597
629
 
598
630
  console.log('\nšŸ Publishing complete!');
599
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.`,