Unify repeat-install freshness and `verifyDepsBeforeRun` validation across pnpm v11 and v12 (Closes pnpm/tasks#161, pnpm/tasks#157, pnpm/tasks#148): 1. Centralize v11 workspace state updates in `@pnpm/workspace.state` (`preserveCarriedSettings`) and `@pnpm/installing.commands` (`workspaceStateUpdate.ts`), preserving carried `dev`/`optional`/`production` settings when `ignoredWorkspaceStateSettings` ignores them and preserving v12-recorded `autoDedupe`. A filtered install that prunes direct dependencies no longer records the workspace root as installed. 2. Hold `{.}...` workspace dependencies for non-recursive `pnpm run`, `pnpm exec`, and `pnpm restart` inside a workspace project in v11 (`selectProjectsToVerify.ts`). 3. Align v11 recorded validation (`checkRecordedWorkspace`) and legacy unrecorded validation (`checkModifiedSinceLastValidation`) in `@pnpm/deps.status` with v12's `check_freshness`, including empty-project handling, `dedupeDirectDeps`, `dedupePeerDependents`, `autoInstallPeers`, `excludeLinksFromLockfile`, catalog/override config comparison, and `opts.lockfileDir` resolution. 4. Forward effective CLI flags (`--lockfile-dir`, `--modules-dir`, `--virtual-store-dir`, `--store-dir`, `--dir`, `--workspace-root`, `--prod`, `--dev`, `--no-optional`, `--filter`, `--filter-prod`, `--test-pattern`, `--changed-files-ignore-pattern`, and `--config.*`) when `verifyDepsBeforeRun: install` spawns `pnpm install` in both v11 and v12. 5. Align `GatePolicy::dedupe_peers` and empty-project predicates in v12 `pnpm-package-manager`, and run `repeat_install_oracle.rs` across v12, v11, and mixed v11/v12 writers in CI, with a longer nextest period for the peer convergence harness that shares the job. 6. Parse `--config.<key>=<value>` with the type the command declares for `<key>` in v11, so the list and number options forwarded to the spawned install arrive as lists and numbers; forward `--npmrc-auth-file` in v11 and `--test-pattern` / `--changed-files-ignore-pattern` in v12, so both forward the same set; and skip the current-lockfile read for a project whose manifest changed but whose lockfile did not, as v12 does. 7. Resolve relative v11 store overrides against the workspace or project root before forwarding them to the spawned install. Remove the isolated linker's duplicate package merge so filtered installs retain unselected dependencies and drop obsolete selected dependencies from the current lockfile. Cover both cases with CLI regressions. The restore API also handles a selected project absent from the previous current lockfile. 8. Record workspace validation from the projects the installers actually processed. Ignored projects receive no new proof, and empty or entirely ignored selections leave state untouched. Cover both lockfile modes and an ignored workspace root.
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import path from 'node:path'
|
|
|
|
import { describe, expect, test } from '@jest/globals'
|
|
import type { Project, ProjectRootDir, ProjectsGraph } from '@pnpm/types'
|
|
|
|
import { selectProjectsToVerify } from '../src/selectProjectsToVerify.js'
|
|
|
|
const workspaceDir = path.resolve('/workspace')
|
|
|
|
describe('selectProjectsToVerify', () => {
|
|
test('adds the workspace dependencies of the selected projects transitively', async () => {
|
|
const allProjectsGraph = createGraph({ a: ['b'], b: ['c'], c: [], d: [] })
|
|
expect(await selectNames({
|
|
filter: ['a'],
|
|
allProjectsGraph,
|
|
selectedProjectsGraph: pickGraph(allProjectsGraph, ['a']),
|
|
})).toStrictEqual(['a', 'b', 'c'])
|
|
})
|
|
|
|
test('reads a project selected only by --filter-prod from the prod graph', async () => {
|
|
const allProjectsGraph = createGraph({ a: ['b'], b: [] })
|
|
const prodAllProjectsGraph = createGraph({ a: [], b: [] })
|
|
expect(await selectNames({
|
|
filterProd: ['a'],
|
|
allProjectsGraph,
|
|
prodAllProjectsGraph,
|
|
prodOnlySelectedProjectDirs: [dirOf('a')],
|
|
selectedProjectsGraph: pickGraph(prodAllProjectsGraph, ['a']),
|
|
})).toStrictEqual(['a'])
|
|
})
|
|
|
|
test('leaves out a dependency that a negated selector excludes from the install', async () => {
|
|
const allProjectsGraph = createGraph({ a: ['b'], b: ['c'], c: [] })
|
|
expect(await selectNames({
|
|
filter: ['!b'],
|
|
allProjectsGraph,
|
|
selectedProjectsGraph: pickGraph(allProjectsGraph, ['a', 'c']),
|
|
})).toStrictEqual(['a', 'c'])
|
|
})
|
|
|
|
test('keeps a selected project the install selection does not reach', async () => {
|
|
// `root` is selected by --workspace-root, which adds no selector to the
|
|
// install's filter arguments.
|
|
const allProjectsGraph = createGraph({ root: [], a: ['b'], b: [] })
|
|
expect(await selectNames({
|
|
filter: ['a', '!b'],
|
|
allProjectsGraph,
|
|
selectedProjectsGraph: pickGraph(allProjectsGraph, ['a', 'root']),
|
|
})).toStrictEqual(['a', 'root'])
|
|
})
|
|
|
|
test('resolves a path selector where the install the gate spawns runs', async () => {
|
|
// The install runs in `dir`, where `!../b` names b.
|
|
const allProjectsGraph = createGraph({ a: ['b'], b: [] })
|
|
expect(await selectNames({
|
|
dir: dirOf('a'),
|
|
filter: ['!../b'],
|
|
allProjectsGraph,
|
|
selectedProjectsGraph: pickGraph(allProjectsGraph, ['a']),
|
|
})).toStrictEqual(['a'])
|
|
})
|
|
|
|
test('returns the selection as it is without a workspace graph', async () => {
|
|
const selectedProjectsGraph = createGraph({ a: ['b'] })
|
|
expect(await selectProjectsToVerify({ selectedProjectsGraph })).toBe(selectedProjectsGraph)
|
|
expect(await selectProjectsToVerify({})).toBeUndefined()
|
|
})
|
|
|
|
test('under dedicated lockfiles with injection, follows link: edges while skipping injected workspace edges', async () => {
|
|
const allProjectsGraph = createGraph({ a: ['b', 'c'], b: [], c: [] })
|
|
allProjectsGraph[dirOf('a')].package.manifest.dependencies = {
|
|
b: 'workspace:*',
|
|
c: 'link:../c',
|
|
}
|
|
expect(await selectNames({
|
|
filter: ['a'],
|
|
sharedWorkspaceLockfile: false,
|
|
injectWorkspacePackages: true,
|
|
allProjectsGraph,
|
|
selectedProjectsGraph: pickGraph(allProjectsGraph, ['a']),
|
|
})).toStrictEqual(['a', 'c'])
|
|
})
|
|
})
|
|
|
|
function dirOf (name: string): ProjectRootDir {
|
|
return path.join(workspaceDir, name) as ProjectRootDir
|
|
}
|
|
|
|
function createGraph (adjacency: Record<string, string[]>): ProjectsGraph {
|
|
return Object.fromEntries(Object.entries(adjacency).map(([name, dependencies]) => [
|
|
dirOf(name),
|
|
{
|
|
dependencies: dependencies.map(dirOf),
|
|
package: { rootDir: dirOf(name), manifest: { name } } as unknown as Project,
|
|
},
|
|
]))
|
|
}
|
|
|
|
function pickGraph (graph: ProjectsGraph, names: string[]): ProjectsGraph {
|
|
return Object.fromEntries(names.map((name) => [dirOf(name), graph[dirOf(name)]]))
|
|
}
|
|
|
|
async function selectNames (opts: Parameters<typeof selectProjectsToVerify>[0]): Promise<string[]> {
|
|
const selected = await selectProjectsToVerify({ workspaceDir, ...opts })
|
|
return Object.keys(selected ?? {}).map((dir) => path.basename(dir)).sort()
|
|
}
|