@mui/internal-code-infra 0.0.4-canary.95 → 0.0.4-canary.97

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
@@ -37,6 +37,9 @@ This is stored in the `docs` top-level directory.
37
37
 
38
38
  Whenever new packages are added to the repo (that will get published to npm) or a private package is turned into a public one, follow the below steps before invoking the publish workflow of the previous section.
39
39
 
40
+ > [!NOTE]
41
+ > This applies to packages published to npm, which is where Trusted Publishing needs the package to already exist. Packages that set `publishConfig.registry` to another registry are skipped by this check and need none of these steps.
42
+
40
43
  1. Go to your repo's code base on your system, then log in to npm using
41
44
 
42
45
  ```bash
@@ -2,14 +2,14 @@
2
2
  export type PublicPackage = import('../utils/pnpm.mjs').PublicPackage;
3
3
  export type Args = {
4
4
  publicOnly?: boolean;
5
- output?: 'json' | 'path' | 'name';
5
+ output?: 'json' | 'path' | 'name' | 'publish-dir';
6
6
  sinceRef?: string;
7
7
  filter?: string[];
8
8
  };
9
9
  /**
10
10
  * @typedef {Object} Args
11
11
  * @property {boolean} [publicOnly] - Whether to filter to only public packages
12
- * @property {'json'|'path'|'name'} [output] - Output format (name, path, or json)
12
+ * @property {'json'|'path'|'name'|'publish-dir'} [output] - Output format (name, path, or json)
13
13
  * @property {string} [sinceRef] - Git reference to filter changes since
14
14
  * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
15
15
  */
@@ -29,22 +29,45 @@ export type PnpmListResultItem = {
29
29
  export type GetWorkspacePackagesOptions = {
30
30
  sinceRef?: string | null;
31
31
  publicOnly?: boolean;
32
- nonPublishedOnly?: boolean;
33
32
  cwd?: string;
34
33
  filter?: string[];
35
34
  };
36
35
  export declare function getWorkspacePackages(options?: {
37
36
  publicOnly: true;
38
37
  } & GetWorkspacePackagesOptions): Promise<PublicPackage[]>;
39
- export declare function getWorkspacePackages(options?: {
40
- nonPublishedOnly: true;
41
- } & GetWorkspacePackagesOptions): Promise<PublicPackage[]>;
42
38
  export declare function getWorkspacePackages(options?: {
43
39
  publicOnly?: false | undefined;
44
40
  } & GetWorkspacePackagesOptions): Promise<PrivatePackage[]>;
45
41
  export declare function getWorkspacePackages(options?: GetWorkspacePackagesOptions): Promise<(PrivatePackage | PublicPackage)[]>;
46
42
  /**
47
- * Get package version info from registry
43
+ * Resolve the registry a package will be published to.
44
+ *
45
+ * Only `publishConfig.registry` and the ambient `npm_config_registry` are
46
+ * consulted — `.npmrc` layering and `@scope:registry` entries are not.
47
+ *
48
+ * @param {string} packagePath - Path to the package directory
49
+ * @returns {Promise<string>} Normalized registry URL, ending in a slash
50
+ */
51
+ export declare function getPublishRegistry(packagePath: string): Promise<string>;
52
+ /**
53
+ * Filter to the packages that must be published by hand before CI can take over.
54
+ *
55
+ * A brand new package has to be pushed once manually (see
56
+ * `code-infra publish-new-package`) before the OIDC-based workflow can publish
57
+ * it, so the release fails with a clear message instead of a confusing 404
58
+ * midway through. See {@link getPackageVersionInfo} for the version-level check.
59
+ *
60
+ * @param {PublicPackage[]} packages - Packages to check
61
+ * @returns {Promise<PublicPackage[]>} The subset needing a manual first publish
62
+ */
63
+ export declare function getPackagesNeedingManualPublish(packages: PublicPackage[]): Promise<PublicPackage[]>;
64
+ /**
65
+ * Get package version info from registry.
66
+ *
67
+ * Resolves through `pnpm view`, which reports a lookup failure the same way it
68
+ * reports a missing version. Use {@link getPackagesNeedingManualPublish} where
69
+ * absence has to be told apart from an error.
70
+ *
48
71
  * @param {string} packageName - Name of the package
49
72
  * @param {string} baseVersion - Base version to check
50
73
  * @returns {Promise<VersionInfo>} Version information
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-code-infra",
3
- "version": "0.0.4-canary.95",
3
+ "version": "0.0.4-canary.97",
4
4
  "author": "MUI Team",
5
5
  "description": "Infra scripts and configs to be used across MUI repos.",
6
6
  "license": "MIT",
@@ -136,8 +136,8 @@
136
136
  "yaml": "^2.9.0",
137
137
  "yargs": "^18.0.0",
138
138
  "@mui/internal-babel-plugin-display-name": "1.0.4-canary.23",
139
- "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.40",
140
- "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.30"
139
+ "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.30",
140
+ "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.40"
141
141
  },
142
142
  "peerDependencies": {
143
143
  "@next/eslint-plugin-next": "*",
@@ -188,7 +188,7 @@
188
188
  "publishConfig": {
189
189
  "access": "public"
190
190
  },
191
- "gitSha": "446bfbb8d10f0a108322553b707a5f54d2993c24",
191
+ "gitSha": "f5098e75e68dc8dcfc3c4535b1a0a3fb97c6c68d",
192
192
  "scripts": {
193
193
  "build": "tsgo -p tsconfig.build.json",
194
194
  "typescript": "tsgo -noEmit",
@@ -6,12 +6,14 @@
6
6
  * @typedef {import('../utils/pnpm.mjs').PublicPackage} PublicPackage
7
7
  */
8
8
 
9
+ import * as fs from 'node:fs/promises';
10
+ import * as path from 'node:path';
9
11
  import { getWorkspacePackages } from '../utils/pnpm.mjs';
10
12
 
11
13
  /**
12
14
  * @typedef {Object} Args
13
15
  * @property {boolean} [publicOnly] - Whether to filter to only public packages
14
- * @property {'json'|'path'|'name'} [output] - Output format (name, path, or json)
16
+ * @property {'json'|'path'|'name'|'publish-dir'} [output] - Output format (name, path, or json)
15
17
  * @property {string} [sinceRef] - Git reference to filter changes since
16
18
  * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
17
19
  */
@@ -28,10 +30,10 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
28
30
  })
29
31
  .option('output', {
30
32
  type: 'string',
31
- choices: ['json', 'path', 'name'],
33
+ choices: ['json', 'path', 'name', 'publish-dir'],
32
34
  default: 'path',
33
35
  description:
34
- 'Output format: name (package names), path (package paths), or json (full JSON)',
36
+ 'Output format: name (package names), path (package paths), publish-dir (publish directories), or json (full JSON)',
35
37
  })
36
38
  .option('since-ref', {
37
39
  type: 'string',
@@ -58,6 +60,32 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
58
60
  packages.forEach((pkg) => {
59
61
  console.log(pkg.path);
60
62
  });
63
+ } else if (output === 'publish-dir') {
64
+ // Works around https://github.com/stackblitz-labs/pkg.pr.new/issues/389: pkg-pr-new rewrites
65
+ // workspace:* deps to its URLs in the source package.json, but `pnpm pack` honors
66
+ // publishConfig.directory and packs the build package.json, whose workspace:* deps are left to
67
+ // resolve to plain versions. Pointing pkg-pr-new at the build dir makes both act on the same file.
68
+ // Note: #389 was closed by pkg-pr-new#499, but that only fixed the tarball location, not the dep
69
+ // resolution — verified still broken on 0.0.78. Do not remove until the packed deps are URLs
70
+ // without this. Removing it prematurely regressed publishing once (see mui-public#1354).
71
+ // Print publish directories (package.json publishConfig.directory or package path)
72
+ const publishDirs = await Promise.all(
73
+ packages.map(async (pkg) => {
74
+ const packageJsonPath = path.join(pkg.path, 'package.json');
75
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
76
+ const packageJson = JSON.parse(packageJsonContent);
77
+
78
+ if (packageJson.publishConfig?.directory) {
79
+ return path.join(pkg.path, packageJson.publishConfig.directory);
80
+ }
81
+
82
+ return pkg.path;
83
+ }),
84
+ );
85
+
86
+ publishDirs.forEach((dir) => {
87
+ console.log(dir);
88
+ });
61
89
  } else if (output === 'name') {
62
90
  // Print package names (default)
63
91
  packages.forEach((pkg) => {
@@ -19,6 +19,7 @@ import * as semver from 'semver';
19
19
 
20
20
  import { persistentAuthStrategy } from '../utils/github.mjs';
21
21
  import {
22
+ getPackagesNeedingManualPublish,
22
23
  getWorkspacePackages,
23
24
  publishPackages,
24
25
  validatePublishDependencies,
@@ -327,9 +328,9 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
327
328
  // Get all packages
328
329
  console.log('🔍 Discovering all workspace packages...');
329
330
 
330
- const allPackages = await getWorkspacePackages({ publicOnly: true, filter });
331
+ const filteredPackages = await getWorkspacePackages({ publicOnly: true, filter });
331
332
 
332
- if (allPackages.length === 0) {
333
+ if (filteredPackages.length === 0) {
333
334
  console.log(
334
335
  `⚠️ No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
