@mui/internal-code-infra 0.0.4-canary.98 → 0.1.1-canary.0
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 +4 -0
- package/build/babel-config.d.mts +4 -4
- package/build/babelPluginInlineEnvironmentVariables.d.mts +11 -0
- package/build/brokenLinksChecker/index.d.mts +147 -0
- package/build/changelog/fetchChangelogs.d.mts +3 -0
- package/build/cli/cmdArgosPush.d.mts +6 -0
- package/build/cli/cmdBuild.d.mts +45 -0
- package/build/cli/cmdCopyFiles.d.mts +12 -0
- package/build/cli/cmdGenerateChangelog.d.mts +15 -0
- package/build/cli/cmdListWorkspaces.d.mts +14 -2
- package/build/cli/cmdNetlifyIgnore.d.mts +9 -0
- package/build/cli/cmdPublish.d.mts +20 -2
- package/build/cli/cmdPublishCanary.d.mts +18 -0
- package/build/cli/cmdPublishNewPackage.d.mts +6 -0
- package/build/cli/cmdRenameScope.d.mts +18 -0
- package/build/cli/cmdSetVersionOverrides.d.mts +3 -0
- package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +6 -0
- package/build/eslint/testConfig.d.mts +1 -7
- package/build/utils/babel.d.mts +6 -0
- package/build/utils/build.d.mts +3 -15
- package/build/utils/extractErrorCodes.d.mts +16 -0
- package/build/utils/git.d.mts +9 -0
- package/build/utils/github.d.mts +12 -0
- package/build/utils/pnpm.d.mts +147 -9
- package/build/utils/testUtils.d.mts +21 -0
- package/build/utils/typescript.d.mts +2 -2
- package/package.json +54 -56
- package/src/babel-config.mjs +21 -11
- package/src/babelPluginInlineEnvironmentVariables.mjs +45 -0
- package/src/brokenLinksChecker/index.mjs +3 -3
- package/src/cli/cmdArgosPush.mjs +2 -4
- package/src/cli/cmdBuild.mjs +8 -9
- package/src/cli/cmdCopyFiles.mjs +3 -3
- package/src/cli/cmdListWorkspaces.mjs +3 -31
- package/src/cli/cmdPublish.mjs +23 -29
- package/src/cli/cmdRenameScope.mjs +77 -0
- package/src/cli/cmdSetVersionOverrides.mjs +32 -25
- package/src/cli/cmdVale.mjs +3 -6
- package/src/cli/cmdValidateBuiltTypes.mjs +3 -4
- package/src/cli/index.mjs +2 -0
- package/src/eslint/testConfig.mjs +3 -31
- package/src/untyped-plugins.d.ts +11 -49
- package/src/utils/build.mjs +0 -39
- package/src/utils/extractErrorCodes.mjs +12 -7
- package/src/utils/pnpm.mjs +380 -16
- package/src/utils/testUtils.mjs +35 -0
- package/src/utils/typescript.mjs +60 -42
package/src/untyped-plugins.d.ts
CHANGED
|
@@ -95,73 +95,35 @@ declare module '@next/eslint-plugin-next' {
|
|
|
95
95
|
export default config;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
declare module '@babel/plugin-transform-runtime' {
|
|
99
|
-
import type { PluginItem } from '@babel/core';
|
|
100
|
-
|
|
101
|
-
declare const plugin: PluginItem;
|
|
102
|
-
export default plugin;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
declare module '@babel/plugin-syntax-jsx' {
|
|
106
|
-
import type { PluginItem } from '@babel/core';
|
|
107
|
-
|
|
108
|
-
declare const plugin: PluginItem;
|
|
109
|
-
export default plugin;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
declare module '@babel/plugin-syntax-typescript' {
|
|
113
|
-
import type { PluginItem } from '@babel/core';
|
|
114
|
-
|
|
115
|
-
declare const plugin: PluginItem;
|
|
116
|
-
export default plugin;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
98
|
declare module 'babel-plugin-optimize-clsx' {
|
|
120
|
-
import type {
|
|
99
|
+
import type { PluginTarget } from '@babel/core';
|
|
121
100
|
|
|
122
|
-
declare const plugin:
|
|
101
|
+
declare const plugin: PluginTarget;
|
|
123
102
|
export default plugin;
|
|
124
103
|
}
|
|
125
104
|
|
|
126
105
|
declare module 'babel-plugin-transform-react-remove-prop-types' {
|
|
127
|
-
import type {
|
|
106
|
+
import type { PluginTarget } from '@babel/core';
|
|
128
107
|
|
|
129
|
-
declare const plugin:
|
|
108
|
+
declare const plugin: PluginTarget;
|
|
130
109
|
export default plugin;
|
|
131
110
|
}
|
|
132
111
|
|
|
133
|
-
declare module 'babel-plugin-transform-
|
|
134
|
-
|
|
112
|
+
declare module 'babel-plugin-transform-remove-imports' {
|
|
113
|
+
// The package's own bundled types mislabel the export's signature.
|
|
114
|
+
import type { PluginTarget } from '@babel/core';
|
|
135
115
|
|
|
136
|
-
declare const plugin:
|
|
116
|
+
declare const plugin: PluginTarget;
|
|
137
117
|
export default plugin;
|
|
138
118
|
}
|
|
139
119
|
|
|
140
|
-
declare module 'babel-plugin-transform-
|
|
141
|
-
import type {
|
|
120
|
+
declare module 'babel-plugin-transform-import-meta' {
|
|
121
|
+
import type { PluginTarget } from '@babel/core';
|
|
142
122
|
|
|
143
|
-
declare const plugin:
|
|
123
|
+
declare const plugin: PluginTarget;
|
|
144
124
|
export default plugin;
|
|
145
125
|
}
|
|
146
126
|
|
|
147
|
-
declare module '@babel/preset-react' {
|
|
148
|
-
import type { PluginItem } from '@babel/core';
|
|
149
|
-
|
|
150
|
-
export type Options = {
|
|
151
|
-
runtime: 'string';
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
declare const preset: PluginItem;
|
|
155
|
-
export default preset;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
declare module '@babel/preset-typescript' {
|
|
159
|
-
import type { PluginItem } from '@babel/core';
|
|
160
|
-
|
|
161
|
-
declare const preset: PluginItem;
|
|
162
|
-
export default preset;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
127
|
declare module 'stylelint-config-standard' {
|
|
166
128
|
import type { Config } from 'stylelint';
|
|
167
129
|
|
package/src/utils/build.mjs
CHANGED
|
@@ -894,42 +894,3 @@ export function measureFn(label) {
|
|
|
894
894
|
const endMark = `${label}-end`;
|
|
895
895
|
return performance.measure(label, startMark, endMark);
|
|
896
896
|
}
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* A utility to map a function over an array of items in a worker pool.
|
|
900
|
-
*
|
|
901
|
-
* This function will create a pool of workers and distribute the items to be processed among them.
|
|
902
|
-
* Each worker will process items sequentially, but multiple workers will run in parallel.
|
|
903
|
-
*
|
|
904
|
-
* @function
|
|
905
|
-
* @template T
|
|
906
|
-
* @template R
|
|
907
|
-
* @param {T[]} items
|
|
908
|
-
* @param {(item: T) => Promise<R>} mapper
|
|
909
|
-
* @param {number} concurrency
|
|
910
|
-
* @returns {Promise<(R|Error)[]>}
|
|
911
|
-
*/
|
|
912
|
-
export async function mapConcurrently(items, mapper, concurrency) {
|
|
913
|
-
if (!items.length) {
|
|
914
|
-
return Promise.resolve([]); // nothing to do
|
|
915
|
-
}
|
|
916
|
-
const itemIterator = items.entries();
|
|
917
|
-
const count = Math.min(concurrency, items.length);
|
|
918
|
-
const workers = [];
|
|
919
|
-
/**
|
|
920
|
-
* @type {(R|Error)[]}
|
|
921
|
-
*/
|
|
922
|
-
const results = new Array(items.length);
|
|
923
|
-
for (let i = 0; i < count; i += 1) {
|
|
924
|
-
const worker = Promise.resolve().then(async () => {
|
|
925
|
-
for (const [index, item] of itemIterator) {
|
|
926
|
-
// eslint-disable-next-line no-await-in-loop
|
|
927
|
-
const res = await mapper(item);
|
|
928
|
-
results[index] = res;
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
workers.push(worker);
|
|
932
|
-
}
|
|
933
|
-
await Promise.all(workers);
|
|
934
|
-
return results;
|
|
935
|
-
}
|
|
@@ -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
|
|
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
|
|
55
|
-
await
|
|
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:
|
|
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
|
|
116
|
+
await extractErrorCodesForFiles(filesToProcess, errors, detection);
|
|
112
117
|
|
|
113
118
|
// Partition collected entries into valid messages and unminifyable errors.
|
|
114
119
|
/** @type {string[]} */
|
package/src/utils/pnpm.mjs
CHANGED
|
@@ -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
|
|
@@ -303,11 +320,29 @@ export async function publishPackages(packages, options = {}) {
|
|
|
303
320
|
* @property {boolean} [includeDev=true] - Whether to include devDependencies in the traversal
|
|
304
321
|
*/
|
|
305
322
|
|
|
323
|
+
/**
|
|
324
|
+
* The package a `workspace:` alias spec resolves to, if it is one.
|
|
325
|
+
*
|
|
326
|
+
* @param {string} spec - Dependency spec
|
|
327
|
+
* @returns {string | null} The aliased package name, or null for a plain spec
|
|
328
|
+
*/
|
|
329
|
+
function aliasTarget(spec) {
|
|
330
|
+
if (!spec.startsWith('workspace:')) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
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);
|
|
337
|
+
}
|
|
338
|
+
|
|
306
339
|
/**
|
|
307
340
|
* Get all transitive workspace dependencies for a set of packages.
|
|
308
341
|
*
|
|
309
342
|
* Only follows deps whose version spec starts with `workspace:` (e.g. `workspace:*`
|
|
310
|
-
* or `workspace:^`), meaning they are sourced directly from the monorepo.
|
|
343
|
+
* or `workspace:^`), meaning they are sourced directly from the monorepo. An
|
|
344
|
+
* alias spec (`workspace:@scope/name@range`) is followed to the package it
|
|
345
|
+
* targets, which need not match the dependency key. Pinned
|
|
311
346
|
* external versions (e.g. `^1.0.0`) are ignored even when the package name exists
|
|
312
347
|
* in the workspace. Traverses `dependencies` and optionally `devDependencies`.
|
|
313
348
|
* Results are cached per package so each package is read from disk at most once
|
|
@@ -344,14 +379,15 @@ export async function getTransitiveDependencies(packageNames, options = {}) {
|
|
|
344
379
|
...Object.entries(pkgJson.dependencies ?? {}),
|
|
345
380
|
...(includeDev ? Object.entries(pkgJson.devDependencies ?? {}) : []),
|
|
346
381
|
];
|
|
347
|
-
const workspaceDeps = allDepEntries
|
|
348
|
-
.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
.
|
|
382
|
+
const workspaceDeps = allDepEntries.flatMap(([dep, spec]) => {
|
|
383
|
+
if (typeof spec !== 'string' || !spec.startsWith('workspace:')) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
// An aliased spec (`workspace:@scope/name@range`) names the workspace
|
|
387
|
+
// package it resolves to, which need not match the dependency key.
|
|
388
|
+
const name = aliasTarget(spec) ?? dep;
|
|
389
|
+
return workspacePathByName.has(name) ? [name] : [];
|
|
390
|
+
});
|
|
355
391
|
|
|
356
392
|
const recursiveResults = await Promise.all(workspaceDeps.map(collectDeps));
|
|
357
393
|
return new Set([...workspaceDeps, ...recursiveResults.flatMap((s) => [...s])]);
|
|
@@ -541,26 +577,354 @@ export async function writeOverridesToWorkspace(workspaceDir, overrides) {
|
|
|
541
577
|
}
|
|
542
578
|
|
|
543
579
|
/**
|
|
544
|
-
*
|
|
580
|
+
* Map a package name onto a different npm scope.
|
|
581
|
+
* @param {string} name - Package name, e.g. `@base-ui/mosaic`
|
|
582
|
+
* @param {string} fromScope - Scope to replace, e.g. `@base-ui`
|
|
583
|
+
* @param {string} toScope - Replacement scope, e.g. `@base-ui-private`
|
|
584
|
+
* @returns {string | null} The renamed package, or null when the scope doesn't match
|
|
585
|
+
*/
|
|
586
|
+
function renameScope(name, fromScope, toScope) {
|
|
587
|
+
const prefix = `${fromScope}/`;
|
|
588
|
+
return name.startsWith(prefix) ? `${toScope}/${name.slice(prefix.length)}` : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Move the publishable workspace packages in one scope to another.
|
|
593
|
+
*
|
|
594
|
+
* Only packages that are part of the workspace are touched, so dependencies
|
|
595
|
+
* that merely share the scope but come from the registry (say `@base-ui/react`
|
|
596
|
+
* alongside a workspace `@base-ui/mosaic`) are left alone. Dependents keep the
|
|
597
|
+
* original dependency name and gain a `workspace:` alias, so imports in the
|
|
598
|
+
* repo resolve exactly as before.
|
|
599
|
+
*
|
|
600
|
+
* @param {(PublicPackage | PrivatePackage)[]} packages - All workspace packages
|
|
601
|
+
* @param {string} fromScope - Scope to move away from
|
|
602
|
+
* @param {string} toScope - Scope to move to
|
|
603
|
+
* @returns {Promise<Map<string, string>>} Old package name to new package name
|
|
604
|
+
*/
|
|
605
|
+
export async function renameWorkspaceScope(packages, fromScope, toScope) {
|
|
606
|
+
// Renaming a scope onto itself only rewrites every manifest to itself. The
|
|
607
|
+
// CLI rejects this earlier, but the guard belongs with the operation so a
|
|
608
|
+
// direct caller cannot trip the silent churn either.
|
|
609
|
+
if (fromScope === toScope) {
|
|
610
|
+
throw new Error(`Cannot rename ${fromScope} to itself.`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** @type {Map<string, string>} */
|
|
614
|
+
const renamed = new Map();
|
|
615
|
+
|
|
616
|
+
for (const pkg of packages) {
|
|
617
|
+
if (pkg.isPrivate) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const newName = renameScope(pkg.name, fromScope, toScope);
|
|
621
|
+
if (newName) {
|
|
622
|
+
renamed.set(pkg.name, newName);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// A rename that lands on a name another package keeps would leave the
|
|
627
|
+
// workspace with two packages sharing a name. Catch it up front, before any
|
|
628
|
+
// manifest is written.
|
|
629
|
+
/** @type {Map<string, string>} */
|
|
630
|
+
const finalNames = new Map();
|
|
631
|
+
for (const pkg of packages) {
|
|
632
|
+
if (!pkg.name) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const finalName = renamed.get(pkg.name) ?? pkg.name;
|
|
636
|
+
const owner = finalNames.get(finalName);
|
|
637
|
+
if (owner) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`Cannot rename ${fromScope} to ${toScope}: ${pkg.name} and ${owner} would both be named ${finalName}.`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
finalNames.set(finalName, pkg.name);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Rewrite in memory first. A dependency that cannot be pointed at its renamed
|
|
646
|
+
// package has to fail before anything is written, or the workspace is left
|
|
647
|
+
// half renamed with nothing to restore it.
|
|
648
|
+
const rewritten = await Promise.all(
|
|
649
|
+
packages.map(async (pkg) => {
|
|
650
|
+
const packageJson = await readPackageJson(pkg.path);
|
|
651
|
+
const label = pkg.name ?? pkg.path;
|
|
652
|
+
/** @type {string[]} */
|
|
653
|
+
const problems = [];
|
|
654
|
+
let changed = false;
|
|
655
|
+
|
|
656
|
+
const newName = pkg.name ? renamed.get(pkg.name) : undefined;
|
|
657
|
+
if (newName) {
|
|
658
|
+
packageJson.name = newName;
|
|
659
|
+
changed = true;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// peerDependencies are deliberately absent: a peer is supplied by the
|
|
663
|
+
// consumer, who installs the package under its original name. An alias
|
|
664
|
+
// range would be unsatisfiable for them.
|
|
665
|
+
for (const deps of [
|
|
666
|
+
packageJson.dependencies,
|
|
667
|
+
packageJson.devDependencies,
|
|
668
|
+
packageJson.optionalDependencies,
|
|
669
|
+
]) {
|
|
670
|
+
if (!deps) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
for (const [depName, spec] of Object.entries(deps)) {
|
|
674
|
+
if (!spec) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const existingTarget = aliasTarget(spec);
|
|
679
|
+
if (existingTarget) {
|
|
680
|
+
if (renamed.has(existingTarget)) {
|
|
681
|
+
problems.push(
|
|
682
|
+
`"${depName}" in ${label} already aliases ${existingTarget}, which is being renamed. Point it at the package directly so it can be rewritten.`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
// Otherwise it already aliases what it should, as a re-run does.
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const target = renamed.get(depName);
|
|
690
|
+
if (!target) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (!spec.startsWith('workspace:')) {
|
|
694
|
+
// Only `workspace:` specs can be aliased. Anything else would keep
|
|
695
|
+
// resolving the original name from the registry after the rename.
|
|
696
|
+
problems.push(
|
|
697
|
+
`"${depName}" in ${label} is required as "${spec}" rather than a workspace: dependency, so it cannot be pointed at ${target}.`,
|
|
698
|
+
);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
deps[depName] = `workspace:${target}@${spec.slice('workspace:'.length)}`;
|
|
702
|
+
changed = true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return { path: pkg.path, packageJson, changed, problems };
|
|
707
|
+
}),
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const problems = rewritten.flatMap((entry) => entry.problems);
|
|
711
|
+
if (problems.length > 0) {
|
|
712
|
+
throw new Error(`Cannot rename ${fromScope} to ${toScope}:\n ${problems.join('\n ')}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
await Promise.all(
|
|
716
|
+
rewritten
|
|
717
|
+
.filter((entry) => entry.changed)
|
|
718
|
+
.map((entry) => writePackageJson(entry.path, entry.packageJson)),
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return renamed;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
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
|
+
*
|
|
545
862
|
* @param {string} packageSpec - Package specifier in format "package@version"
|
|
863
|
+
* @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Cooldown from {@link getMinimumReleaseAgePolicy}
|
|
546
864
|
* @returns {Promise<string>} Exact version string
|
|
547
865
|
*/
|
|
548
|
-
export async function resolveVersion(packageSpec) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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;
|
|
552
911
|
}
|
|
553
912
|
|
|
554
913
|
/**
|
|
555
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
|
+
*
|
|
556
919
|
* @param {string} packageSpec - Package specifier in format "package@version"
|
|
557
920
|
* @param {string} dependency - Dependency name to look up
|
|
921
|
+
* @param {import('@pnpm/config.version-policy').PublishedByPolicy} policy - Registry cooldown to resolve within
|
|
558
922
|
* @returns {Promise<string>} Exact version string of the dependency
|
|
559
923
|
*/
|
|
560
|
-
export async function findDependencyVersionFromSpec(packageSpec, dependency) {
|
|
924
|
+
export async function findDependencyVersionFromSpec(packageSpec, dependency, policy) {
|
|
561
925
|
const result = await $`pnpm info ${packageSpec} dependencies.${dependency}`;
|
|
562
926
|
const spec = result.stdout.trim();
|
|
563
|
-
return resolveVersion(`${dependency}@${spec}
|
|
927
|
+
return resolveVersion(`${dependency}@${spec}`, policy);
|
|
564
928
|
}
|
|
565
929
|
|
|
566
930
|
/**
|
package/src/utils/testUtils.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import * as os from 'node:os';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { onTestFinished } from 'vitest';
|
|
5
5
|
|
|
6
|
+
import { writePackageJson } from './pnpm.mjs';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Creates a temporary directory and registers an `onTestFinished` hook to
|
|
8
10
|
* remove it automatically when the current test ends — even if the test throws.
|
|
@@ -16,3 +18,36 @@ export async function makeTempDir() {
|
|
|
16
18
|
});
|
|
17
19
|
return tmpDir;
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write a package.json into a subdirectory of a temp workspace.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} root - Workspace root
|
|
26
|
+
* @param {string} dir - Subdirectory to create
|
|
27
|
+
* @param {object} pkgJson - Manifest contents
|
|
28
|
+
* @returns {Promise<string>} The package directory
|
|
29
|
+
*/
|
|
30
|
+
export async function writePackage(root, dir, pkgJson) {
|
|
31
|
+
const pkgDir = path.join(root, dir);
|
|
32
|
+
await fs.mkdir(pkgDir, { recursive: true });
|
|
33
|
+
await writePackageJson(pkgDir, pkgJson);
|
|
34
|
+
return pkgDir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} name
|
|
39
|
+
* @param {string} pkgPath
|
|
40
|
+
* @returns {import('./pnpm.mjs').PublicPackage}
|
|
41
|
+
*/
|
|
42
|
+
export function publicPkg(name, pkgPath) {
|
|
43
|
+
return { name, version: '1.0.0', path: pkgPath, isPrivate: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @param {string} pkgPath
|
|
49
|
+
* @returns {import('./pnpm.mjs').PrivatePackage}
|
|
50
|
+
*/
|
|
51
|
+
export function privatePkg(name, pkgPath) {
|
|
52
|
+
return { name, version: '1.0.0', path: pkgPath, isPrivate: true };
|
|
53
|
+
}
|