@mui/internal-code-infra 0.0.4-canary.99 → 0.1.1-canary.1

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.
@@ -6,9 +6,12 @@ import minifyErrorsPlugin from '@mui/internal-babel-plugin-minify-errors';
6
6
  import { globby } from 'globby';
7
7
  import * as fs from 'node:fs/promises';
8
8
  import * as path from 'node:path';
9
+ import { mapAsync } from 'es-toolkit/array';
9
10
 
10
11
  import { getWorkspacePackages } from './pnpm.mjs';
11
- import { BASE_IGNORES, mapConcurrently } from './build.mjs';
12
+ import { BASE_IGNORES } from './build.mjs';
13
+
14
+ const PLAIN_TS_EXTENSION = /\.[cm]?ts$/;
12
15
 
13
16
  /**
14
17
  * @typedef {Object} Args
@@ -51,18 +54,20 @@ async function getFilesForPackage(pkg) {
51
54
  * @param {Set<string | Error>} errors
52
55
  * @param {import('@mui/internal-babel-plugin-minify-errors').Options['detection']} [detection='opt-in']
53
56
  */
54
- async function extractErrorCodesForWorkspace(files, errors, detection = 'opt-in') {
55
- await mapConcurrently(
57
+ export async function extractErrorCodesForFiles(files, errors, detection = 'opt-in') {
58
+ await mapAsync(
56
59
  files,
57
60
  async (fullPath) => {
58
61
  const code = await fs.readFile(fullPath, 'utf8');
62
+ // Plain .ts files must not get the JSX parser: `<T = unknown>(x) => x` would be read as JSX.
63
+ const isPlainTs = PLAIN_TS_EXTENSION.test(fullPath);
59
64
  // Replicate how Babel is run when preparing the source to be published on npm, so we can extract the same messages.
60
65
  await transformAsync(code, {
61
66
  filename: fullPath,
62
67
  sourceType: 'module',
63
68
  plugins: [
64
- [babelSyntaxTypescript, { isTSX: true }],
65
- [babelSyntaxJsx],
69
+ [babelSyntaxTypescript, { isTSX: !isPlainTs }],
70
+ ...(isPlainTs ? [] : [babelSyntaxJsx]),
66
71
  [minifyErrorsPlugin, { collectErrors: errors, detection }],
67
72
  ],
68
73
  configFile: false,
@@ -71,7 +76,7 @@ async function extractErrorCodesForWorkspace(files, errors, detection = 'opt-in'
71
76
  code: false,
72
77
  });
73
78
  },
74
- 30,
79
+ { concurrency: 30 },
75
80
  );
76
81
  }
77
82
 
@@ -108,7 +113,7 @@ export default async function extractErrorCodes(args) {
108
113
  // Extract error codes from said files.
109
114
  const filesToProcess = files.flat();
110
115
  console.log(`🔍 Extracting error codes from ${filesToProcess.length} files...`);
111
- await extractErrorCodesForWorkspace(filesToProcess, errors, detection);
116
+ await extractErrorCodesForFiles(filesToProcess, errors, detection);
112
117
 
113
118
  // Partition collected entries into valid messages and unminifyable errors.
114
119
  /** @type {string[]} */
@@ -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';
@@ -160,6 +162,21 @@ export async function getPublishRegistry(packagePath) {
160
162
  return registryUrl.href;
161
163
  }
162
164
 
165
+ /**
166
+ * Get the version to release from the workspace-root package.json.
167
+ *
168
+ * Resolves the workspace root so the version is the monorepo's regardless of
169
+ * which directory publish runs from.
170
+ *
171
+ * @param {string} [cwd] - Directory to resolve the workspace root from
172
+ * @returns {Promise<string | null>} Version string, or null when absent/invalid
173
+ */
174
+ export async function getReleaseVersion(cwd = process.cwd()) {
175
+ const workspaceDir = (await findWorkspaceDir(cwd)) ?? cwd;
176
+ const { version } = await readPackageJson(workspaceDir);
177
+ return version ? semver.valid(version) : null;
178
+ }
179
+
163
180
  /**
164
181
  * Whether a registry requires a package to exist before CI can publish to it.
165
182
  * @param {string} registry - Normalized registry URL
@@ -313,10 +330,10 @@ function aliasTarget(spec) {
313
330
  if (!spec.startsWith('workspace:')) {
314
331
  return null;
315
332
  }
316
- const range = spec.slice('workspace:'.length);
317
- // Search from 1 so a scoped name's own leading `@` isn't read as the separator.
318
- const separator = range.indexOf('@', 1);
319
- 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);
320
337
  }
321
338
 
322
339
  /**
@@ -705,26 +722,209 @@ export async function renameWorkspaceScope(packages, fromScope, toScope) {
705
722
  }
706
723
 
707
724
  /**
708
- * 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
+ *
709
862
  * @param {string} packageSpec - Package specifier in format "package@version"
863
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Cooldown from {@link getMinimumReleaseAgePolicy}
710
864
  * @returns {Promise<string>} Exact version string
711
865
  */
712
- export async function resolveVersion(packageSpec) {
713
- const result = await $`pnpm info ${packageSpec} version --json`;
714
- const versions = JSON.parse(result.stdout);
715
- 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;
716
911
  }
717
912
 
718
913
  /**
719
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
+ *
720
919
  * @param {string} packageSpec - Package specifier in format "package@version"
721
920
  * @param {string} dependency - Dependency name to look up
921
+ * @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Registry cooldown to resolve within
722
922
  * @returns {Promise<string>} Exact version string of the dependency
723
923
  */
724
- export async function findDependencyVersionFromSpec(packageSpec, dependency) {
924
+ export async function findDependencyVersionFromSpec(packageSpec, dependency, policy) {
725
925
  const result = await $`pnpm info ${packageSpec} dependencies.${dependency}`;
726
926
  const spec = result.stdout.trim();
727
- return resolveVersion(`${dependency}@${spec}`);
927
+ return resolveVersion(`${dependency}@${spec}`, policy);
728
928
  }
729
929
 
730
930
  /**
@@ -9,26 +9,49 @@ import { globby } from 'globby';
9
9
  import * as fs from 'node:fs/promises';
10
10
  import * as os from 'node:os';
11
11
  import * as path from 'node:path';
12
- import { getOutExtension, mapConcurrently } from '../utils/build.mjs';
12
+ import { mapAsync } from 'es-toolkit/array';
13
+ import { getOutExtension } from '../utils/build.mjs';
13
14
 
14
15
  const $$ = $({ stdio: 'inherit' });
15
16
 
16
17
  /**
17
- * Checks if tsgo CLI is available in the workspace's node_modules.
18
- * @param {string} cwd - The current working directory to start searching from.
19
- * @returns {Promise<string | null>} - The path to tsgo if found, null otherwise.
18
+ * Finds a CLI shim in a directory's node_modules/.bin.
19
+ * @param {string} dir
20
+ * @param {string} name
21
+ * @returns {Promise<string | null>}
20
22
  */
21
- async function findTsgo(cwd) {
22
- const workspaceDir = await findWorkspaceDir(cwd);
23
- if (!workspaceDir) {
24
- return null;
25
- }
26
- const tsgoPath = path.join(workspaceDir, 'node_modules', '.bin', 'tsgo');
27
- const exists = await fs.stat(tsgoPath).then(
23
+ async function findBin(dir, name) {
24
+ const binPath = path.join(dir, 'node_modules', '.bin', name);
25
+ const exists = await fs.stat(binPath).then(
28
26
  (stat) => stat.isFile(),
29
27
  () => false,
30
28
  );
31
- return exists ? tsgoPath : null;
29
+ return exists ? binPath : null;
30
+ }
31
+
32
+ /**
33
+ * Resolves the native/JS TypeScript CLI pair from the workspace root's
34
+ * `node_modules/.bin`: tsgo/tsc when the legacy `tsgo` shim exists, tsc/tsc6
35
+ * otherwise, with bare `tsc` from PATH as the JS fallback.
36
+ *
37
+ * @param {string} cwd
38
+ * @returns {Promise<{ native: string | null, js: string }>}
39
+ */
40
+ async function findTscPair(cwd) {
41
+ const workspaceDir = await findWorkspaceDir(cwd);
42
+ if (!workspaceDir) {
43
+ return { native: null, js: 'tsc' };
44
+ }
45
+
46
+ const [tsgo, tsc, tsc6] = await Promise.all([
47
+ findBin(workspaceDir, 'tsgo'),
48
+ findBin(workspaceDir, 'tsc'),
49
+ findBin(workspaceDir, 'tsc6'),
50
+ ]);
51
+ if (tsgo) {
52
+ return { native: tsgo, js: 'tsc' };
53
+ }
54
+ return { native: tsc, js: tsc6 ?? 'tsc' };
32
55
  }
33
56
 
34
57
  /**
@@ -36,44 +59,38 @@ async function findTsgo(cwd) {
36
59
  * @param {string} tsconfig - The path to the tsconfig.json file.
37
60
  * @param {string} outDir - The output directory for the declaration files.
38
61
  * @param {Object} options
39
- * @param {boolean} [options.useTsgo] - Whether to use typescript native (tsgo).
62
+ * @param {boolean} [options.useTsgo] - Whether to use the native TypeScript compiler.
40
63
  */
41
64
  export async function emitDeclarations(tsconfig, outDir, options) {
42
65
  const { useTsgo = false } = options ?? {};
43
66
  const tsconfigDir = path.dirname(tsconfig);
44
67
  const rootDir = path.resolve(tsconfigDir, './src');
45
68
 
46
- const tsgoPath = useTsgo ? await findTsgo(tsconfigDir) : null;
69
+ const pair = await findTscPair(tsconfigDir);
70
+ const tsgoPath = useTsgo ? pair.native : null;
47
71
  if (useTsgo && !tsgoPath) {
48
72
  throw new Error(
49
- '--tsgo flag was passed or MUI_USE_TSGO environment was set but no tsgo cli was found. Either remove the flag to use tsc or install the native package "@typescript/native-preview" at the workspace level to use tsgo.',
73
+ '--tsgo flag was passed but no native TypeScript cli was found. Either remove the flag to use the JS compiler or install TypeScript 7 at the workspace root, as "typescript@^7" or under an alias such as "@typescript/native".',
50
74
  );
51
75
  }
52
76
 
53
- if (tsgoPath) {
54
- console.log('Using tsgo for declaration emit');
55
- await $$`${tsgoPath}
56
- -p ${tsconfig}
57
- --rootDir ${rootDir}
58
- --outDir ${outDir}
59
- --declaration
60
- --emitDeclarationOnly
61
- --noEmit false
62
- --composite false
63
- --incremental false
64
- --declarationMap false`;
65
- } else {
66
- await $$`tsc
67
- -p ${tsconfig}
68
- --rootDir ${rootDir}
69
- --outDir ${outDir}
70
- --declaration
71
- --emitDeclarationOnly
72
- --noEmit false
73
- --composite false
74
- --incremental false
75
- --declarationMap false`;
76
- }
77
+ const tscPath = tsgoPath ?? pair.js;
78
+ console.log(
79
+ tsgoPath
80
+ ? `Using ts-native for declaration emit: ${tscPath}`
81
+ : `Using ${tscPath} for declaration emit`,
82
+ );
83
+
84
+ await $$`${tscPath}
85
+ -p ${tsconfig}
86
+ --rootDir ${rootDir}
87
+ --outDir ${outDir}
88
+ --declaration
89
+ --emitDeclarationOnly
90
+ --noEmit false
91
+ --composite false
92
+ --incremental false
93
+ --declarationMap false`;
77
94
  }
78
95
 
79
96
  /**
@@ -129,7 +146,7 @@ export async function moveAndTransformDeclarations({ inputDir, buildDir, bundles
129
146
  return;
130
147
  }
131
148
 
132
- await mapConcurrently(
149
+ await mapAsync(
133
150
  dtsFiles,
134
151
  async (dtsFile) => {
135
152
  // Normalize to native separators to make path comparisons reliable on Windows
@@ -160,6 +177,7 @@ export async function moveAndTransformDeclarations({ inputDir, buildDir, bundles
160
177
  const outFileRelative = relativePath.replace(/\.d\.ts$/, newFileExtension);
161
178
  const outFilePath = path.join(buildDir, outFileRelative);
162
179
 
180
+ /** @type {import('@babel/core').PluginItem[]} */
163
181
  const babelPlugins = [
164
182
  [pluginTypescriptSyntax, { dts: true }],
165
183
  [pluginResolveImports, { outExtension: importExtension }],
@@ -182,7 +200,7 @@ export async function moveAndTransformDeclarations({ inputDir, buildDir, bundles
182
200
  await fs.unlink(nativeDtsFile);
183
201
  }
184
202
  },
185
- 30,
203
+ { concurrency: 30 },
186
204
  );
187
205
  }
188
206
 
@@ -198,7 +216,7 @@ export async function moveAndTransformDeclarations({ inputDir, buildDir, bundles
198
216
  * @param {string} param0.buildDir - The build directory.
199
217
  * @param {string} param0.cwd - The current working directory.
200
218
  * @param {boolean} param0.skipTsc - Whether to skip running TypeScript compiler (tsc) for building types.
201
- * @param {boolean} [param0.useTsgo=false] - Whether to build types using typescript native (tsgo).
219
+ * @param {boolean} [param0.useTsgo=false] - Whether to build types using the native TypeScript compiler.
202
220
  * @param {'module' | 'commonjs'} [param0.packageType] - The package.json type field.
203
221
  */
204
222
  export async function createTypes({