Files
pnpm/pnpm11/installing/commands/test/importRecursive.ts
Zoltan Kochan 430d892741 feat(pnpr)!: serve every ecosystem under its own prefix (#17087)
pnpr served npm at the server root when npm was the only configured
ecosystem and moved it under /npm/ as soon as a second ecosystem was
added. Adding an ecosystem therefore broke every .npmrc registry URL,
every tarball URL already written into a lockfile, named-registry URLs,
and OIDC workload publish paths, and /npm meant the package `npm` on one
server and the ecosystem prefix on another. This settles section A1 of
pnpm/tasks#131: every ecosystem is always prefixed.

- Ecosystem::base_path is now independent of the configuration. The
  per-configuration Registries::base_path and is_only_ecosystem are gone,
  and the routes, the resolver's tarball URLs, OIDC workload paths and
  pnpr's own-origin route classification all use it.
- The default image registry still answers at /v2/ on the host root; an
  image client cannot be pointed at a prefix. The registry directory keeps
  its `prefixed` and `namedPrefixed` flags for the web UI.
- The read-only token allowlist matched the npm audit endpoints at the
  root only, so read-only tokens were already refused for audits on
  multi-ecosystem servers. It now matches them under the npm prefix.
- /metrics no longer switches between Prometheus metrics and the npm
  package `metrics` on the Accept header; the package is /npm/metrics.
- The pnpm v11 and v12 test harnesses, the registry-mock crate and the
  integrated benchmark address the fixture registry at /npm/ and keep the
  server origin for the pnpr protocol endpoints. The benchmark asks each
  revision mock's registry directory whether npm is prefixed, so it can
  still compare against a pnpr revision from before this change.
2026-10-18 16:55:55 +02:00

199 lines
7.5 KiB
TypeScript

/// <reference path="../../../__typings__/index.d.ts" />
import fs from 'node:fs'
import path from 'node:path'
import { expect, test } from '@jest/globals'
import { assertProject } from '@pnpm/assert-project'
import { importCommand } from '@pnpm/installing.commands'
import { prepareEmpty } from '@pnpm/prepare'
import { fixtures } from '@pnpm/test-fixtures'
import { REGISTRY_MOCK_PORT } from '@pnpm/testing.registry-mock'
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
import { temporaryDirectory } from 'tempy'
const testFixtures = fixtures(import.meta.dirname)
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}/npm/`
const TMP = temporaryDirectory()
const DEFAULT_OPTS = {
ca: undefined,
cacheDir: path.join(TMP, 'cache'),
cert: undefined,
fetchRetries: 2,
fetchRetryFactor: 90,
fetchRetryMaxtimeout: 90,
fetchRetryMintimeout: 10,
httpsProxy: undefined,
key: undefined,
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
minimumReleaseAge: 0,
networkConcurrency: 16,
offline: false,
preferWorkspacePackages: true,
proxy: undefined,
pnpmHomeDir: '',
configByUri: {},
registriesByScope: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',
storeDir: path.join(TMP, 'store'),
strictSsl: false,
userAgent: 'pnpm',
userConfig: {},
useRunningStoreServer: false,
useStoreServer: false,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}
test('import from shared yarn.lock of monorepo', async () => {
testFixtures.prepare('workspace-has-shared-yarn-lock')
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), [])
await importCommand.handler({
...DEFAULT_OPTS,
allProjects: allProjects as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- filtered projects lack the buildIndex that the handler's allProjects type requires
allProjectsGraph,
selectedProjectsGraph,
workspaceDir: process.cwd(),
lockfileDir: process.cwd(),
dir: process.cwd(),
resolutionMode: 'highest', // TODO: this should work with the default resolution mode (TODOv8)
}, [])
const project = assertProject(process.cwd())
const lockfile = project.readLockfile()
expect(lockfile.packages).toHaveProperty(['is-positive@1.0.0'])
expect(lockfile.packages).toHaveProperty(['is-negative@1.0.1'])
// node_modules is not created
project.hasNot('is-positive')
project.hasNot('is-negative')
})
test('import from shared package-lock.json of monorepo', async () => {
testFixtures.prepare('workspace-has-shared-package-lock-json')
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), [])
await importCommand.handler({
...DEFAULT_OPTS,
allProjects: allProjects as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- filtered projects lack the buildIndex that the handler's allProjects type requires
allProjectsGraph,
selectedProjectsGraph,
workspaceDir: process.cwd(),
lockfileDir: process.cwd(),
dir: process.cwd(),
resolutionMode: 'highest', // TODO: this should work with the default resolution mode (TODOv8)
}, [])
const project = assertProject(process.cwd())
const lockfile = project.readLockfile()
expect(lockfile.packages).toHaveProperty(['is-positive@1.0.0'])
expect(lockfile.packages).toHaveProperty(['is-negative@1.0.1'])
// node_modules is not created
project.hasNot('is-positive')
project.hasNot('is-negative')
})
test('import from shared npm-shrinkwrap.json of monorepo', async () => {
testFixtures.prepare('workspace-has-shared-npm-shrinkwrap-json')
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), [])
await importCommand.handler({
...DEFAULT_OPTS,
allProjects: allProjects as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- filtered projects lack the buildIndex that the handler's allProjects type requires
allProjectsGraph,
selectedProjectsGraph,
workspaceDir: process.cwd(),
lockfileDir: process.cwd(),
dir: process.cwd(),
resolutionMode: 'highest', // TODO: this should work with the default resolution mode (TODOv8)
}, [])
const project = assertProject(process.cwd())
const lockfile = project.readLockfile()
expect(lockfile.packages).toHaveProperty(['is-positive@1.0.0'])
expect(lockfile.packages).toHaveProperty(['is-negative@1.0.1'])
// node_modules is not created
project.hasNot('is-positive')
project.hasNot('is-negative')
})
test('import keeps the root project on the version pinned by yarn.lock when another project allows a newer one', async () => {
prepareEmpty()
fs.writeFileSync('pnpm-workspace.yaml', 'packages:\n - packages/*\n')
fs.writeFileSync('package.json', JSON.stringify({
name: 'root',
version: '1.0.0',
dependencies: { '@pnpm.e2e/bravo-dep': '^1.0.0' },
}))
fs.writeFileSync('yarn.lock', `# yarn lockfile v1
"@pnpm.e2e/bravo-dep@^1.0.0":
version "1.0.0"
`)
fs.mkdirSync('packages/foo', { recursive: true })
fs.writeFileSync('packages/foo/package.json', JSON.stringify({
name: 'foo',
version: '1.0.0',
dependencies: { '@pnpm.e2e/bravo-dep': '^1.0.1' },
}))
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), [])
await importCommand.handler({
...DEFAULT_OPTS,
allProjects: allProjects as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- filtered projects lack the buildIndex that the handler's allProjects type requires
allProjectsGraph,
selectedProjectsGraph,
workspaceDir: process.cwd(),
lockfileDir: process.cwd(),
dir: process.cwd(),
}, [])
const { importers } = assertProject(process.cwd()).readLockfile()
expect(importers['.'].dependencies?.['@pnpm.e2e/bravo-dep'].version).toBe('1.0.0')
expect(importers['packages/foo'].dependencies?.['@pnpm.e2e/bravo-dep'].version).toBe('1.1.0')
})
test('import keeps the version pinned by a nested yarn.lock', async () => {
prepareEmpty()
fs.writeFileSync('pnpm-workspace.yaml', 'packages:\n - packages/*\n')
fs.writeFileSync('package.json', JSON.stringify({
name: 'root',
version: '1.0.0',
dependencies: { '@pnpm.e2e/bravo-dep': '^1.0.0' },
}))
fs.writeFileSync('yarn.lock', `# yarn lockfile v1
"@pnpm.e2e/bravo-dep@^1.0.0":
version "1.0.0"
`)
fs.mkdirSync('packages/foo', { recursive: true })
fs.writeFileSync('packages/foo/package.json', JSON.stringify({
name: 'foo',
version: '1.0.0',
dependencies: { '@pnpm.e2e/bravo-dep': '^1.0.1' },
}))
fs.writeFileSync('packages/foo/yarn.lock', `# yarn lockfile v1
"@pnpm.e2e/bravo-dep@^1.0.1":
version "1.0.1"
`)
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), [])
await importCommand.handler({
...DEFAULT_OPTS,
allProjects: allProjects as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- filtered projects lack the buildIndex that the handler's allProjects type requires
allProjectsGraph,
selectedProjectsGraph,
workspaceDir: process.cwd(),
lockfileDir: process.cwd(),
dir: process.cwd(),
}, [])
const { importers } = assertProject(process.cwd()).readLockfile()
expect(importers['.'].dependencies?.['@pnpm.e2e/bravo-dep'].version).toBe('1.0.0')
expect(importers['packages/foo'].dependencies?.['@pnpm.e2e/bravo-dep'].version).toBe('1.0.1')
})