@mui/internal-code-infra 0.1.1-canary.20 → 0.1.1-canary.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,12 +24,15 @@ This is stored in the `docs` top-level directory.
24
24
  > - **Commit SHA to release from:** the commit that contains the merged release on master. This commit is linked to the GitHub release.
25
25
  > - **Run in dry-run mode:** Used for debugging.
26
26
  > - **Create GitHub release:** Keep selected if you want a GitHub release to be automatically created from the changelog.
27
- > - **npm dist tag to publish to** Use to publish legacy or canary versions.
27
+ > - **npm dist tag to publish to** Use to publish legacy or canary versions. The publish fails before it publishes anything when a dist-tag would move to a lower version. `latest` and `latest-*` also cannot move to a prerelease unless they already point to one, and `latest-*` cannot change its major.
28
28
 
29
29
  3. Click "Run workflow"
30
30
  4. Refresh the page to see the newly created workflow, and click it.
31
31
  5. The next screen shows "@username requested your review to deploy to npm-publish", click "Review deployments" and authorize your workflow run. **Never approve workflow runs you didn't initiaite.**
32
32
 
33
+ > [!NOTE]
34
+ > A CI dry run (`code-infra publish --ci --dry-run`) runs the same dist-tag check. On a branch that does not publish to `latest`, pass the dist-tag of the release to it, for example `--tag next` for prereleases or `--tag latest-v8` for a previous major.
35
+
33
36
  > [!IMPORTANT]
34
37
  > Go through the below steps if there is an error that says `The following packages are new and need to be published manually first` in the publish flow.