335
336
  );
@@ -339,7 +340,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
339
340
  if (filter.length > 0) {
340
341
  console.log('🔍 Validating workspace dependencies for filtered packages...');
341
342
 
342
- const { issues } = await validatePublishDependencies(allPackages);
343
+ const { issues } = await validatePublishDependencies(filteredPackages);
343
344
 
344
345
  if (issues.length > 0) {
345
346
  throw new Error(
@@ -369,20 +370,23 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
369
370
  githubReleaseData = await validateGitHubRelease(version);
370
371
  }
371
372
 
372
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
373
+ const newPackages = await getPackagesNeedingManualPublish(filteredPackages);
373
374
 
374
375
  if (newPackages.length > 0) {
376
+ const newPackageNames = newPackages.map((pkg) => pkg.name).join(', ');
375
377
  throw new Error(
376
- `The following packages are new and need to be published manually first: ${newPackages.join(
377
- ', ',
378
- )}. Read more about it here: https://github.com/mui/mui-public/blob/master/packages/code-infra/README.md#adding-and-publishing-new-packages`,
378
+ `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`,
379
379
  );
380
380
  }
381
381
 
382
382
  // Publish to npm (pnpm handles duplicate checking automatically)
383
383
  // No git checks, we'll do our own
384
384
  console.log('\n📦 Publishing packages to npm...');
385
- const publishedPackages = await publishToNpm(allPackages, { dryRun, noGitChecks: true, tag });
385
+ const publishedPackages = await publishToNpm(filteredPackages, {
386
+ dryRun,
387
+ noGitChecks: true,
388
+ tag,
389
+ });
386
390
 
387
391
  if (publishedPackages.length === 0) {
388
392
  console.log('ℹ️ No packages were published (all may already be up to date on npm)');
@@ -428,11 +432,13 @@ const PUBLISH_WORKFLOW_ID = `.github/${WORKFLOW_PATH}`;
428
432
  */
429
433
  async function triggerLocalGithubPublishWorkflow(opts) {
430
434
  console.log(`🔍 Checking if there are new packages to publish in the workspace...`);
431
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
435
+ const newPackages = await getPackagesNeedingManualPublish(
436
+ await getWorkspacePackages({ publicOnly: true }),
437
+ );
432
438
  if (newPackages.length) {
433
439
  console.warn(
434
440
  `⚠️ Found new packages that should be published to npm first before triggering a release:
435
- * ${newPackages.map((pkg) => pkg.name).join(' * ')}
441
+ ${newPackages.map((pkg) => ` * ${pkg.name}`).join('\n')}
436
442
  Please run the command "${chalk.bold('pnpm code-infra publish-new-package')}" first to publish and configure npm.`,
437
443
  );
438
444
  return;
@@ -11,7 +11,7 @@ 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 { getWorkspacePackages } from '../utils/pnpm.mjs';
14
+ import { getPackagesNeedingManualPublish, getWorkspacePackages } from '../utils/pnpm.mjs';
15
15
 
16
16
  /**
17
17
  * @typedef {Object} Args
@@ -46,7 +46,9 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
46
46
  }),
47
47
  async handler(args) {
48
48
  console.log(`🔍 Detecting new packages to publish in workspace...`);
49
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
49
+ const newPackages = await getPackagesNeedingManualPublish(
50
+ await getWorkspacePackages({ publicOnly: true }),
51
+ );
50
52
 
51
53
  if (!newPackages.length) {
52
54
  console.log('No new packages to publish.');
@@ -7,6 +7,9 @@ import * as path from 'node:path';
7
7
  import * as semver from 'semver';
8
8
  import { parseDocument, isMap } from 'yaml';
9
9
 
10
+ /** Canonical npm registry URL, in the form `getPublishRegistry` returns. */
11
+ const NPMJS_REGISTRY = 'https://registry.npmjs.org/';
12
+
10
13
  /**
11
14
  * @typedef {Object} PrivatePackage
12
15
  * @property {string} [name] - Package name
@@ -48,7 +51,6 @@ import { parseDocument, isMap } from 'yaml';
48
51
  * @typedef {Object} GetWorkspacePackagesOptions
49
52
  * @property {string|null} [sinceRef] - Git reference to filter changes since
50
53
  * @property {boolean} [publicOnly=false] - Whether to filter to only public packages
51
- * @property {boolean} [nonPublishedOnly=false] - Whether to filter to only non-published packages. It by default means public packages yet to be published.
52
54
  * @property {string} [cwd] - Current working directory to run pnpm command in
53
55
  * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
54
56
  */
@@ -61,10 +63,6 @@ import { parseDocument, isMap } from 'yaml';
61
63
  * @returns {Promise<PublicPackage[]>} Array of packages
62
64
  *
63
65
  * @overload
64
- * @param {{ nonPublishedOnly: true } & GetWorkspacePackagesOptions} [options={}] - Options for filtering packages
65
- * @returns {Promise<PublicPackage[]>} Array of packages
66
- *
67
- * @overload
68
66
  * @param {{ publicOnly?: false | undefined } & GetWorkspacePackagesOptions} [options={}] - Options for filtering packages
69
67
  * @returns {Promise<PrivatePackage[]>} Array of packages
70
68
  *
@@ -76,7 +74,7 @@ import { parseDocument, isMap } from 'yaml';
76
74
  * @returns {Promise<(PrivatePackage | PublicPackage)[]>} Array of packages
77
75
  */
78
76
  export async function getWorkspacePackages(options = {}) {
79
- const { sinceRef = null, publicOnly = false, nonPublishedOnly = false, filter = [] } = options;
77
+ const { sinceRef = null, publicOnly = false, filter = [] } = options;
80
78
 
81
79
  /**
82
80
  * Run `pnpm ls` with the given --filter args and return the parsed list.
@@ -125,24 +123,100 @@ export async function getWorkspacePackages(options = {}) {
125
123
  ];
126
124
  });
127
125
 
128
- if (nonPublishedOnly) {
129
- // Check if any of the packages are new/need manual publishing first.
130
- const filteredPublicPackages = filteredPackages.filter((pkg) => !pkg.isPrivate);
126
+ return filteredPackages;
127
+ }
131
128
 
132
- const results = await Promise.all(
133
- filteredPublicPackages.map(async (pkg) => {
134
- const url = `${process.env.npm_config_registry || 'https://registry.npmjs.org'}/${pkg.name}`;
135
- return fetch(url).then((res) => res.status === 404);
136
- }),
129
+ /**
130
+ * Resolve the registry a package will be published to.
131
+ *
132
+ * Only `publishConfig.registry` and the ambient `npm_config_registry` are
133
+ * consulted — `.npmrc` layering and `@scope:registry` entries are not.
134
+ *
135
+ * @param {string} packagePath - Path to the package directory
136
+ * @returns {Promise<string>} Normalized registry URL, ending in a slash
137
+ */
138
+ export async function getPublishRegistry(packagePath) {
139
+ const packageJson = await readPackageJson(packagePath);
140
+ const registry =
141
+ packageJson.publishConfig?.registry || process.env.npm_config_registry || NPMJS_REGISTRY;
142
+
143
+ // Normalizing through the URL parser keeps host casing and default ports from
144
+ // defeating the equality check in `requiresTrustedPublisherBootstrap`. The
145
+ // trailing slash is required because `new URL(name, base)` replaces the last
146
+ // path segment of a base that lacks one, mangling registries served under a
147
+ // path prefix such as Artifactory's `/api/npm/<repo>`.
148
+ let registryUrl;
149
+ try {
150
+ registryUrl = new URL(registry);
151
+ } catch (error) {
152
+ // Node's own message names neither the offending value nor where it came
153
+ // from, leaving the operator to bisect package.json files mid-release.
154
+ throw new Error(
155
+ `Invalid publish registry ${JSON.stringify(registry)} for the package at ${packagePath}`,
156
+ { cause: error },
137
157
  );
138
- return filteredPublicPackages.filter((_pkg, index) => !!results[index]);
139
158
  }
159
+ registryUrl.pathname = `${registryUrl.pathname.replace(/\/+$/, '')}/`;
160
+ return registryUrl.href;
161
+ }
140
162
 
141
- return filteredPackages;
163
+ /**
164
+ * Whether a registry requires a package to exist before CI can publish to it.
165
+ * @param {string} registry - Normalized registry URL
166
+ * @returns {boolean}
167
+ */
168
+ function requiresTrustedPublisherBootstrap(registry) {
169
+ // npm won't attach a Trusted Publisher to a name that doesn't exist yet. No
170
+ // other registry we publish to has an equivalent step.
171
+ return registry === NPMJS_REGISTRY;
172
+ }
173
+
174
+ /**
175
+ * Filter to the packages that must be published by hand before CI can take over.
176
+ *
177
+ * A brand new package has to be pushed once manually (see
178
+ * `code-infra publish-new-package`) before the OIDC-based workflow can publish
179
+ * it, so the release fails with a clear message instead of a confusing 404
180
+ * midway through. See {@link getPackageVersionInfo} for the version-level check.
181
+ *
182
+ * @param {PublicPackage[]} packages - Packages to check
183
+ * @returns {Promise<PublicPackage[]>} The subset needing a manual first publish
184
+ */
185
+ export async function getPackagesNeedingManualPublish(packages) {
186
+ const results = await Promise.all(
187
+ packages.map(async (pkg) => {
188
+ const registry = await getPublishRegistry(pkg.path);
189
+ if (!requiresTrustedPublisherBootstrap(registry)) {
190
+ return false;
191
+ }
192
+
193
+ // HEAD, because only the status matters — a packument runs to megabytes.
194
+ const res = await fetch(new URL(pkg.name, registry), { method: 'HEAD' });
195
+ if (res.status === 404) {
196
+ return true;
197
+ }
198
+ if (!res.ok) {
199
+ // Anything else (401, 5xx, a proxy hiccup) tells us nothing about
200
+ // whether the package exists. Treating it as "already published" would
201
+ // silently disable the check, so fail loudly instead.
202
+ throw new Error(
203
+ `Failed to check whether ${pkg.name} exists on ${registry}: HTTP ${res.status}`,
204
+ );
205
+ }
206
+ return false;
207
+ }),
208
+ );
209
+
210
+ return packages.filter((_pkg, index) => results[index]);
142
211
  }
143
212
 
144
213
  /**
145
- * Get package version info from registry
214
+ * Get package version info from registry.
215
+ *
216
+ * Resolves through `pnpm view`, which reports a lookup failure the same way it
217
+ * reports a missing version. Use {@link getPackagesNeedingManualPublish} where
218
+ * absence has to be told apart from an error.
219
+ *
146
220
  * @param {string} packageName - Name of the package
147
221
  * @param {string} baseVersion - Base version to check
148
222
  * @returns {Promise<VersionInfo>} Version information