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

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.
@@ -263,18 +263,65 @@ export declare function writeOverridesToWorkspace(workspaceDir: string, override
263
263
  */
264
264
  export declare function renameWorkspaceScope(packages: (PublicPackage | PrivatePackage)[], fromScope: string, toScope: string): Promise<Map<string, string>>;
265
265
  /**
266
- * Resolve a package@version specifier to an exact version
266
+ * Pick the version to pin for a specifier whose newest match is too recent to
267
+ * satisfy a `minimumReleaseAge` cooldown.
268
+ *
269
+ * Only the three specifier shapes this tool is given are handled. An exact
270
+ * version is not a choice, so it stands and the install reports it. A range
271
+ * resolves within itself. A dist-tag repoints to the highest aged-in version
272
+ * sharing the major and prerelease-ness — except `latest`, which pnpm allows to
273
+ * cross majors. Unlike pnpm, a deprecated version is not passed over: `pnpm info`
274
+ * lists bare version strings, so that flag would cost a request per candidate.
275
+ *
276
+ * @param {string} requested - The specifier as given: a dist-tag, range or exact version
277
+ * @param {string} resolvedVersion - Version the specifier resolves to today
278
+ * @param {string[]} versions - Every published version, from `pnpm info <pkg> versions`
279
+ * @param {Record<string, string>} publishTimes - Version to ISO publish date, from `pnpm info <pkg> time`
280
+ * @param {number} cutoff - Epoch ms; versions published after this are too recent
281
+ * @param {string[]} [exemptVersions] - Versions `minimumReleaseAgeExclude` installs regardless of age
282
+ * @returns {string | null} Version to pin, or null when nothing qualifies
283
+ * @internal exported for unit tests
284
+ */
285
+ export declare function selectAgedVersion(requested: string, resolvedVersion: string, versions: string[], publishTimes: Record<string, string>, cutoff: number, exemptVersions?: string[]): string | null;
286
+ /**
287
+ * Read the effective pnpm configuration, including everything resolved from
288
+ * pnpm-workspace.yaml. `config list` types the values and omits unset keys,
289
+ * where `config get` stringifies everything and prints `undefined`.
290
+ *
291
+ * @returns {Promise<Record<string, any>>} Parsed configuration
292
+ */
293
+ export declare function readPnpmConfig(): Promise<Record<string, any>>;
294
+ /**
295
+ * Read the registry cooldown from pnpm config, so resolution stays in step with
296
+ * what the subsequent install will enforce.
297
+ *
298
+ * @returns {Promise<import('@pnpm/config.version-policy').PublishedByPolicy>} Cutoff date and exemption policy, both undefined when no cooldown is configured
299
+ */
300
+ export declare function getMinimumReleaseAgePolicy(): Promise<import('@pnpm/config.version-policy').PublishedByPolicy>;
301
+ /**
302
+ * Resolve a package@version specifier to an exact version.
303
+ *
304
+ * Given a cooldown policy, the result is the newest version the install will
305
+ * actually accept — a dist-tag on a daily channel always points at a build too
306
+ * recent to install under one. See {@link selectAgedVersion}.
307
+ *
267
308
  * @param {string} packageSpec - Package specifier in format "package@version"
309
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Cooldown from {@link getMinimumReleaseAgePolicy}
268
310
  * @returns {Promise<string>} Exact version string
269
311
  */
270
- export declare function resolveVersion(packageSpec: string): Promise<string>;
312
+ export declare function resolveVersion(packageSpec: string, policy: import('@pnpm/config.version-policy').PublishedByPolicy): Promise<string>;
271
313
  /**
272
314
  * Find the version of a dependency for a specific package@version
315
+ *
316
+ * The parent's spec is often a range, so the cooldown applies here too. Stepping
317
+ * back stays within that range and so never contradicts the parent.
318
+ *
273
319
  * @param {string} packageSpec - Package specifier in format "package@version"
274
320
  * @param {string} dependency - Dependency name to look up
321
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Registry cooldown to resolve within
275
322
  * @returns {Promise<string>} Exact version string of the dependency
276
323
  */
277
- export declare function findDependencyVersionFromSpec(packageSpec: string, dependency: string): Promise<string>;
324
+ export declare function findDependencyVersionFromSpec(packageSpec: string, dependency: string, policy: import('@pnpm/config.version-policy').PublishedByPolicy): Promise<string>;
278
325
  /**
279
326
  * Get the maximum semver version between two versions
280
327
  * @param {string} a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-code-infra",
3
- "version": "0.0.4-canary.100",
3
+ "version": "0.0.4-canary.101",
4
4
  "author": "MUI Team",
5
5
  "description": "Infra scripts and configs to be used across MUI repos.",
6
6
  "license": "MIT",
@@ -71,7 +71,9 @@
71
71
  "@octokit/auth-action": "^6.0.2",
72
72
  "@octokit/oauth-methods": "^6.0.2",
73
73
  "@octokit/rest": "^22.0.1",
74
+ "@pnpm/config.version-policy": "^1100.1.9",
74
75
  "@pnpm/find-workspace-dir": "^1000.1.5",
76
+ "@pnpm/parse-wanted-dependency": "^1001.0.0",
75
77
  "@typescript-eslint/types": "^8.64.0",
76
78
  "@typescript-eslint/utils": "^8.64.0",
77
79
  "@vitest/eslint-plugin": "^1.6.23",
@@ -188,7 +190,7 @@
188
190
  "publishConfig": {
189
191
  "access": "public"
190
192
  },
191
- "gitSha": "6542022a25f84984a49b5a8beafdff2df0e0966b",
193
+ "gitSha": "cbb48ed31d032fb3e0f5b02d9eaf72fcd5828376",
192
194
  "scripts": {
193
195
  "build": "tsgo -p tsconfig.build.json",
194
196
  "typescript": "tsgo -noEmit",
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable no-console */
2
2
  import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
3
- import { $ } from 'execa';
4
3
  import { globby } from 'globby';
5
4
  import * as fs from 'node:fs/promises';
6
5
  import * as path from 'node:path';
@@ -15,6 +14,7 @@ import {
15
14
  mapConcurrently,
16
15
  validatePkgJson,
17
16
  } from '../utils/build.mjs';
17
+ import { readPnpmConfig } from '../utils/pnpm.mjs';
18
18
 
19
19
  /**
20
20
  * @typedef {Object} Args
@@ -279,8 +279,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
279
279
  if (babelRuntimeVersion === 'catalog:') {
280
280
  // resolve the version from the given package
281
281
  // outputs the pnpm-workspace.yaml config as json
282
- const { stdout: configStdout } = await $`pnpm config list --json`;
283
- const pnpmWorkspaceConfig = JSON.parse(configStdout);
282
+ const pnpmWorkspaceConfig = await readPnpmConfig();
284
283
  babelRuntimeVersion = pnpmWorkspaceConfig.catalog['@babel/runtime'];
285
284
  }
286
285
 
@@ -3,7 +3,9 @@
3
3
  import * as semver from 'semver';
4
4
  import { $ } from 'execa';
5
5
  import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
6
+ import { parseWantedDependency } from '@pnpm/parse-wanted-dependency';
6
7
  import {
8
+ getMinimumReleaseAgePolicy,
7
9
  resolveVersion,
8
10
  findDependencyVersionFromSpec,
9
11
  writeOverridesToWorkspace,
@@ -17,61 +19,62 @@ import {
17
19
  /**
18
20
  * Process a single package override
19
21
  * @param {string} packageSpec - Package specifier in format "package@version"
22
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Registry cooldown to resolve within
20
23
  * @returns {Promise<Record<string, string>>} Overrides object for this package
21
24
  */
22
- async function processPackageOverride(packageSpec) {
25
+ async function processPackageOverride(packageSpec, policy) {
23
26
  /** @type {Record<string, string>} */
24
27
  const overrides = {};
25
28
 
26
- // Extract package name to check for special cases
27
- const lastAtIndex = packageSpec.lastIndexOf('@');
28
- if (lastAtIndex === -1) {
29
+ const { alias: packageName, bareSpecifier: version } = parseWantedDependency(packageSpec);
30
+ if (!packageName || version === undefined) {
29
31
  throw new Error(`Invalid package specifier: ${packageSpec}`);
30
32
  }
31
33
 
32
- const packageName = packageSpec.slice(0, lastAtIndex);
33
- const version = packageSpec.slice(lastAtIndex + 1);
34
-
35
- if (!packageName || !version || version === 'stable') {
34
+ // An empty version is distinct from a missing one: CI interpolates a matrix
35
+ // value into `--pkg pkg@<version>`, and the leg that leaves it unset must skip
36
+ // the override rather than fail the job.
37
+ if (!version || version === 'stable') {
36
38
  return overrides;
37
39
  }
38
40
 
39
41
  // eslint-disable-next-line no-console
40
42
  console.log(`Resolving overrides for ${packageName} version: ${version}`);
41
43
 
44
+ /** @param {string} spec */
45
+ const resolve = (spec) => resolveVersion(spec, policy);
46
+
42
47
  if (packageName === 'react') {
43
48
  // Special case for React - also override related packages
44
- overrides.react = await resolveVersion(packageSpec);
45
- overrides['react-dom'] = await resolveVersion(`react-dom@${version}`);
46
- overrides['react-is'] = await resolveVersion(`react-is@${version}`);
49
+ overrides.react = await resolve(packageSpec);
50
+ overrides['react-dom'] = await resolve(`react-dom@${version}`);
51
+ overrides['react-is'] = await resolve(`react-is@${version}`);
47
52
  overrides.scheduler = await findDependencyVersionFromSpec(
48
53
  `react-dom@${overrides['react-dom']}`,
49
54
  'scheduler',
55
+ policy,
50
56
  );
51
57
 
52
58
  const reactMajor = semver.major(overrides.react);
53
59
  if (reactMajor === 17) {
54
- overrides['@testing-library/react'] = await resolveVersion('@testing-library/react@^12.1.0');
60
+ overrides['@testing-library/react'] = await resolve('@testing-library/react@^12.1.0');
55
61
  }
56
62
  } else if (packageName === '@mui/material') {
57
63
  // Special case for MUI - also override related packages
58
- overrides['@mui/material'] = await resolveVersion(`@mui/material@${version}`);
59
- overrides['@mui/system'] = await resolveVersion(`@mui/system@${version}`);
60
- overrides['@mui/icons-material'] = await resolveVersion(`@mui/icons-material@${version}`);
61
- overrides['@mui/utils'] = await resolveVersion(`@mui/utils@${version}`);
62
- overrides['@mui/material-nextjs'] = await resolveVersion(`@mui/material-nextjs@${version}`);
64
+ overrides['@mui/material'] = await resolve(`@mui/material@${version}`);
65
+ overrides['@mui/system'] = await resolve(`@mui/system@${version}`);
66
+ overrides['@mui/icons-material'] = await resolve(`@mui/icons-material@${version}`);
67
+ overrides['@mui/utils'] = await resolve(`@mui/utils@${version}`);
68
+ overrides['@mui/material-nextjs'] = await resolve(`@mui/material-nextjs@${version}`);
63
69
 
64
- const latest = await resolveVersion(`@mui/material@latest`);
70
+ const latest = await resolve(`@mui/material@latest`);
65
71
  const latestMajor = semver.major(latest);
66
72
  const muiMajor = semver.major(overrides['@mui/material']);
67
- if (muiMajor < latestMajor) {
68
- overrides['@mui/lab'] = await resolveVersion(`@mui/lab@latest-v${muiMajor}`);
69
- } else {
70
- overrides['@mui/lab'] = await resolveVersion(`@mui/lab@latest`);
71
- }
73
+ const labTag = muiMajor < latestMajor ? `latest-v${muiMajor}` : 'latest';
74
+ overrides['@mui/lab'] = await resolve(`@mui/lab@${labTag}`);
72
75
  } else {
73
76
  // Generic case for other packages
74
- overrides[packageName] = await resolveVersion(packageSpec);
77
+ overrides[packageName] = await resolve(packageSpec);
75
78
  }
76
79
 
77
80
  return overrides;
@@ -89,8 +92,12 @@ async function handler(args) {
89
92
  return;
90
93
  }
91
94
 
95
+ // Read once and thread through: every override must land on a version that
96
+ // clears the same cooldown the following `pnpm dedupe` enforces.
97
+ const policy = await getMinimumReleaseAgePolicy();
98
+
92
99
  const packageOverridePromises = args.pkg.map((packageSpec) =>
93
- processPackageOverride(packageSpec),
100
+ processPackageOverride(packageSpec, policy),
94
101
  );
95
102
  const packageOverrideResults = await Promise.all(packageOverridePromises);
96
103
 
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { getPublishedByPolicy } from '@pnpm/config.version-policy';
4
+ import { parseWantedDependency } from '@pnpm/parse-wanted-dependency';
3
5
  import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
4
6
  import { $ } from 'execa';
5
7
  import * as fs from 'node:fs/promises';
@@ -328,10 +330,10 @@ function aliasTarget(spec) {
328
330
  if (!spec.startsWith('workspace:')) {
329
331
  return null;
330
332
  }
331
- const range = spec.slice('workspace:'.length);
332
- // Search from 1 so a scoped name's own leading `@` isn't read as the separator.
333
- const separator = range.indexOf('@', 1);
334
- return separator === -1 ? null : range.slice(0, separator);
333
+ const { alias, bareSpecifier } = parseWantedDependency(spec.slice('workspace:'.length));
334
+ // A plain range parses as one half or the other (`*` as an alias, `^1.0.0` as a
335
+ // specifier); only an aliased spec carries both.
336
+ return bareSpecifier === undefined ? null : (alias ?? null);
335
337
  }
336
338
 
337
339
  /**
@@ -720,26 +722,209 @@ export async function renameWorkspaceScope(packages, fromScope, toScope) {
720
722
  }
721
723
 
722
724
  /**
723
- * Resolve a package@version specifier to an exact version
725
+ * The versions a `minimumReleaseAgeExclude` policy exempts from the cooldown. A
726
+ * policy answers `true` for a package that is exempt at any version, or the list
727
+ * of versions exempted individually.
728
+ *
729
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Policy from {@link getMinimumReleaseAgePolicy}
730
+ * @param {string} name - Package name
731
+ * @returns {true | string[]} `true` when every version is exempt, otherwise the exempt versions
732
+ */
733
+ function cooldownExemptions(policy, name) {
734
+ const exemption = policy.publishedByExclude?.(name);
735
+ if (exemption === true) {
736
+ return true;
737
+ }
738
+ return Array.isArray(exemption) ? exemption : [];
739
+ }
740
+
741
+ /**
742
+ * Pick the version to pin for a specifier whose newest match is too recent to
743
+ * satisfy a `minimumReleaseAge` cooldown.
744
+ *
745
+ * Only the three specifier shapes this tool is given are handled. An exact
746
+ * version is not a choice, so it stands and the install reports it. A range
747
+ * resolves within itself. A dist-tag repoints to the highest aged-in version
748
+ * sharing the major and prerelease-ness — except `latest`, which pnpm allows to
749
+ * cross majors. Unlike pnpm, a deprecated version is not passed over: `pnpm info`
750
+ * lists bare version strings, so that flag would cost a request per candidate.
751
+ *
752
+ * @param {string} requested - The specifier as given: a dist-tag, range or exact version
753
+ * @param {string} resolvedVersion - Version the specifier resolves to today
754
+ * @param {string[]} versions - Every published version, from `pnpm info <pkg> versions`
755
+ * @param {Record<string, string>} publishTimes - Version to ISO publish date, from `pnpm info <pkg> time`
756
+ * @param {number} cutoff - Epoch ms; versions published after this are too recent
757
+ * @param {string[]} [exemptVersions] - Versions `minimumReleaseAgeExclude` installs regardless of age
758
+ * @returns {string | null} Version to pin, or null when nothing qualifies
759
+ * @internal exported for unit tests
760
+ */
761
+ export function selectAgedVersion(
762
+ requested,
763
+ resolvedVersion,
764
+ versions,
765
+ publishTimes,
766
+ cutoff,
767
+ exemptVersions = [],
768
+ ) {
769
+ const exempt = new Set(exemptVersions);
770
+
771
+ /**
772
+ * Whether the install would accept this version: old enough, or excluded from
773
+ * the cooldown altogether.
774
+ *
775
+ * @param {string} version
776
+ * @returns {boolean}
777
+ */
778
+ const isInstallable = (version) => {
779
+ if (exempt.has(version)) {
780
+ return true;
781
+ }
782
+ const published = Date.parse(publishTimes[version]);
783
+ return Number.isFinite(published) && published <= cutoff;
784
+ };
785
+
786
+ // An unknown publish date leaves nothing to compare against, so the
787
+ // resolution stands rather than being swapped for another build.
788
+ if (!Object.hasOwn(publishTimes, resolvedVersion) || isInstallable(resolvedVersion)) {
789
+ return resolvedVersion;
790
+ }
791
+
792
+ if (semver.valid(requested)) {
793
+ return resolvedVersion;
794
+ }
795
+
796
+ // `time` keeps an entry for a version after it is unpublished, so candidates
797
+ // come from the version list rather than from the dates.
798
+ const candidates = versions.filter(isInstallable);
799
+
800
+ if (semver.validRange(requested)) {
801
+ return semver.maxSatisfying(candidates, requested, true);
802
+ }
803
+
804
+ const resolved = semver.parse(resolvedVersion, true);
805
+ if (!resolved) {
806
+ return resolvedVersion;
807
+ }
808
+ const resolvedIsPrerelease = resolved.prerelease.length > 0;
809
+
810
+ let best = null;
811
+ for (const version of candidates) {
812
+ const parsed = semver.parse(version, true);
813
+ if (
814
+ !parsed ||
815
+ (requested !== 'latest' && parsed.major !== resolved.major) ||
816
+ parsed.prerelease.length > 0 !== resolvedIsPrerelease
817
+ ) {
818
+ continue;
819
+ }
820
+ if (!best || semver.gt(version, best, true)) {
821
+ best = version;
822
+ }
823
+ }
824
+ return best;
825
+ }
826
+
827
+ /**
828
+ * Read the effective pnpm configuration, including everything resolved from
829
+ * pnpm-workspace.yaml. `config list` types the values and omits unset keys,
830
+ * where `config get` stringifies everything and prints `undefined`.
831
+ *
832
+ * @returns {Promise<Record<string, any>>} Parsed configuration
833
+ */
834
+ export async function readPnpmConfig() {
835
+ const result = await $`pnpm config list --json`;
836
+ return JSON.parse(result.stdout);
837
+ }
838
+
839
+ /**
840
+ * Read the registry cooldown from pnpm config, so resolution stays in step with
841
+ * what the subsequent install will enforce.
842
+ *
843
+ * @returns {Promise<import('@pnpm/config.version-policy').PublishedByPolicy>} Cutoff date and exemption policy, both undefined when no cooldown is configured
844
+ */
845
+ export async function getMinimumReleaseAgePolicy() {
846
+ const config = await readPnpmConfig();
847
+ // pnpm's own parser, so exclude entries keep their full grammar — wildcards,
848
+ // version unions (`pkg@1.0.0 || 2.0.0`) and `!` negation.
849
+ return getPublishedByPolicy({
850
+ minimumReleaseAge: config.minimumReleaseAge,
851
+ minimumReleaseAgeExclude: config.minimumReleaseAgeExclude,
852
+ });
853
+ }
854
+
855
+ /**
856
+ * Resolve a package@version specifier to an exact version.
857
+ *
858
+ * Given a cooldown policy, the result is the newest version the install will
859
+ * actually accept — a dist-tag on a daily channel always points at a build too
860
+ * recent to install under one. See {@link selectAgedVersion}.
861
+ *
724
862
  * @param {string} packageSpec - Package specifier in format "package@version"
863
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Cooldown from {@link getMinimumReleaseAgePolicy}
725
864
  * @returns {Promise<string>} Exact version string
726
865
  */
727
- export async function resolveVersion(packageSpec) {
728
- const result = await $`pnpm info ${packageSpec} version --json`;
729
- const versions = JSON.parse(result.stdout);
730
- return typeof versions === 'string' ? versions : versions[versions.length - 1];
866
+ export async function resolveVersion(packageSpec, policy) {
867
+ // The unprojected document carries both the resolved version and every
868
+ // publish date, so honouring the cooldown costs no extra round-trip.
869
+ const info = JSON.parse((await $`pnpm info ${packageSpec} --json`).stdout);
870
+ const manifest = Array.isArray(info) ? info[info.length - 1] : info;
871
+ const { name, version: exactVersion, time: publishTimes = {}, versions = [] } = manifest;
872
+
873
+ if (!policy.publishedBy) {
874
+ return exactVersion;
875
+ }
876
+
877
+ const exemptVersions = cooldownExemptions(policy, name);
878
+ if (exemptVersions === true || exemptVersions.includes(exactVersion)) {
879
+ return exactVersion;
880
+ }
881
+
882
+ const cutoff = policy.publishedBy.toISOString();
883
+ const { bareSpecifier: requested } = parseWantedDependency(packageSpec);
884
+ const agedVersion = selectAgedVersion(
885
+ requested ?? '',
886
+ exactVersion,
887
+ versions,
888
+ publishTimes,
889
+ policy.publishedBy.getTime(),
890
+ exemptVersions,
891
+ );
892
+
893
+ if (!agedVersion) {
894
+ throw new Error(
895
+ `No version matching ${packageSpec} was published before the minimumReleaseAge cutoff ` +
896
+ `(${cutoff}). The newest match, ${exactVersion}, was published at ` +
897
+ `${publishTimes[exactVersion]}, and no earlier match qualifies. Lower ` +
898
+ `minimumReleaseAge, add ${name} to minimumReleaseAgeExclude, or wait for ${exactVersion} ` +
899
+ `to age in.`,
900
+ );
901
+ }
902
+
903
+ if (agedVersion !== exactVersion) {
904
+ // eslint-disable-next-line no-console
905
+ console.log(
906
+ `Resolved ${packageSpec} to ${agedVersion} rather than ${exactVersion}, which was published after the minimumReleaseAge cutoff (${cutoff}).`,
907
+ );
908
+ }
909
+
910
+ return agedVersion;
731
911
  }
732
912
 
733
913
  /**
734
914
  * Find the version of a dependency for a specific package@version
915
+ *
916
+ * The parent's spec is often a range, so the cooldown applies here too. Stepping
917
+ * back stays within that range and so never contradicts the parent.
918
+ *
735
919
  * @param {string} packageSpec - Package specifier in format "package@version"
736
920
  * @param {string} dependency - Dependency name to look up
921
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Registry cooldown to resolve within
737
922
  * @returns {Promise<string>} Exact version string of the dependency
738
923
  */
739
- export async function findDependencyVersionFromSpec(packageSpec, dependency) {
924
+ export async function findDependencyVersionFromSpec(packageSpec, dependency, policy) {
740
925
  const result = await $`pnpm info ${packageSpec} dependencies.${dependency}`;
741
926
  const spec = result.stdout.trim();
742
- return resolveVersion(`${dependency}@${spec}`);
927
+ return resolveVersion(`${dependency}@${spec}`, policy);
743
928
  }
744
929
 
745
930
  /**