mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-29 18:35:18 -04:00
feat(installing): delegate resolution to pacquet >= 0.11.7 when configured (#12210)
* feat(installing): delegate resolution to pacquet >= 0.11 when configured When pacquet is declared in configDependencies, pnpm previously always ran it with --frozen-lockfile (pnpm resolved, pacquet materialized). If the installed pacquet is >= 0.11 it ships its own resolver, so a non-frozen plain install on the default isolated linker is now delegated end-to-end: pacquet resolves, writes pnpm-lock.yaml, and materializes in a single pass. Older pacquet keeps the resolve-then-materialize split, and add/update/remove still resolve in pnpm. * test(pnpm): cover pacquet 0.11 resolution delegation end-to-end Bump the e2e pacquet pin to 0.11.0 (published under both pacquet and @pnpm/pacquet) and add tests for the resolve path, the materialize-only fallback (pinned to 0.2.14), and the scoped alias. Un-skip the add/update tests now that pacquet 0.11 writes a compatible .modules.yaml, and fix the update test (is-positive has no v4; update from 1.0.0 instead). Also preserve the configDependencies env document when pacquet resolves: pacquet rewrites pnpm-lock.yaml without the leading env YAML doc, which dropped configDependencies and broke the next --frozen-lockfile install. Capture it before delegating and restore it after. * fix(installing): restore configDependencies env document even when pacquet fails On the pacquet resolve-delegation path the configDependencies env document was only restored after a successful pacquet run. A non-zero exit could leave a rewritten pnpm-lock.yaml without it, breaking the next --frozen-lockfile install's config-deps freshness gate. Restore it in a finally block, swallowing (and warning on) any restore error so it cannot mask the original pacquet failure. * fix(installing): require pacquet 0.11.7 for resolving installs * fix(installing): skip pacquet in lockfile check mode * fix(installing): harden pacquet lockfile handoff * fix(installing): preserve policy handling with pacquet * fix(installing): skip pacquet when lockfile is disabled * fix(installing): skip pacquet with branch lockfiles
This commit is contained in:
9
.changeset/pacquet-resolving-install-delegation.md
Normal file
9
.changeset/pacquet-resolving-install-delegation.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@pnpm/installing.commands": minor
|
||||
"@pnpm/installing.deps-installer": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
When [`pacquet`](https://github.com/pnpm/pnpm/tree/main/pacquet) (the Rust port of pnpm) is declared in `configDependencies`, pnpm now delegates dependency **resolution** to it too — not just materialization — provided the installed pacquet is new enough to support full resolving installs (>= 0.11.7).
|
||||
|
||||
Previously pacquet only ran in frozen-install mode: pnpm always resolved the dependency graph itself (writing `pnpm-lock.yaml`) and handed pacquet a finished lockfile to fetch / import / link. With pacquet >= 0.11.7, a non-frozen `pnpm install` (default isolated `nodeLinker`, plain install) is delegated to pacquet end-to-end in a single pass — pacquet resolves the manifests, writes the lockfile, and materializes `node_modules`. pnpm detects the capability from the installed pacquet's version; older pacquet releases keep the resolve-then-materialize split, and `add` / `update` / `remove` still resolve in pnpm (it has to mutate the manifests first). This remains an opt-in preview of the Rust install engine [#11723](https://github.com/pnpm/pnpm/issues/11723).
|
||||
@@ -123,11 +123,14 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
|
||||
pnpmfile: string[]
|
||||
/**
|
||||
* Alternative install engine (today: pacquet) the deps-installer
|
||||
* delegates the materialization phase to. Built in `installDeps`
|
||||
* when `configDependencies.pacquet` is declared, threaded through
|
||||
* here so the recursive workspace path picks it up too.
|
||||
* delegates the install to. Built in `installDeps` when
|
||||
* `configDependencies.pacquet` is declared, threaded through here so
|
||||
* the recursive workspace path picks it up too.
|
||||
*/
|
||||
runPacquet?: () => Promise<void>
|
||||
runPacquet?: {
|
||||
supportsResolution: boolean
|
||||
run: (opts?: { filterResolvedProgress?: boolean, resolve?: boolean }) => Promise<void>
|
||||
}
|
||||
} & Partial<
|
||||
Pick<Config,
|
||||
| 'ci'
|
||||
|
||||
@@ -51,21 +51,6 @@ export interface MakeRunPacquetOpts {
|
||||
isInstallCommand: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the install-engine callback `mutateModules` invokes when
|
||||
* `configDependencies` declares pacquet.
|
||||
*
|
||||
* The callback spawns the pacquet binary installed under
|
||||
* `node_modules/.pnpm-config/pacquet`. From `pnpm install`/`pnpm i` it
|
||||
* forwards the user's own pnpm CLI flags to pacquet's `install`
|
||||
* subcommand; from `add`/`update`/`dedupe` it doesn't forward (warning
|
||||
* instead). Pacquet's NDJSON stderr is parsed line-by-line and the
|
||||
* valid JSON records are re-emitted on pnpm's global `streamParser` so
|
||||
* `@pnpm/cli.default-reporter` renders pacquet's events the same way it
|
||||
* renders pnpm's own. Non-JSON stderr lines (panic backtraces,
|
||||
* unexpected diagnostics) are forwarded to the real stderr verbatim so
|
||||
* they reach the user.
|
||||
*/
|
||||
/** Args the deps-installer passes per pacquet invocation. */
|
||||
export interface RunPacquetCallOpts {
|
||||
/**
|
||||
@@ -79,9 +64,56 @@ export interface RunPacquetCallOpts {
|
||||
* source.
|
||||
*/
|
||||
filterResolvedProgress?: boolean
|
||||
/**
|
||||
* `true` to let pacquet perform the resolution itself rather than
|
||||
* materialize an already-resolved lockfile. Drops the injected
|
||||
* `--frozen-lockfile`, so pacquet resolves the manifests, writes
|
||||
* `pnpm-lock.yaml`, and materializes in a single pass. Only valid
|
||||
* when {@link PacquetEngine.supportsResolution} is `true` (pacquet
|
||||
* >= 0.11.7).
|
||||
*/
|
||||
resolve?: boolean
|
||||
}
|
||||
|
||||
export function makeRunPacquet (opts: MakeRunPacquetOpts): (callOpts?: RunPacquetCallOpts) => Promise<void> {
|
||||
/**
|
||||
* Handle to the pacquet install engine: its capabilities plus the
|
||||
* callback `mutateModules` invokes to run it.
|
||||
*/
|
||||
export interface PacquetEngine {
|
||||
/**
|
||||
* `true` when the installed pacquet is new enough (>= 0.11.7) to
|
||||
* perform dependency resolution itself. When `false`, pacquet can
|
||||
* only materialize an already-resolved lockfile, so the deps-installer
|
||||
* runs its own resolve pass first and hands the written lockfile to
|
||||
* pacquet.
|
||||
*/
|
||||
supportsResolution: boolean
|
||||
run: (callOpts?: RunPacquetCallOpts) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the pacquet install engine `mutateModules` delegates to when
|
||||
* `configDependencies` declares pacquet.
|
||||
*
|
||||
* `run` spawns the pacquet binary installed under
|
||||
* `node_modules/.pnpm-config/pacquet`. From `pnpm install`/`pnpm i` it
|
||||
* forwards the user's own pnpm CLI flags to pacquet's `install`
|
||||
* subcommand; from `add`/`update`/`dedupe` it doesn't forward (warning
|
||||
* instead). Pacquet's NDJSON stderr is parsed line-by-line and the
|
||||
* valid JSON records are re-emitted on pnpm's global `streamParser` so
|
||||
* `@pnpm/cli.default-reporter` renders pacquet's events the same way it
|
||||
* renders pnpm's own. Non-JSON stderr lines (panic backtraces,
|
||||
* unexpected diagnostics) are forwarded to the real stderr verbatim so
|
||||
* they reach the user.
|
||||
*/
|
||||
export function makeRunPacquet (opts: MakeRunPacquetOpts): PacquetEngine {
|
||||
return {
|
||||
supportsResolution: pacquetSupportsResolution(resolvePacquetVersion(opts.lockfileDir, opts.packageName)),
|
||||
run: makeRun(opts),
|
||||
}
|
||||
}
|
||||
|
||||
function makeRun (opts: MakeRunPacquetOpts): (callOpts?: RunPacquetCallOpts) => Promise<void> {
|
||||
return async (callOpts) => {
|
||||
const pacquetBin = resolvePacquetBin(opts.lockfileDir, opts.packageName)
|
||||
// From `pnpm install`/`pnpm i` we forward the user's flags through to
|
||||
@@ -94,16 +126,21 @@ export function makeRunPacquet (opts: MakeRunPacquetOpts): (callOpts?: RunPacque
|
||||
// `pnpm-workspace.yaml` / `.npmrc` on its own, so a non-install
|
||||
// delegation isn't broken by the omission.
|
||||
const forwardedFlags = opts.isInstallCommand ? collectForwardedFlags(opts.argv) : []
|
||||
// `--ignore-manifest-check` tells pacquet to skip its per-importer
|
||||
// `package.json` ↔ `pnpm-lock.yaml` freshness gate. pnpm just
|
||||
// resolved and wrote the lockfile itself; on `pnpm up` / `add` /
|
||||
// `remove` the manifest on disk is still the pre-mutation copy
|
||||
// (pnpm writes it after `mutateModules` returns), so pacquet's own
|
||||
// check would always fire here. See
|
||||
// In resolve mode pacquet does the resolution itself, so it must not
|
||||
// be pinned to the existing lockfile — drop both injected flags.
|
||||
//
|
||||
// Otherwise (frozen materialization) inject `--frozen-lockfile` plus
|
||||
// `--ignore-manifest-check`. The latter tells pacquet to skip its
|
||||
// per-importer `package.json` ↔ `pnpm-lock.yaml` freshness gate:
|
||||
// pnpm just resolved and wrote the lockfile itself; on `pnpm up` /
|
||||
// `add` / `remove` the manifest on disk is still the pre-mutation
|
||||
// copy (pnpm writes it after `mutateModules` returns), so pacquet's
|
||||
// own check would always fire here. See
|
||||
// https://github.com/pnpm/pnpm/issues/11797. The flag is narrow
|
||||
// (only the manifest check); settings drift like `overrides` is
|
||||
// still enforced and was already re-validated by pnpm.
|
||||
const args = ['--reporter=ndjson', 'install', '--frozen-lockfile', '--ignore-manifest-check', ...forwardedFlags]
|
||||
const frozenArgs = callOpts?.resolve === true ? [] : ['--frozen-lockfile', '--ignore-manifest-check']
|
||||
const args = ['--reporter=ndjson', 'install', ...frozenArgs, ...forwardedFlags]
|
||||
const droppedFlags = opts.isInstallCommand ? [] : collectDroppedFlags(opts.argv)
|
||||
if (droppedFlags.length > 0) {
|
||||
logger.warn({
|
||||
@@ -193,6 +230,35 @@ export function pacquetPlatformPkgName (): string {
|
||||
return `@pacquet/${process.platform}-${process.arch}${libc}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the installed pacquet's version from its `package.json` under
|
||||
* `node_modules/.pnpm-config/<packageName>`. Returns `undefined` if it
|
||||
* can't be read — callers treat that as "assume the older,
|
||||
* materialization-only pacquet" so a missing/garbled manifest degrades
|
||||
* to the safe path rather than failing the install.
|
||||
*/
|
||||
function resolvePacquetVersion (lockfileDir: string, packageName: 'pacquet' | '@pnpm/pacquet'): string | undefined {
|
||||
try {
|
||||
const pacquetPkg = fs.realpathSync(path.join(lockfileDir, 'node_modules/.pnpm-config', packageName, 'package.json'))
|
||||
const { version } = JSON.parse(fs.readFileSync(pacquetPkg, 'utf8')) as { version?: string }
|
||||
return version
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pacquet gained full resolving installs in 0.11.7; earlier releases
|
||||
* stay on pnpm's resolve-then-materialize path. Pre-release builds of
|
||||
* 0.11.7 (e.g. `0.11.7-rc.1`) count as supporting it.
|
||||
*/
|
||||
function pacquetSupportsResolution (version: string | undefined): boolean {
|
||||
if (version == null) return false
|
||||
const [major, minor, patch] = version.split('.', 3).map((part) => parseInt(part, 10))
|
||||
if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false
|
||||
return major > 0 || (major === 0 && (minor > 11 || (minor === 11 && patch >= 7)))
|
||||
}
|
||||
|
||||
/**
|
||||
* From `pnpm install`/`pnpm i`, return everything in argv that should
|
||||
* ride along to pacquet's own `install` subcommand. Drops the
|
||||
|
||||
57
installing/commands/test/runPacquet.ts
Normal file
57
installing/commands/test/runPacquet.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { expect, test } from '@jest/globals'
|
||||
import { temporaryDirectory } from 'tempy'
|
||||
|
||||
import { makeRunPacquet } from '../lib/runPacquet.js'
|
||||
|
||||
function setupPacquetConfigDep (version: string | undefined, packageName: 'pacquet' | '@pnpm/pacquet' = 'pacquet'): string {
|
||||
const lockfileDir = temporaryDirectory()
|
||||
const pkgDir = path.join(lockfileDir, 'node_modules/.pnpm-config', packageName)
|
||||
fs.mkdirSync(pkgDir, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(pkgDir, 'package.json'),
|
||||
JSON.stringify(version == null ? { name: packageName } : { name: packageName, version })
|
||||
)
|
||||
return lockfileDir
|
||||
}
|
||||
|
||||
function makeEngine (lockfileDir: string, packageName: 'pacquet' | '@pnpm/pacquet' = 'pacquet'): ReturnType<typeof makeRunPacquet> {
|
||||
return makeRunPacquet({
|
||||
lockfileDir,
|
||||
packageName,
|
||||
argv: { original: [], remain: [] },
|
||||
isInstallCommand: true,
|
||||
})
|
||||
}
|
||||
|
||||
test.each([
|
||||
['0.11.7', true],
|
||||
['0.11.7-rc.1', true],
|
||||
['0.12.3', true],
|
||||
['1.0.0', true],
|
||||
['0.11.6', false],
|
||||
['0.11.0', false],
|
||||
['0.10.99', false],
|
||||
['0.2.2', false],
|
||||
['0.0.1', false],
|
||||
])('pacquet %s -> supportsResolution %s', (version, expected) => {
|
||||
const engine = makeEngine(setupPacquetConfigDep(version))
|
||||
expect(engine.supportsResolution).toBe(expected)
|
||||
})
|
||||
|
||||
test('supportsResolution is false when the pacquet version cannot be read', () => {
|
||||
const engine = makeEngine(setupPacquetConfigDep(undefined))
|
||||
expect(engine.supportsResolution).toBe(false)
|
||||
})
|
||||
|
||||
test('supportsResolution is false when the pacquet config dependency is absent', () => {
|
||||
const engine = makeEngine(temporaryDirectory())
|
||||
expect(engine.supportsResolution).toBe(false)
|
||||
})
|
||||
|
||||
test('the version is read from the @pnpm/pacquet scoped alias too', () => {
|
||||
const lockfileDir = setupPacquetConfigDep('0.11.7', '@pnpm/pacquet')
|
||||
expect(makeEngine(lockfileDir, '@pnpm/pacquet').supportsResolution).toBe(true)
|
||||
})
|
||||
@@ -235,21 +235,30 @@ export interface StrictInstallOptions {
|
||||
packageVulnerabilityAudit?: PackageVulnerabilityAudit
|
||||
blockExoticSubdeps?: boolean
|
||||
/**
|
||||
* Optional alternative install engine. When set, the frozen-install
|
||||
* path invokes this callback instead of `headlessInstall`. The CLI
|
||||
* layer constructs it (today: spawning the pacquet binary installed
|
||||
* via `configDependencies` and forwarding pnpm's own CLI argv); the
|
||||
* installer treats it as an opaque "do the install" hook so it
|
||||
* doesn't need to know about pacquet's binary path, CLI surface, or
|
||||
* any settings that only pacquet consumes.
|
||||
* Optional alternative install engine. When set, the installer
|
||||
* delegates the install to `run` instead of calling `headlessInstall`.
|
||||
* The CLI layer constructs it (today: the pacquet binary installed via
|
||||
* `configDependencies`, forwarding pnpm's own CLI argv); the installer
|
||||
* treats it as an opaque "do the install" hook so it doesn't need to
|
||||
* know about pacquet's binary path, CLI surface, or any settings that
|
||||
* only pacquet consumes.
|
||||
*
|
||||
* `filterResolvedProgress` tells the helper to drop the engine's
|
||||
* own `pnpm:progress status:resolved` events because pnpm already
|
||||
* emitted one per package during a preceding lockfileOnly resolve
|
||||
* pass. The frozen-install path passes `false` (or nothing): no
|
||||
* resolve pass ran, so the engine's events are the only source.
|
||||
* `supportsResolution` is `true` when the engine can resolve
|
||||
* dependencies itself (pacquet >= 0.11.7). When `false` the installer
|
||||
* runs its own resolve pass first and the engine only materializes the
|
||||
* written lockfile.
|
||||
*
|
||||
* `run`'s `filterResolvedProgress` tells the helper to drop the
|
||||
* engine's own `pnpm:progress status:resolved` events because pnpm
|
||||
* already emitted one per package during a preceding lockfileOnly
|
||||
* resolve pass. `resolve` tells the engine to do the resolution
|
||||
* itself (non-frozen install). The frozen/materialize paths leave
|
||||
* both unset.
|
||||
*/
|
||||
runPacquet?: (opts?: { filterResolvedProgress?: boolean }) => Promise<void>
|
||||
runPacquet?: {
|
||||
supportsResolution: boolean
|
||||
run: (opts?: { filterResolvedProgress?: boolean, resolve?: boolean }) => Promise<void>
|
||||
}
|
||||
/**
|
||||
* If true, `mutateModules` does not emit the per-install `summary` log
|
||||
* event. Used by `pnpm add -g` when it runs multiple isolated installs
|
||||
|
||||
@@ -46,8 +46,11 @@ import {
|
||||
isEmptyLockfile,
|
||||
type LockfileObject,
|
||||
type ProjectSnapshot,
|
||||
readEnvLockfile,
|
||||
readWantedLockfile,
|
||||
readWantedLockfileFile,
|
||||
writeCurrentLockfile,
|
||||
writeEnvLockfile,
|
||||
writeLockfiles,
|
||||
writeWantedLockfile,
|
||||
} from '@pnpm/lockfile.fs'
|
||||
@@ -371,11 +374,12 @@ export async function mutateModules (
|
||||
// install even mid-flight, and an install that finishes first is held back
|
||||
// until the verdict arrives.
|
||||
//
|
||||
// Skipped when we already know pacquet will run the install: pacquet's
|
||||
// frozen-install path applies the same resolver-policy gate (port of
|
||||
// this function), so re-running here would duplicate the work — and
|
||||
// for `minimumReleaseAge` in strict mode each lockfile entry is an
|
||||
// HTTP probe.
|
||||
// Skipped when we already know pacquet will run the install: pacquet
|
||||
// applies the same resolver-policy gate (port of this function) whether
|
||||
// it materializes a frozen lockfile or re-resolves from the manifests,
|
||||
// so re-running here would duplicate the work — and for
|
||||
// `minimumReleaseAge` in strict mode each lockfile entry is an HTTP
|
||||
// probe.
|
||||
//
|
||||
// The predicate mirrors every short-circuit `tryFrozenInstall` checks
|
||||
// before reaching the pacquet branch: anything that would make it
|
||||
@@ -385,13 +389,28 @@ export async function mutateModules (
|
||||
// isn't known here — so verification still runs in that window, the
|
||||
// duplicate is bounded to it.
|
||||
const willDelegateToPacquet = opts.runPacquet != null &&
|
||||
opts.useLockfile &&
|
||||
!opts.useGitBranchLockfile &&
|
||||
!opts.mergeGitBranchLockfiles &&
|
||||
opts.lockfileCheck == null &&
|
||||
opts.enableModulesDir &&
|
||||
installsOnly &&
|
||||
!opts.lockfileOnly &&
|
||||
!opts.fixLockfile &&
|
||||
!opts.dedupe &&
|
||||
!ctx.lockfileHadConflicts &&
|
||||
ctx.existsNonEmptyWantedLockfile &&
|
||||
(opts.frozenLockfile === true || opts.frozenLockfileIfExists === true)
|
||||
(
|
||||
// Frozen materialization: pacquet reads the existing lockfile and
|
||||
// re-applies the resolver-policy gate as it walks it.
|
||||
(ctx.existsNonEmptyWantedLockfile &&
|
||||
(opts.frozenLockfile === true || opts.frozenLockfileIfExists === true)) ||
|
||||
// Resolving install: pacquet (>= 0.11.7) re-resolves from the
|
||||
// manifests itself — applying the policy during fresh resolution —
|
||||
// so the existing lockfile entries verified here would just be
|
||||
// discarded. If a policy handler is active, keep resolution in pnpm
|
||||
// so violations can be returned to the command layer.
|
||||
(opts.saveLockfile && opts.runPacquet.supportsResolution && opts.frozenLockfile !== true && opts.nodeLinker !== 'hoisted' && opts.handleResolutionPolicyViolations == null)
|
||||
)
|
||||
let verifyLockfilePromise: Promise<void> | undefined
|
||||
if (!willDelegateToPacquet && !opts.trustLockfile) {
|
||||
const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0
|
||||
@@ -1044,9 +1063,9 @@ Note that in CI environments, this setting is enabled by default.`,
|
||||
} else {
|
||||
logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir })
|
||||
}
|
||||
if (opts.runPacquet != null) {
|
||||
if (opts.runPacquet != null && opts.useLockfile && !opts.useGitBranchLockfile && !opts.mergeGitBranchLockfiles && opts.lockfileCheck == null && opts.enableModulesDir) {
|
||||
try {
|
||||
await opts.runPacquet()
|
||||
await opts.runPacquet.run()
|
||||
} catch (err) {
|
||||
// Same reasoning as the verifyLockfileResolutions catch above: this
|
||||
// is the user-facing failure path, so detach the reporter listener
|
||||
@@ -1921,6 +1940,29 @@ function allMutationsAreInstalls (projects: MutatedProject[]): boolean {
|
||||
return projects.every((project) => project.mutation === 'install' && !project.update && !project.updateMatching)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `InstallFunctionResult` for an install pacquet resolved and
|
||||
* materialized end-to-end. pacquet wrote `pnpm-lock.yaml` and the
|
||||
* `node_modules` tree itself. `ctx.wantedLockfile` has already been
|
||||
* refreshed from disk, and pacquet reports its own stats / ignored-builds
|
||||
* via NDJSON, so the structured `stats` / `ignoredBuilds` fall back to
|
||||
* their no-op defaults. Resolution-policy handlers are guarded out before
|
||||
* this path, so there are no command-layer policy violations to return.
|
||||
* Manifests are returned unchanged — this path only runs for plain
|
||||
* installs, which don't rewrite `package.json`.
|
||||
*/
|
||||
function pacquetResolveResult (projects: ImporterToUpdate[], ctx: PnpmContext): InstallFunctionResult {
|
||||
return {
|
||||
newLockfile: ctx.wantedLockfile,
|
||||
projects: projects.map((project) => ({
|
||||
manifest: project.originalManifest ?? project.manifest,
|
||||
rootDir: project.rootDir,
|
||||
})),
|
||||
depsRequiringBuild: [],
|
||||
resolutionPolicyViolations: [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the pacquet binary if it's configured, otherwise run the JS
|
||||
* `headlessInstall`. Callers can hand off any code path that materializes
|
||||
@@ -1937,16 +1979,28 @@ function allMutationsAreInstalls (projects: MutatedProject[]): boolean {
|
||||
* stats record and a no-op ignoredBuilds iteration).
|
||||
*/
|
||||
async function materializeOrDelegate (
|
||||
opts: { runPacquet?: (opts?: { filterResolvedProgress?: boolean }) => Promise<void> },
|
||||
opts: {
|
||||
mergeGitBranchLockfiles?: boolean
|
||||
runPacquet?: { run: (opts?: { filterResolvedProgress?: boolean }) => Promise<void> }
|
||||
saveLockfile?: boolean
|
||||
useGitBranchLockfile?: boolean
|
||||
useLockfile?: boolean
|
||||
},
|
||||
runHeadlessInstall: () => Promise<{ stats: InstallationResultStats, ignoredBuilds: IgnoredBuilds | undefined }>
|
||||
): Promise<{ stats?: InstallationResultStats, ignoredBuilds?: IgnoredBuilds }> {
|
||||
if (opts.runPacquet != null) {
|
||||
if (
|
||||
opts.runPacquet != null &&
|
||||
opts.useLockfile !== false &&
|
||||
opts.saveLockfile !== false &&
|
||||
opts.useGitBranchLockfile !== true &&
|
||||
opts.mergeGitBranchLockfiles !== true
|
||||
) {
|
||||
// Reached only from the resolve-then-materialize call sites
|
||||
// (workspace-partial, hoisted-linker, pnpr server install). Each ran a
|
||||
// lockfileOnly resolve pass that emitted one
|
||||
// `pnpm:progress status:resolved` per package, so pacquet's
|
||||
// duplicate `resolved` events would double the reporter's count.
|
||||
await opts.runPacquet({ filterResolvedProgress: true })
|
||||
await opts.runPacquet.run({ filterResolvedProgress: true })
|
||||
return {}
|
||||
}
|
||||
return runHeadlessInstall()
|
||||
@@ -1958,7 +2012,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
if (!opts.frozenLockfile && opts.useLockfile) {
|
||||
const allProjectsLocatedInsideWorkspace = Object.values(ctx.projects)
|
||||
.filter((project) => isPathInsideWorkspace(project.rootDirRealPath ?? project.rootDir))
|
||||
if (allProjectsLocatedInsideWorkspace.length > projects.length) {
|
||||
if (allProjectsLocatedInsideWorkspace.length > projects.length && opts.lockfileCheck == null && opts.enableModulesDir) {
|
||||
const newProjects = [...projects]
|
||||
const getWantedDepsOpts = {
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
@@ -2010,7 +2064,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly) {
|
||||
if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly && opts.lockfileCheck == null && opts.enableModulesDir) {
|
||||
const result = await _installInContext(projects, ctx, {
|
||||
...opts,
|
||||
lockfileOnly: true,
|
||||
@@ -2036,20 +2090,67 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
ignoredBuilds,
|
||||
}
|
||||
}
|
||||
// Isolated `nodeLinker` (the default) with a non-frozen install:
|
||||
// pacquet doesn't ship a resolver yet, so split the install in two —
|
||||
// ask `_installInContext` for a `lockfileOnly` resolve pass (writes
|
||||
// `pnpm-lock.yaml`), then hand the freshly-written lockfile to
|
||||
// pacquet for the fetch / import / link / build phases. The frozen
|
||||
// branch is handled earlier in `tryFrozenInstall`; the hoisted
|
||||
// branch above already runs the same resolve-then-materialize
|
||||
// sequence (it had to even before pacquet existed). When no pacquet
|
||||
// is configured this falls through to the full single-pass install.
|
||||
if (opts.runPacquet != null && !opts.lockfileOnly) {
|
||||
// Isolated `nodeLinker` (the default) with a non-frozen install.
|
||||
// The frozen branch is handled earlier in `tryFrozenInstall`; the
|
||||
// hoisted branch above runs a resolve-then-materialize sequence.
|
||||
if (opts.runPacquet != null && opts.useLockfile && opts.saveLockfile && !opts.useGitBranchLockfile && !opts.mergeGitBranchLockfiles && !opts.lockfileOnly && opts.lockfileCheck == null && opts.enableModulesDir) {
|
||||
// pacquet >= 0.11.7 resolves itself: hand it the whole install
|
||||
// (resolve + fetch + import + link + build, writing the lockfile)
|
||||
// in a single non-frozen pass. Only for plain installs — `add` /
|
||||
// `update` / `remove` need pnpm to mutate the manifests and
|
||||
// resolve the new specs first (pacquet's `install` reads
|
||||
// package.json from disk, which pnpm hasn't rewritten yet).
|
||||
if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && opts.handleResolutionPolicyViolations == null && allMutationsAreInstalls(projects)) {
|
||||
// `configDependencies` are recorded in a YAML document prepended
|
||||
// to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
|
||||
// model. Capture it before pacquet rewrites the lockfile and
|
||||
// restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
|
||||
// document and re-prepends the env document), otherwise the next
|
||||
// `--frozen-lockfile` install fails its config-deps freshness gate.
|
||||
// The restore runs even if pacquet fails partway: a non-zero exit can
|
||||
// still leave a rewritten lockfile behind, so the env document must be
|
||||
// put back regardless.
|
||||
const envLockfile = await readEnvLockfile(ctx.lockfileDir)
|
||||
let pacquetError: unknown
|
||||
try {
|
||||
await opts.runPacquet.run({ resolve: true })
|
||||
} catch (err: unknown) {
|
||||
pacquetError = err
|
||||
throw err
|
||||
} finally {
|
||||
if (envLockfile != null) {
|
||||
await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
|
||||
if (pacquetError == null) {
|
||||
throw restoreErr
|
||||
}
|
||||
logger.warn({
|
||||
error: restoreErr,
|
||||
message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
|
||||
prefix: ctx.lockfileDir,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
|
||||
ignoreIncompatible: opts.force || opts.ci === true,
|
||||
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
||||
useGitBranchLockfile: opts.useGitBranchLockfile,
|
||||
wantedVersions: [LOCKFILE_VERSION],
|
||||
})
|
||||
if (wantedLockfile == null) {
|
||||
throw new PnpmError('PACQUET_LOCKFILE_READ_FAILED', `pacquet did not write a readable ${WANTED_LOCKFILE}`)
|
||||
}
|
||||
ctx.wantedLockfile = wantedLockfile
|
||||
return pacquetResolveResult(projects, ctx)
|
||||
}
|
||||
// Older pacquet can only materialize: split the install in two —
|
||||
// ask `_installInContext` for a `lockfileOnly` resolve pass (writes
|
||||
// `pnpm-lock.yaml`), then hand the freshly-written lockfile to
|
||||
// pacquet for the fetch / import / link / build phases. The resolve
|
||||
// pass emitted a `pnpm:progress status:resolved` per package; ask
|
||||
// pacquet to drop its own duplicates.
|
||||
const result = await _installInContext(projects, ctx, { ...opts, lockfileOnly: true })
|
||||
// The resolve pass above emitted a `pnpm:progress status:resolved`
|
||||
// per package; ask pacquet to drop its own duplicates.
|
||||
await opts.runPacquet({ filterResolvedProgress: true })
|
||||
await opts.runPacquet.run({ filterResolvedProgress: true })
|
||||
return result
|
||||
}
|
||||
return await _installInContext(projects, ctx, opts)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { expect, test } from '@jest/globals'
|
||||
import { expect, jest, test } from '@jest/globals'
|
||||
import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { addDistTag } from '@pnpm/testing.registry-mock'
|
||||
import { writeYamlFileSync } from 'write-yaml-file'
|
||||
|
||||
import { testDefaults } from '../utils/index.js'
|
||||
|
||||
@@ -25,6 +27,111 @@ test('prefer version ranges specified for top dependencies', async () => {
|
||||
expect(lockfile.packages).not.toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0'])
|
||||
})
|
||||
|
||||
test('does not delegate lockfile check mode to pacquet', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
await install({
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pkg-with-1-dep': '100.0.0',
|
||||
},
|
||||
}, testDefaults())
|
||||
|
||||
const lockfileCheck = jest.fn()
|
||||
const runPacquet = jest.fn<() => Promise<void>>().mockResolvedValue(undefined)
|
||||
|
||||
await install({
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pkg-with-1-dep': '100.0.0',
|
||||
},
|
||||
}, testDefaults({
|
||||
dedupe: true,
|
||||
lockfileCheck,
|
||||
runPacquet: {
|
||||
supportsResolution: true,
|
||||
run: runPacquet,
|
||||
},
|
||||
}))
|
||||
|
||||
expect(lockfileCheck).toHaveBeenCalled()
|
||||
expect(runPacquet).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does not delegate no-lockfile installs to pacquet', async () => {
|
||||
const project = prepareEmpty()
|
||||
const runPacquet = jest.fn<() => Promise<void>>().mockResolvedValue(undefined)
|
||||
|
||||
await install({
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
}, testDefaults({
|
||||
runPacquet: {
|
||||
supportsResolution: true,
|
||||
run: runPacquet,
|
||||
},
|
||||
useLockfile: false,
|
||||
}))
|
||||
|
||||
expect(runPacquet).not.toHaveBeenCalled()
|
||||
expect(project.readLockfile()).toBeFalsy()
|
||||
})
|
||||
|
||||
test('uses the lockfile written by pacquet for post-install checks', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
await install({
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
}, testDefaults({
|
||||
allowBuilds: {
|
||||
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
|
||||
},
|
||||
}))
|
||||
|
||||
const depPath = '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0'
|
||||
const runPacquet = jest.fn<(opts?: { resolve?: boolean }) => Promise<void>>().mockImplementation(async () => {
|
||||
writeYamlFileSync(WANTED_LOCKFILE, {
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pre-and-postinstall-scripts-example': {
|
||||
specifier: '1.0.0',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
packages: {
|
||||
[depPath]: {
|
||||
resolution: {
|
||||
integrity: 'sha512-test',
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshots: {
|
||||
[depPath]: {},
|
||||
},
|
||||
}, { lineWidth: 1000 })
|
||||
})
|
||||
|
||||
const { ignoredBuilds } = await install({
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
|
||||
},
|
||||
}, testDefaults({
|
||||
allowBuilds: {},
|
||||
runPacquet: {
|
||||
supportsResolution: true,
|
||||
run: runPacquet,
|
||||
},
|
||||
}))
|
||||
|
||||
expect(runPacquet).toHaveBeenCalledWith({ resolve: true })
|
||||
expect(Array.from(ignoredBuilds ?? [])).toContain(depPath)
|
||||
})
|
||||
|
||||
test('prefer version ranges specified for top dependencies, when doing named installation', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
|
||||
@@ -34,6 +34,30 @@ test('install with git-branch-lockfile = true', async () => {
|
||||
expect(fs.existsSync(WANTED_LOCKFILE)).toBe(false)
|
||||
})
|
||||
|
||||
test('git-branch-lockfile installs are not delegated to pacquet', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const branchName: string = 'main-branch'
|
||||
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve(branchName))
|
||||
const runPacquet = jest.fn<() => Promise<void>>().mockResolvedValue(undefined)
|
||||
|
||||
await install({
|
||||
dependencies: {
|
||||
'is-positive': '^3.0.0',
|
||||
},
|
||||
}, testDefaults({
|
||||
runPacquet: {
|
||||
supportsResolution: true,
|
||||
run: runPacquet,
|
||||
},
|
||||
useGitBranchLockfile: true,
|
||||
}))
|
||||
|
||||
expect(runPacquet).not.toHaveBeenCalled()
|
||||
expect(fs.existsSync(`pnpm-lock.${branchName}.yaml`)).toBe(true)
|
||||
expect(fs.existsSync(WANTED_LOCKFILE)).toBe(false)
|
||||
})
|
||||
|
||||
test('install with git-branch-lockfile = true and no lockfile changes', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from '@jest/globals'
|
||||
import { expect, jest, test } from '@jest/globals'
|
||||
import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer'
|
||||
import { readWantedLockfile, writeWantedLockfile } from '@pnpm/lockfile.fs'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
@@ -251,6 +251,35 @@ test('loose mode surfaces immature fresh picks in the install result', async ()
|
||||
)
|
||||
})
|
||||
|
||||
test('pacquet materializes after pnpm resolves when policy violations must be surfaced', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge })
|
||||
const runPacquet = jest.fn<(opts?: { filterResolvedProgress?: boolean, resolve?: boolean }) => Promise<void>>().mockResolvedValue(undefined)
|
||||
const result = await install({
|
||||
dependencies: {
|
||||
'is-odd': '0.1',
|
||||
},
|
||||
}, {
|
||||
...opts,
|
||||
handleResolutionPolicyViolations: async () => {},
|
||||
runPacquet: {
|
||||
supportsResolution: true,
|
||||
run: runPacquet,
|
||||
},
|
||||
})
|
||||
|
||||
expect(runPacquet).toHaveBeenCalledWith({ filterResolvedProgress: true })
|
||||
expect(runPacquet).not.toHaveBeenCalledWith({ resolve: true })
|
||||
expect(result.resolutionPolicyViolations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: 'is-odd',
|
||||
version: '0.1.0',
|
||||
code: 'MINIMUM_RELEASE_AGE_VIOLATION',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('versions excluded via minimumReleaseAgeExclude are not surfaced as violations', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
|
||||
@@ -7,11 +7,14 @@ import { writeYamlFileSync } from 'write-yaml-file'
|
||||
import { execPnpm, execPnpmSync } from '../utils/index.js'
|
||||
|
||||
// `pacquet` is fetched from the real npm registry — registry-mock doesn't
|
||||
// carry it (or its platform-specific binary sub-packages). Pinned to a
|
||||
// version known to ship the `configDependencies` integration surface this
|
||||
// PR depends on; tests are gated on the public registry being reachable.
|
||||
// carry it (or its platform-specific binary sub-packages). Tests are gated
|
||||
// on the public registry being reachable.
|
||||
const PUBLIC_REGISTRY = '--config.registry=https://registry.npmjs.org/'
|
||||
const PACQUET_VERSION = '0.2.2'
|
||||
// pacquet >= 0.11.7 supports full resolving installs, so pnpm delegates
|
||||
// non-frozen plain installs to it too.
|
||||
const PACQUET_VERSION = '0.11.7'
|
||||
// pacquet < 0.11.7 stays on pnpm's resolve-then-materialize path.
|
||||
const PACQUET_RESOLVE_WITH_PNPM_VERSION = '0.11.6'
|
||||
|
||||
// Each test runs two or three installs against the public registry; raise
|
||||
// the per-test timeout above jest's 5s default to allow for cold caches.
|
||||
@@ -21,6 +24,8 @@ interface PrepareOpts {
|
||||
manifest?: { dependencies?: Record<string, string>, devDependencies?: Record<string, string> }
|
||||
/** Which `configDependencies` slot declares pacquet. Both work. */
|
||||
pacquetConfigDepName?: 'pacquet' | '@pnpm/pacquet'
|
||||
/** Which pacquet version to declare. Defaults to {@link PACQUET_VERSION}. */
|
||||
version?: string
|
||||
}
|
||||
|
||||
/** Set up a temp project + workspace yaml + initial install. */
|
||||
@@ -28,7 +33,7 @@ async function prepareWithPacquet (opts: PrepareOpts = {}): Promise<void> {
|
||||
prepare(opts.manifest ?? {})
|
||||
writeYamlFileSync('pnpm-workspace.yaml', {
|
||||
configDependencies: {
|
||||
[opts.pacquetConfigDepName ?? 'pacquet']: PACQUET_VERSION,
|
||||
[opts.pacquetConfigDepName ?? 'pacquet']: opts.version ?? PACQUET_VERSION,
|
||||
},
|
||||
})
|
||||
// Initial install populates pnpm-lock.yaml plus configDependencies
|
||||
@@ -56,14 +61,13 @@ test('pnpm install --frozen-lockfile delegates to pacquet when declared in confi
|
||||
expect(fs.existsSync('node_modules/is-positive/package.json')).toBe(true)
|
||||
}, TIMEOUT)
|
||||
|
||||
test('bare `pnpm install` (no --frozen-lockfile) delegates the materialization to pacquet', async () => {
|
||||
test('bare `pnpm install` (no --frozen-lockfile) delegates to pacquet when the lockfile is up to date', async () => {
|
||||
await prepareWithPacquet({ manifest: { dependencies: { 'is-positive': '3.1.0' } } })
|
||||
await fs.promises.rm('node_modules', { recursive: true, force: true })
|
||||
|
||||
// No `--frozen-lockfile` flag. The expected path is: pnpm runs a
|
||||
// lockfileOnly resolve pass (the lockfile is already up-to-date so
|
||||
// it's a no-op write), then hands fetch / import / link off to
|
||||
// pacquet via the default-isolated-linker branch.
|
||||
// No `--frozen-lockfile` flag, but the lockfile is already up to date
|
||||
// with the manifest, so no resolution is needed: pnpm delegates the
|
||||
// whole install to pacquet just as it would for a frozen install.
|
||||
const { stdout, status } = execPnpmSync(
|
||||
[PUBLIC_REGISTRY, 'install'],
|
||||
{ env: { pnpm_config_silent: 'false' }, stdio: 'pipe', expectSuccess: true }
|
||||
@@ -73,17 +77,50 @@ test('bare `pnpm install` (no --frozen-lockfile) delegates the materialization t
|
||||
expect(fs.existsSync('node_modules/is-positive/package.json')).toBe(true)
|
||||
}, TIMEOUT)
|
||||
|
||||
// Skipped until pacquet writes a `.modules.yaml` whose `publicHoistPattern`
|
||||
// matches what pnpm computes on a follow-up command. Today pacquet's
|
||||
// materialization writes a different value, so the second pnpm command
|
||||
// in the same project fails with
|
||||
// `ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF`. Bare `--frozen-lockfile` /
|
||||
// `install` tests escape this by wiping `node_modules` between
|
||||
// invocations; `pnpm add` and `pnpm update` can't, because they need
|
||||
// the prior install's state to do anything meaningful. Tracked as a
|
||||
// pacquet-side parity gap; re-enable once pacquet's `.modules.yaml`
|
||||
// shape matches pnpm's.
|
||||
test.skip('`pnpm add <pkg>` resolves the new dep with pnpm and materializes with pacquet', async () => {
|
||||
test('pnpm install resolves a newly-added dependency with pacquet >= 0.11.7', async () => {
|
||||
// `prepare` installs with no dependencies, so the lockfile has no entry
|
||||
// for `is-positive`. Adding it to the manifest forces a real resolution
|
||||
// on the next install — which pacquet performs itself, in a single
|
||||
// non-frozen pass (resolve + materialize), without a pnpm resolve pass.
|
||||
await prepareWithPacquet()
|
||||
const manifest = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||
manifest.dependencies = { 'is-positive': '3.1.0' }
|
||||
fs.writeFileSync('package.json', JSON.stringify(manifest, null, 2))
|
||||
|
||||
const { stdout, status } = execPnpmSync(
|
||||
[PUBLIC_REGISTRY, 'install'],
|
||||
{ env: { pnpm_config_silent: 'false' }, stdio: 'pipe', expectSuccess: true }
|
||||
)
|
||||
expect(status).toBe(0)
|
||||
const output = stdout.toString()
|
||||
expect(output).toContain('Using pacquet for this install')
|
||||
expect(output).toContain('Progress: resolved')
|
||||
expect(output.indexOf('Using pacquet for this install')).toBeLessThan(output.indexOf('Progress: resolved'))
|
||||
expect(fs.existsSync('node_modules/is-positive/package.json')).toBe(true)
|
||||
}, TIMEOUT)
|
||||
|
||||
test('pnpm install resolves a newly-added dependency itself when pacquet < 0.11.7', async () => {
|
||||
// Same setup as the resolving test above, but with an older
|
||||
// pacquet: pnpm runs its own lockfileOnly resolve pass for the new dep
|
||||
// and hands the freshly-written lockfile to pacquet to materialize.
|
||||
await prepareWithPacquet({ version: PACQUET_RESOLVE_WITH_PNPM_VERSION })
|
||||
const manifest = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||
manifest.dependencies = { 'is-positive': '3.1.0' }
|
||||
fs.writeFileSync('package.json', JSON.stringify(manifest, null, 2))
|
||||
|
||||
const { stdout, status } = execPnpmSync(
|
||||
[PUBLIC_REGISTRY, 'install'],
|
||||
{ env: { pnpm_config_silent: 'false' }, stdio: 'pipe', expectSuccess: true }
|
||||
)
|
||||
expect(status).toBe(0)
|
||||
const output = stdout.toString()
|
||||
expect(output).toContain('Using pacquet for this install')
|
||||
expect(output).toContain('Progress: resolved')
|
||||
expect(output.indexOf('Progress: resolved')).toBeLessThan(output.indexOf('Using pacquet for this install'))
|
||||
expect(fs.existsSync('node_modules/is-positive/package.json')).toBe(true)
|
||||
}, TIMEOUT)
|
||||
|
||||
test('`pnpm add <pkg>` resolves the new dep with pnpm and materializes with pacquet', async () => {
|
||||
await prepareWithPacquet()
|
||||
|
||||
const { stdout, status } = execPnpmSync(
|
||||
@@ -101,14 +138,14 @@ test.skip('`pnpm add <pkg>` resolves the new dep with pnpm and materializes with
|
||||
expect(manifest.dependencies?.['is-positive']).toBeDefined()
|
||||
}, TIMEOUT)
|
||||
|
||||
// Same skip reason as the `pnpm add` test above:
|
||||
// `ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF` on the second invocation.
|
||||
test.skip('`pnpm update <pkg>` resolves a new version with pnpm and materializes with pacquet', async () => {
|
||||
// Start pinned to an older minor so `update` has something to do.
|
||||
await prepareWithPacquet({ manifest: { dependencies: { 'is-positive': '^3.0.0' } } })
|
||||
test('`pnpm update <pkg>` resolves a new version with pnpm and materializes with pacquet', async () => {
|
||||
// Start pinned to an old exact version so `update --latest` has
|
||||
// something to do (is-positive's latest is 3.1.0).
|
||||
await prepareWithPacquet({ manifest: { dependencies: { 'is-positive': '1.0.0' } } })
|
||||
const oldVersion = JSON.parse(
|
||||
await fs.promises.readFile('node_modules/is-positive/package.json', 'utf8')
|
||||
).version as string
|
||||
expect(oldVersion).toBe('1.0.0')
|
||||
|
||||
const { stdout, status } = execPnpmSync(
|
||||
[PUBLIC_REGISTRY, 'update', 'is-positive', '--latest'],
|
||||
@@ -119,17 +156,11 @@ test.skip('`pnpm update <pkg>` resolves a new version with pnpm and materializes
|
||||
const newVersion = JSON.parse(
|
||||
await fs.promises.readFile('node_modules/is-positive/package.json', 'utf8')
|
||||
).version as string
|
||||
// is-positive@4 is the current latest and is a major bump from the 3.x
|
||||
// line; `update --latest` should move past the original `^3.0.0` pin.
|
||||
// `update --latest` moves the `1.0.0` pin to the current latest (3.1.0).
|
||||
expect(newVersion).not.toBe(oldVersion)
|
||||
}, TIMEOUT)
|
||||
|
||||
// Skipped until pacquet ships a release built with the updated
|
||||
// `generate-packages.mjs` (this PR's change) so the `@pnpm/pacquet`
|
||||
// scoped alias actually exists on npm. The pinned PACQUET_VERSION
|
||||
// above doesn't publish that mirror yet. Re-enable when the next
|
||||
// pacquet release ships under both names.
|
||||
test.skip('the `@pnpm/pacquet` scoped alias is recognized in configDependencies', async () => {
|
||||
test('the `@pnpm/pacquet` scoped alias is recognized in configDependencies', async () => {
|
||||
await prepareWithPacquet({
|
||||
manifest: { dependencies: { 'is-positive': '3.1.0' } },
|
||||
pacquetConfigDepName: '@pnpm/pacquet',
|
||||
|
||||
Reference in New Issue
Block a user