35
38
 
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ /** Version that `publish-new-package` publishes to reserve the name of a new package. */
3
+ export declare const PLACEHOLDER_VERSION = "0.0.1";
2
4
  export type PrivatePackage = {
3
5
  /**
4
6
  * - Package name
@@ -134,6 +136,53 @@ export declare function getReleaseVersion(cwd?: string): Promise<string | null>;
134
136
  * @returns {Promise<PublicPackage[]>} The subset needing a manual first publish
135
137
  */
136
138
  export declare function getPackagesNeedingManualPublish(packages: PublicPackage[]): Promise<PublicPackage[]>;
139
+ export type RegistryPackage = {
140
+ /**
141
+ * - Dist-tags of the package
142
+ */
143
+ distTags: Record<string, string>;
144
+ /**
145
+ * - Published versions of the package
146
+ */
147
+ versions: string[];
148
+ };
149
+ /**
150
+ * @typedef {Object} RegistryPackage
151
+ * @property {Record<string, string>} distTags - Dist-tags of the package
152
+ * @property {string[]} versions - Published versions of the package
153
+ */
154
+ /**
155
+ * Read the dist-tags and the published versions of each package with `pnpm view`, so
156
+ * the lookup uses the same registry configuration and credentials as `pnpm publish`.
157
+ *
158
+ * @param {PublicPackage[]} packages - Packages to read
159
+ * @returns {Promise<Map<string, RegistryPackage | null>>} By package name, null for a package that is not on the registry
160
+ */
161
+ export declare function getRegistryPackages(packages: PublicPackage[]): Promise<Map<string, RegistryPackage | null>>;
162
+ export type DistTagConflict = {
163
+ /**
164
+ * - Package to publish
165
+ */
166
+ pkg: PublicPackage;
167
+ /**
168
+ * - Why the dist-tag would move to the wrong release line
169
+ */
170
+ reason: string;
171
+ };
172
+ /**
173
+ * @typedef {Object} DistTagConflict
174
+ * @property {PublicPackage} pkg - Package to publish
175
+ * @property {string} reason - Why the dist-tag would move to the wrong release line
176
+ */
177
+ /**
178
+ * Find the packages whose publish with `tag` would move the dist-tag to the wrong release line.
179
+ *
180
+ * @param {PublicPackage[]} packages - Packages to publish
181
+ * @param {string} tag - Dist-tag to publish with
182
+ * @param {Map<string, RegistryPackage | null>} registryPackages - Registry data from {@link getRegistryPackages}
183
+ * @returns {DistTagConflict[]}
184
+ */
185
+ export declare function getDistTagConflicts(packages: PublicPackage[], tag: string, registryPackages: Map<string, RegistryPackage | null>): DistTagConflict[];
137
186
  /**
138
187
  * Get package version info from registry.
139
188
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-code-infra",
3
- "version": "0.1.1-canary.20",
3
+ "version": "0.1.1-canary.21",
4
4
  "author": "MUI Team",
5
5
  "description": "Infra scripts and configs to be used across MUI repos.",
6
6
  "license": "MIT",
@@ -190,7 +190,7 @@
190
190
  "publishConfig": {
191
191
  "access": "public"
192
192
  },
193
- "gitSha": "98dec53f859a1480bed0e813be9a37cce8fea796",
193
+ "gitSha": "e8aec3f286672d4639af7b385ef313c3d8ec74f3",
194
194
  "scripts": {
195
195
  "build": "tsc -p tsconfig.build.json",
196
196
  "typescript": "tsc -noEmit",
@@ -19,7 +19,9 @@ import * as semver from 'semver';
19
19
 
20
20
  import { persistentAuthStrategy } from '../utils/github.mjs';
21
21
  import {
22
+ getDistTagConflicts,
22
23
  getPackagesNeedingManualPublish,
24
+ getRegistryPackages,
23
25
  getReleaseVersion,
24
26
  getWorkspacePackages,
25
27
  publishPackages,
@@ -373,6 +375,19 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
373
375
  );
374
376
  }
375
377
 
378
+ const distTagConflicts = getDistTagConflicts(
379
+ filteredPackages,
380
+ tag,
381
+ await getRegistryPackages(filteredPackages),
382
+ );
383
+ if (distTagConflicts.length > 0) {
384
+ throw new Error(
385
+ `Publishing with the "${tag}" dist-tag would move it to the wrong release line:
386
+ ${distTagConflicts.map(({ pkg, reason }) => ` * ${pkg.name}: ${reason}`).join('\n')}
387
+ Publish a previous major with its "latest-vX" dist-tag and a prerelease of the next major with "next". On a release branch, also make that dist-tag the "dist-tag" default of the publish workflow. If the dist-tag on npm is outdated, fix it with "npm dist-tag" first.`,
388
+ );
389
+ }
390
+
376
391
  // Publish to npm (pnpm handles duplicate checking automatically)
377
392
  // No git checks, we'll do our own
378
393
  console.log('\nšŸ“¦ Publishing packages to npm...');
@@ -11,7 +11,11 @@ import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
11
11
 
12
12
  import { getRepositoryInfo } from '../utils/git.mjs';
13
13
  import { toPosixPath } from '../utils/path.mjs';
14
- import { getPackagesNeedingManualPublish, getWorkspacePackages } from '../utils/pnpm.mjs';
14
+ import {
15
+ getPackagesNeedingManualPublish,
16
+ getWorkspacePackages,
17
+ PLACEHOLDER_VERSION,
18
+ } from '../utils/pnpm.mjs';
15
19
 
16
20
  /**
17
21
  * @typedef {Object} Args
@@ -78,7 +82,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
78
82
  const repo = await getRepositoryInfo();
79
83
  const packageJson = {
80
84
  name: pkg.name,
81
- version: '0.0.1',
85
+ version: PLACEHOLDER_VERSION,
82
86
  repository: {
83
87
  type: 'git',
84
88
  url: `git+https://github.com/${repo.owner}/${repo.repo}.git`,
@@ -12,6 +12,9 @@ import { parseDocument, isMap } from 'yaml';
12
12
  /** Canonical npm registry URL, in the form `getPublishRegistry` returns. */
13
13
  const NPMJS_REGISTRY = 'https://registry.npmjs.org/';
14
14
 
15
+ /** Version that `publish-new-package` publishes to reserve the name of a new package. */
16
+ export const PLACEHOLDER_VERSION = '0.0.1';
17
+
15
18
  /**
16
19
  * @typedef {Object} PrivatePackage
17
20
  * @property {string} [name] - Package name
@@ -227,6 +230,112 @@ export async function getPackagesNeedingManualPublish(packages) {
227
230
  return packages.filter((_pkg, index) => results[index]);
228
231
  }
229
232
 
233
+ /**
234
+ * Why publishing `version` with `tag` would move the dist-tag to the wrong release line.
235
+ *
236
+ * @param {string} tag - Dist-tag to publish with
237
+ * @param {string} version - Version to publish
238
+ * @param {string} current - Version that the dist-tag points to on the registry
239
+ * @returns {string | null} The reason, or null when the publish is safe
240
+ */
241
+ function getDistTagConflict(tag, version, current) {
242
+ if (semver.lt(version, current)) {
243
+ return `"${tag}" can only move to a newer version, but ${version} is lower than the current ${current}`;
244
+ }
245
+ // A tag that already points to a prerelease can move to the next one, for packages still in beta.
246
+ if (
247
+ (tag === 'latest' || tag.startsWith('latest-')) &&
248
+ semver.prerelease(version) &&
249
+ !semver.prerelease(current)
250
+ ) {
251
+ return `"${tag}" cannot point to a prerelease (${version})`;
252
+ }
253
+ if (tag.startsWith('latest-') && semver.major(version) !== semver.major(current)) {
254
+ return `"${tag}" cannot cross a major (${current} to ${version})`;
255
+ }
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * @typedef {Object} RegistryPackage
261
+ * @property {Record<string, string>} distTags - Dist-tags of the package
262
+ * @property {string[]} versions - Published versions of the package
263
+ */
264
+
265
+ /**
266
+ * Read the dist-tags and the published versions of each package with `pnpm view`, so
267
+ * the lookup uses the same registry configuration and credentials as `pnpm publish`.
268
+ *
269
+ * @param {PublicPackage[]} packages - Packages to read
270
+ * @returns {Promise<Map<string, RegistryPackage | null>>} By package name, null for a package that is not on the registry
271
+ */
272
+ export async function getRegistryPackages(packages) {
273
+ const entries = await Promise.all(
274
+ packages.map(async (pkg) => {
275
+ const registry = await getPublishRegistry(pkg.path);
276
+ const { exitCode, stdout, stderr } = await $({
277
+ cwd: pkg.path,
278
+ reject: false,
279
+ // pnpm resolves its own `packageManager` version from `--registry`, which may not serve it.
280
+ env: { PNPM_CONFIG_PM_ON_FAIL: 'ignore' },
281
+ })`pnpm view ${pkg.name} dist-tags versions --json --registry ${registry}`;
282
+ let info;
283
+ try {
284
+ info = JSON.parse(stdout || '{}');
285
+ } catch {
286
+ info = {};
287
+ }
288
+ if (exitCode !== 0) {
289
+ if (info.error?.code === 'ERR_PNPM_FETCH_404') {
290
+ return /** @type {const} */ ([pkg.name, null]);
291
+ }
292
+ throw new Error(
293
+ `Failed to read the dist-tags of ${pkg.name} from ${registry}: ${info.error?.message ?? stderr}`,
294
+ );
295
+ }
296
+ /** @type {RegistryPackage} */
297
+ const registryPackage = {
298
+ distTags: info['dist-tags'] ?? {},
299
+ versions: [info.versions ?? []].flat(),
300
+ };
301
+ return /** @type {const} */ ([pkg.name, registryPackage]);
302
+ }),
303
+ );
304
+ return new Map(entries);
305
+ }
306
+
307
+ /**
308
+ * @typedef {Object} DistTagConflict
309
+ * @property {PublicPackage} pkg - Package to publish
310
+ * @property {string} reason - Why the dist-tag would move to the wrong release line
311
+ */
312
+
313
+ /**
314
+ * Find the packages whose publish with `tag` would move the dist-tag to the wrong release line.
315
+ *
316
+ * @param {PublicPackage[]} packages - Packages to publish
317
+ * @param {string} tag - Dist-tag to publish with
318
+ * @param {Map<string, RegistryPackage | null>} registryPackages - Registry data from {@link getRegistryPackages}
319
+ * @returns {DistTagConflict[]}
320
+ */
321
+ export function getDistTagConflicts(packages, tag, registryPackages) {
322
+ return packages.flatMap((pkg) => {
323
+ const registryPackage = registryPackages.get(pkg.name);
324
+ // A package that is not on the registry yet has no dist-tags to protect. pnpm skips versions
325
+ // that are already published, such as unchanged packages on a legacy branch.
326
+ if (!registryPackage || registryPackage.versions.includes(pkg.version)) {
327
+ return [];
328
+ }
329
+ const current = semver.valid(registryPackage.distTags[tag]);
330
+ // npm points `latest` at the first version, which is the placeholder for a new package.
331
+ if (!current || current === PLACEHOLDER_VERSION) {
332
+ return [];
333
+ }
334
+ const reason = getDistTagConflict(tag, pkg.version, current);
335
+ return reason ? [{ pkg, reason }] : [];
336
+ });
337
+ }
338
+
230
339
  /**
231
340
  * Get package version info from registry.
232
341
  *