Merge remote-tracking branch 'origin/main' into claude/speed-up-pnpm-compile-UZl1m

This commit is contained in:
Claude
2026-03-21 23:17:13 +00:00
14 changed files with 135 additions and 49 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/installing.deps-resolver": patch
pnpm: patch
---
Improved handling of non-string version selectors in an internal function (e.g. `hoistPeers`).

View File

@@ -0,0 +1,5 @@
---
"@pnpm/core": patch
---
The `resolutionMode` option for `mutateModules` now defaults to `highest` to match the default in `@pnpm/config`. It previously defaulted to `lowest-direct`.

2
.gitignore vendored
View File

@@ -48,6 +48,8 @@ RELEASE.md
.turbo
.eslintcache
.claude
.local-settings
.pnpm-typecheck.json
## custom modules-dir fixture
__fixtures__/custom-modules-dir/**/fake_modules/

View File

@@ -1,5 +1,6 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { findWorkspaceProjects } from '@pnpm/workspace.projects-reader'
@@ -54,8 +55,14 @@ async function main (): Promise<void> {
JSON.stringify(typeCheckTSConfig, undefined, 2)
)
console.log('Running tsgo --build...')
execa('tsgo', ['--build', '--singleThreaded', typeCheckDir], {
const singleThreaded = resolveThreadingMode(repoRoot)
const args = ['--build']
if (singleThreaded) {
args.push('--singleThreaded')
}
args.push(typeCheckDir)
console.log(`Running tsgo --build${singleThreaded ? ' --singleThreaded' : ''}...`)
execa('tsgo', args, {
// The INIT_CWD variable is populated by package managers and points towards
// the user's original working directory. It's more useful to run TypeScript
// from the user's actual working directory so any type checking errors can
@@ -72,6 +79,47 @@ async function main (): Promise<void> {
console.log('Running tsgo build done')
}
const AUTO_SINGLE_THREAD_MEMORY_THRESHOLD_GB = 8
function resolveThreadingMode (repoRoot: string): boolean {
const { mode, source } = readThreadingMode(repoRoot)
switch (mode) {
case 'single-threaded':
return true
case 'multi-threaded':
return false
case 'auto':
return os.totalmem() / (1024 ** 3) < AUTO_SINGLE_THREAD_MEMORY_THRESHOLD_GB
default:
throw new Error(
`Invalid threading mode "${mode}" from ${source}. ` +
'Valid values: auto, single-threaded, multi-threaded.'
)
}
}
function readThreadingMode (repoRoot: string): { mode: string, source: string } {
const envValue = process.env.PNPM_TYPECHECK_THREADING?.trim().toLowerCase()
if (envValue) {
return { mode: envValue, source: 'PNPM_TYPECHECK_THREADING env var' }
}
for (const configPath of [
path.join(repoRoot, '.local-settings', 'pnpm-typecheck.json'),
path.join(repoRoot, '.pnpm-typecheck.json'),
]) {
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
const threading = typeof config.threading === 'string' ? config.threading.trim().toLowerCase() : ''
if (threading) {
return { mode: threading, source: configPath }
}
}
}
return { mode: 'auto', source: 'default' }
}
main().catch((error: unknown) => {
if (error && typeof error === 'object' && 'exitCode' in error && 'shortMessage' in error) {
process.exit(error.exitCode as number)

View File

@@ -69,15 +69,17 @@ if (/^\d+$/.test(arg)) {
}
}
// Symlink .claude into the new worktree, pointing at the bare repo's git common
// dir so all worktrees share the same Claude Code settings and approved commands.
// Symlink shared directories into the new worktree, pointing at the bare repo's
// git common dir so all worktrees share the same settings without duplication.
const gitCommonDir = execSync('git rev-parse --git-common-dir', { encoding: 'utf8', cwd: repoRoot }).trim()
const sharedClaudeDir = path.resolve(repoRoot, gitCommonDir, '.claude')
const newClaudeDir = path.join(worktreePath, '.claude')
fs.mkdirSync(sharedClaudeDir, { recursive: true })
if (!fs.existsSync(newClaudeDir)) {
// 'junction' works without elevated privileges on Windows; ignored on Unix
fs.symlinkSync(sharedClaudeDir, newClaudeDir, 'junction')
for (const dir of ['.claude', '.local-settings']) {
const sharedDir = path.resolve(repoRoot, gitCommonDir, dir)
const newDir = path.join(worktreePath, dir)
fs.mkdirSync(sharedDir, { recursive: true })
if (!fs.existsSync(newDir)) {
// 'junction' works without elevated privileges on Windows; ignored on Unix
fs.symlinkSync(sharedDir, newDir, 'junction')
}
}
// Print path to stdout — allows: cd $(pnpm worktree:new <arg>)

View File

@@ -165,6 +165,12 @@ export async function installDeps (
ignoreFilteredInstallCache: true,
})
if (upToDate) {
if (opts.hooks?.customResolvers?.some(r => r.shouldRefreshResolution)) {
logger.warn({
message: 'shouldRefreshResolution hooks were skipped because optimisticRepeatInstall is enabled.',
prefix: opts.dir,
})
}
globalInfo('Already up to date')
return
}

View File

@@ -243,7 +243,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => {
pruneStore: false,
rawConfig: {},
registries: DEFAULT_REGISTRIES,
resolutionMode: 'lowest-direct',
resolutionMode: 'highest',
saveWorkspaceProtocol: 'rolling',
scriptsPrependNodePath: false,
shamefullyHoist: false,

View File

@@ -178,13 +178,13 @@ test('lockfile contains catalog snapshots', async () => {
{
name: 'project1',
dependencies: {
'is-positive': 'catalog:',
'@pnpm.e2e/bar': 'catalog:',
},
},
{
name: 'project2',
dependencies: {
'is-negative': 'catalog:',
'@pnpm.e2e/foo': 'catalog:',
},
},
])
@@ -194,8 +194,8 @@ test('lockfile contains catalog snapshots', async () => {
lockfileOnly: true,
catalogs: {
default: {
'is-positive': '^1.0.0',
'is-negative': '^1.0.0',
'@pnpm.e2e/bar': '^100.0.0',
'@pnpm.e2e/foo': '^100.0.0',
},
},
})
@@ -203,8 +203,8 @@ test('lockfile contains catalog snapshots', async () => {
const lockfile = readLockfile()
expect(lockfile.catalogs).toStrictEqual({
default: {
'is-positive': { specifier: '^1.0.0', version: '1.0.0' },
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
'@pnpm.e2e/bar': { specifier: '^100.0.0', version: '100.1.0' },
'@pnpm.e2e/foo': { specifier: '^100.0.0', version: '100.1.0' },
},
})
})
@@ -306,13 +306,13 @@ test('lockfile catalog snapshots retain existing entries on --filter', async ()
{
name: 'project1',
dependencies: {
'is-negative': 'catalog:',
'@pnpm.e2e/bar': 'catalog:',
},
},
{
name: 'project2',
dependencies: {
'is-positive': 'catalog:',
'@pnpm.e2e/foo': 'catalog:',
},
},
])
@@ -322,16 +322,16 @@ test('lockfile catalog snapshots retain existing entries on --filter', async ()
lockfileOnly: true,
catalogs: {
default: {
'is-positive': '^1.0.0',
'is-negative': '^1.0.0',
'@pnpm.e2e/bar': '^100.0.0',
'@pnpm.e2e/foo': '^1.0.0',
},
},
})
expect(readLockfile().catalogs).toStrictEqual({
default: {
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
'is-positive': { specifier: '^1.0.0', version: '1.0.0' },
'@pnpm.e2e/bar': { specifier: '^100.0.0', version: '100.1.0' },
'@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.3.0' },
},
})
@@ -341,19 +341,19 @@ test('lockfile catalog snapshots retain existing entries on --filter', async ()
lockfileOnly: true,
catalogs: {
default: {
'is-positive': '=3.1.0',
'is-negative': '^1.0.0',
'@pnpm.e2e/bar': '^100.0.0',
'@pnpm.e2e/foo': '=100.0.0',
},
},
})
expect(readLockfile().catalogs).toStrictEqual({
default: {
// The is-negative snapshot should be carried from the previous install,
// The @pnpm.e2e/bar snapshot should be carried from the previous install,
// despite the current filtered install not using it.
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
'@pnpm.e2e/bar': { specifier: '^100.0.0', version: '100.1.0' },
'is-positive': { specifier: '=3.1.0', version: '3.1.0' },
'@pnpm.e2e/foo': { specifier: '=100.0.0', version: '100.0.0' },
},
})
})
@@ -513,28 +513,28 @@ test('--fix-lockfile with --filter does not erase catalog snapshots', async () =
{
name: 'project1',
dependencies: {
'is-negative': 'catalog:',
'@pnpm.e2e/bar': 'catalog:',
},
},
{
name: 'project2',
dependencies: {
'is-positive': 'catalog:',
'@pnpm.e2e/foo': 'catalog:',
},
},
])
const catalogs = {
default: {
'is-positive': '^1.0.0',
'is-negative': '^1.0.0',
'@pnpm.e2e/bar': '^100.0.0',
'@pnpm.e2e/foo': '^100.0.0',
},
}
const expectedCatalogsSnapshot: CatalogSnapshots = {
default: {
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
'is-positive': { specifier: '^1.0.0', version: '1.0.0' },
'@pnpm.e2e/bar': { specifier: '^100.0.0', version: '100.1.0' },
'@pnpm.e2e/foo': { specifier: '^100.0.0', version: '100.1.0' },
},
}
@@ -1210,7 +1210,7 @@ describe('add', () => {
])
const catalogs = {
default: { '@pnpm.e2e/foo': '^100.0.0' },
default: { '@pnpm.e2e/foo': '^100.1.0' },
}
await mutateModules(installProjects(projects), {
@@ -1238,12 +1238,12 @@ describe('add', () => {
// Sanity check that the rest of the lockfile has expected contents.
expect(readLockfile()).toMatchObject({
catalogs: { default: { '@pnpm.e2e/foo': { specifier: '^100.0.0', version: '100.0.0' } } },
catalogs: { default: { '@pnpm.e2e/foo': { specifier: '^100.1.0', version: '100.1.0' } } },
importers: {
project1: { dependencies: { '@pnpm.e2e/foo': { specifier: 'catalog:', version: '100.0.0' } } },
project2: { dependencies: { '@pnpm.e2e/foo': { specifier: 'catalog:', version: '100.0.0' } } },
project1: { dependencies: { '@pnpm.e2e/foo': { specifier: 'catalog:', version: '100.1.0' } } },
project2: { dependencies: { '@pnpm.e2e/foo': { specifier: 'catalog:', version: '100.1.0' } } },
},
packages: { '@pnpm.e2e/foo@100.0.0': expect.any(Object) },
packages: { '@pnpm.e2e/foo@100.1.0': expect.any(Object) },
})
})

View File

@@ -16,13 +16,13 @@ test('fix broken lockfile with --fix-lockfile', async () => {
writeYamlFileSync(WANTED_LOCKFILE, {
dependencies: {
'@types/semver': {
specifier: '^5.3.31',
specifier: '5.3.31',
version: '5.3.31',
},
},
devDependencies: {
fsevents: {
specifier: '^2.3.2',
specifier: '2.3.2',
version: '2.3.2',
},
},
@@ -44,10 +44,10 @@ test('fix broken lockfile with --fix-lockfile', async () => {
await install({
dependencies: {
'@types/semver': '^5.3.31',
'@types/semver': '5.3.31',
},
devDependencies: {
'core-js-pure': '^3.16.2',
'core-js-pure': '3.16.2',
},
}, testDefaults({ fixLockfile: true }))

View File

@@ -1802,7 +1802,7 @@ test('injected local packages are deduped', async () => {
'project-1': 'workspace:1.0.0',
},
devDependencies: {
'is-positive': '1.0.0',
'is-positive': '2.0.0',
},
dependenciesMeta: {
'project-1': {
@@ -1954,9 +1954,9 @@ test('injected local packages are deduped', async () => {
},
})
expect(lockfile.packages['project-1@file:project-1(is-positive@1.0.0)']).toBeFalsy()
expect(lockfile.snapshots['project-2@file:project-2(is-positive@2.0.0)']).toEqual({
expect(lockfile.snapshots['project-2@file:project-2(is-positive@1.0.0)']).toEqual({
dependencies: {
'project-1': 'file:project-1(is-positive@2.0.0)',
'project-1': 'file:project-1(is-positive@1.0.0)',
},
transitivePeerDependencies: ['is-positive'],
})

View File

@@ -1244,6 +1244,7 @@ packages:
test('a lockfile v6 with merge conflicts is autofixed', async () => {
const project = prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.1.0', distTag: 'latest' })
fs.writeFileSync(WANTED_LOCKFILE, `\
lockfileVersion: '${LOCKFILE_VERSION}'

View File

@@ -29,7 +29,8 @@ export function hoistPeers (
if (opts.allPreferredVersions![peerName]) {
const versions: string[] = []
const nonVersions: string[] = []
for (const [spec, specType] of Object.entries(opts.allPreferredVersions![peerName])) {
for (const [spec, selector] of Object.entries(opts.allPreferredVersions![peerName])) {
const specType = typeof selector === 'string' ? selector : selector.selectorType
if (specType === 'version') {
versions.push(spec)
} else {

View File

@@ -96,6 +96,21 @@ test('hoistPeers handles workspace: protocol range without throwing', () => {
})
})
// Regression test for https://github.com/pnpm/pnpm/pull/11048
test('hoistPeers handles version selector with weight', () => {
expect(hoistPeers({
autoInstallPeers: true,
allPreferredVersions: {
foo: {
'1.0.0': { selectorType: 'version', weight: 1 },
},
},
workspaceRootDeps: [],
}, [['foo', { range: '1' }]])).toStrictEqual({
foo: '1.0.0',
})
})
test('getHoistableOptionalPeers only picks a version that satisfies all optional ranges', () => {
expect(getHoistableOptionalPeers({
foo: ['2', '2.1'],

View File

@@ -899,7 +899,7 @@ test('deploy with a shared lockfile that has peer dependencies suffix in workspa
'project-2': 'workspace:*',
},
peerDependencies: {
'is-negative': '>=1.0.0',
'is-negative': '1.0.0',
'project-2': '*',
},
},
@@ -907,7 +907,7 @@ test('deploy with a shared lockfile that has peer dependencies suffix in workspa
name: 'project-2',
version: '0.0.0',
peerDependencies: {
'is-positive': '>=1.0.0',
'is-positive': '1.0.0',
},
},
}