Files
pnpm/exec/commands/test/dlx.e2e.ts
Zoltan Kochan 4a36b9a110 refactor: rename internal packages to @pnpm/<domain>.<leaf> convention (#10997)
## Summary

Rename all internal packages so their npm names follow the `@pnpm/<domain>.<leaf>` convention, matching their directory structure. Also rename directories to remove redundancy and improve clarity.

### Bulk rename (94 packages)

All `@pnpm/` packages now derive their name from their directory path using dot-separated segments. Exceptions: `packages/`, `__utils__/`, and `pnpm/artifacts/` keep leaf names only.

### Directory renames (removing redundant prefixes)

- `cli/cli-meta` → `cli/meta`, `cli/cli-utils` → `cli/utils`
- `config/config` → `config/reader`, `config/config-writer` → `config/writer`
- `fetching/fetching-types` → `fetching/types`
- `lockfile/lockfile-to-pnp` → `lockfile/to-pnp`
- `store/store-connection-manager` → `store/connection-manager`
- `store/store-controller-types` → `store/controller-types`
- `store/store-path` → `store/path`

### Targeted renames (clarity improvements)

- `deps/dependency-path` → `deps/path` (`@pnpm/deps.path`)
- `deps/calc-dep-state` → `deps/graph-hasher` (`@pnpm/deps.graph-hasher`)
- `deps/inspection/dependencies-hierarchy` → `deps/inspection/tree-builder` (`@pnpm/deps.inspection.tree-builder`)
- `bins/link-bins` → `bins/linker`, `bins/remove-bins` → `bins/remover`, `bins/package-bins` → `bins/resolver`
- `installing/get-context` → `installing/context`
- `store/package-store` → `store/controller`
- `pkg-manifest/manifest-utils` → `pkg-manifest/utils`

### Manifest reader/writer renames

- `workspace/read-project-manifest` → `workspace/project-manifest-reader` (`@pnpm/workspace.project-manifest-reader`)
- `workspace/write-project-manifest` → `workspace/project-manifest-writer` (`@pnpm/workspace.project-manifest-writer`)
- `workspace/read-manifest` → `workspace/workspace-manifest-reader` (`@pnpm/workspace.workspace-manifest-reader`)
- `workspace/manifest-writer` → `workspace/workspace-manifest-writer` (`@pnpm/workspace.workspace-manifest-writer`)

### Workspace package renames

- `workspace/find-packages` → `workspace/projects-reader`
- `workspace/find-workspace-dir` → `workspace/root-finder`
- `workspace/resolve-workspace-range` → `workspace/range-resolver`
- `workspace/filter-packages-from-dir` merged into `workspace/filter-workspace-packages` → `workspace/projects-filter`

### Domain moves

- `pkg-manifest/read-project-manifest` → `workspace/project-manifest-reader`
- `pkg-manifest/write-project-manifest` → `workspace/project-manifest-writer`
- `pkg-manifest/exportable-manifest` → `releasing/exportable-manifest`

### Scope

- 1206 files changed
- Updated: package.json names/deps, TypeScript imports, tsconfig references, changeset files, renovate.json, test fixtures, import ordering
2026-03-17 21:50:40 +01:00

446 lines
14 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import { jest } from '@jest/globals'
import { prepareEmpty } from '@pnpm/prepare'
import { DLX_DEFAULT_OPTS as DEFAULT_OPTS } from './utils/index.js'
const { getSystemNodeVersion: originalGetSystemNodeVersion } = await import('@pnpm/engine.runtime.system-node-version')
jest.unstable_mockModule('@pnpm/engine.runtime.system-node-version', () => ({
getSystemNodeVersion: jest.fn(originalGetSystemNodeVersion),
}))
const { add: originalAdd } = await import('@pnpm/installing.commands')
jest.unstable_mockModule('@pnpm/installing.commands', () => ({
add: {
handler: jest.fn(originalAdd.handler),
},
}))
const systemNodeVersion = await import('@pnpm/engine.runtime.system-node-version')
const { add } = await import('@pnpm/installing.commands')
const { dlx } = await import('@pnpm/exec.commands')
const testOnWindowsOnly = process.platform === 'win32' ? test : test.skip
function sanitizeDlxCacheComponent (cacheName: string): string {
if (cacheName === 'pkg') return cacheName
const segments = cacheName.split('-')
if (segments.length !== 2) {
throw new Error(`Unexpected name: ${cacheName}`)
}
const [date, pid] = segments
if (!/[0-9a-f]+/.test(date) && !/[0-9a-f]+/.test(pid)) {
throw new Error(`Name ${cacheName} doesn't end with 2 hex numbers`)
}
return '***********-*****'
}
const createCacheKey = (...packages: string[]): string => dlx.createCacheKey({
packages,
registries: DEFAULT_OPTS.registries,
supportedArchitectures: DEFAULT_OPTS.supportedArchitectures,
})
function verifyDlxCache (cacheName: string): void {
expect(
fs.readdirSync(path.resolve('cache', 'dlx', cacheName))
.map(sanitizeDlxCacheComponent)
.sort()
).toStrictEqual([
'pkg',
'***********-*****',
].sort())
verifyDlxCacheLink(cacheName)
}
function verifyDlxCacheLink (cacheName: string): void {
expect(
fs.readdirSync(path.resolve('cache', 'dlx', cacheName, 'pkg'))
.sort()
).toStrictEqual([
'node_modules',
'package.json',
'pnpm-lock.yaml',
].sort())
expect(
path.dirname(fs.realpathSync(path.resolve('cache', 'dlx', cacheName, 'pkg')))
).toBe(path.resolve('cache', 'dlx', cacheName))
}
afterEach(() => {
jest.restoreAllMocks()
})
test('dlx', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
}, ['shx@0.3.4', 'touch', 'foo'])
expect(fs.existsSync('foo')).toBeTruthy()
})
test('dlx install from git', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
allowBuild: ['shx'],
}, ['shelljs/shx#0dcbb9d1022037268959f8b706e0f06a6fd43fde', 'touch', 'foo'])
expect(fs.existsSync('foo')).toBeTruthy()
})
test('dlx should work when the package name differs from the bin name', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
}, ['@pnpm.e2e/touch-file-one-bin'])
expect(fs.existsSync('touch.txt')).toBeTruthy()
})
test('dlx should fail when the installed package has many commands and none equals the package name', async () => {
prepareEmpty()
await expect(
dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
}, ['@pnpm.e2e/touch-file-many-bins'])
).rejects.toThrow('Could not determine executable to run. @pnpm.e2e/touch-file-many-bins has multiple binaries: t, tt')
})
test('dlx should not fail when the installed package has many commands and one equals the package name', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
}, ['@pnpm.e2e/touch-file-good-bin-name'])
expect(fs.existsSync('touch.txt')).toBeTruthy()
})
test('dlx --package <pkg1> [--package <pkg2>]', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
package: [
'@pnpm.e2e/for-testing-pnpm-dlx',
'is-positive',
],
}, ['foo'])
expect(fs.existsSync('foo')).toBeTruthy()
})
test('dlx should fail when the package has no bins', async () => {
prepareEmpty()
await expect(
dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
}, ['is-positive'])
).rejects.toThrow(/No binaries found in is-positive/)
})
test('dlx should work in shell mode', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
package: [
'is-positive',
],
shellMode: true,
}, ['echo "some text" > foo'])
expect(fs.existsSync('foo')).toBeTruthy()
})
test('dlx should work when symlink=false', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
symlink: false,
}, ['@pnpm.e2e/touch-file-good-bin-name'])
expect(fs.existsSync('touch.txt')).toBeTruthy()
})
test('dlx should return a non-zero exit code when the underlying script fails', async () => {
prepareEmpty()
const { exitCode } = await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
package: [
'touch@3.1.0',
],
}, ['nodetouch', '--bad-option'])
expect(exitCode).toBe(1)
})
testOnWindowsOnly('dlx should work when running in the root of a Windows Drive', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: 'C:\\',
storeDir: path.resolve('store'),
}, ['cowsay', 'hello'])
})
test('dlx with cache', async () => {
prepareEmpty()
const spy = jest.mocked(add.handler)
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['shx@0.3.4', 'touch', 'foo'])
expect(fs.existsSync('foo')).toBe(true)
verifyDlxCache(createCacheKey('shx@0.3.4'))
expect(spy).toHaveBeenCalled()
spy.mockClear()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['shx@0.3.4', 'touch', 'bar'])
expect(fs.existsSync('bar')).toBe(true)
verifyDlxCache(createCacheKey('shx@0.3.4'))
expect(spy).not.toHaveBeenCalled()
spy.mockClear()
// Specify a node version that shx@0.3.4 does not support. Currently supported versions are >= 6.
jest.mocked(systemNodeVersion.getSystemNodeVersion).mockReturnValue('v4.0.0')
await expect(dlx.handler({
...DEFAULT_OPTS,
engineStrict: true,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['shx@0.3.4', 'touch', 'foo'])).rejects.toThrow('Unsupported engine for')
jest.mocked(systemNodeVersion.getSystemNodeVersion).mockImplementation(originalGetSystemNodeVersion)
})
test('dlx does not reuse expired cache', async () => {
prepareEmpty()
const now = new Date()
// first execution to initialize the cache
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['shx@0.3.4', 'echo', 'hello world'])
verifyDlxCache(createCacheKey('shx@0.3.4'))
// change the date attributes of the cache to 30 minutes older than now
const newDate = new Date(now.getTime() - 30 * 60_000)
fs.lutimesSync(path.resolve('cache', 'dlx', createCacheKey('shx@0.3.4'), 'pkg'), newDate, newDate)
const spy = jest.mocked(add.handler)
// main dlx execution
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: 10, // 10 minutes should make 30 minutes old cache expired
}, ['shx@0.3.4', 'touch', 'BAR'])
expect(fs.existsSync('BAR')).toBe(true)
expect(spy).toHaveBeenCalledWith(expect.anything(), ['shx@0.3.4'])
spy.mockClear()
expect(
fs.readdirSync(path.resolve('cache', 'dlx', createCacheKey('shx@0.3.4')))
.map(sanitizeDlxCacheComponent)
.sort()
).toStrictEqual([
'pkg',
'***********-*****',
'***********-*****',
].sort())
verifyDlxCacheLink(createCacheKey('shx@0.3.4'))
})
test('dlx still saves cache even if execution fails', async () => {
prepareEmpty()
fs.writeFileSync(path.resolve('not-a-dir'), 'to make `shx mkdir` fails')
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['shx@0.3.4', 'mkdir', path.resolve('not-a-dir')])
expect(fs.readFileSync(path.resolve('not-a-dir'), 'utf-8')).toEqual(expect.anything())
verifyDlxCache(createCacheKey('shx@0.3.4'))
})
test('dlx builds the package that is executed', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
enableGlobalVirtualStore: false,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['@pnpm.e2e/has-bin-and-needs-build'])
// The command file of the above package is created by a postinstall script
// so if it doesn't fail it means that it was built.
const dlxCacheDir = path.resolve('cache', 'dlx', createCacheKey('@pnpm.e2e/has-bin-and-needs-build@1.0.0'), 'pkg')
const builtPkg1Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+pre-and-postinstall-scripts-example@1.0.0/node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example')
expect(fs.existsSync(path.join(builtPkg1Path, 'package.json'))).toBeTruthy()
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-preinstall.js'))).toBeFalsy()
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-postinstall.js'))).toBeFalsy()
const builtPkg2Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+install-script-example@1.0.0/node_modules/@pnpm.e2e/install-script-example')
expect(fs.existsSync(path.join(builtPkg2Path, 'package.json'))).toBeTruthy()
expect(fs.existsSync(path.join(builtPkg2Path, 'generated-by-install.js'))).toBeFalsy()
})
test('dlx builds the packages passed via --allow-build', async () => {
prepareEmpty()
const allowBuild = ['@pnpm.e2e/install-script-example']
await dlx.handler({
...DEFAULT_OPTS,
enableGlobalVirtualStore: false,
allowBuild,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
}, ['@pnpm.e2e/has-bin-and-needs-build'])
const dlxCacheDir = path.resolve('cache', 'dlx', dlx.createCacheKey({
packages: ['@pnpm.e2e/has-bin-and-needs-build@1.0.0'],
allowBuild,
registries: DEFAULT_OPTS.registries,
supportedArchitectures: DEFAULT_OPTS.supportedArchitectures,
}), 'pkg')
const builtPkg1Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+pre-and-postinstall-scripts-example@1.0.0/node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example')
expect(fs.existsSync(path.join(builtPkg1Path, 'package.json'))).toBeTruthy()
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-preinstall.js'))).toBeFalsy()
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-postinstall.js'))).toBeFalsy()
const builtPkg2Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+install-script-example@1.0.0/node_modules/@pnpm.e2e/install-script-example')
expect(fs.existsSync(path.join(builtPkg2Path, 'package.json'))).toBeTruthy()
expect(fs.existsSync(path.join(builtPkg2Path, 'generated-by-install.js'))).toBeTruthy()
})
test('dlx should fail when the requested package does not meet the minimum age requirement', async () => {
prepareEmpty()
await expect(
dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
minimumReleaseAge: 60 * 24 * 10000,
registries: {
// We must use the public registry instead of verdaccio here
// because verdaccio has the "times" field in the abbreviated metadata too.
default: 'https://registry.npmjs.org/',
},
}, ['shx@0.3.4'])
).rejects.toThrow(/Version 0\.3\.4 \(released .+\) of shx does not meet the minimumReleaseAge constraint/)
})
test('dlx should respect minimumReleaseAgeExclude', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
minimumReleaseAge: 60 * 24 * 10000,
minimumReleaseAgeExclude: ['*'],
registries: {
// We must use the public registry instead of verdaccio here
// because verdaccio has the "times" field in the abbreviated metadata too.
default: 'https://registry.npmjs.org/',
},
}, ['shx@0.3.4', 'touch', 'foo'])
expect(fs.existsSync('foo')).toBeTruthy()
})
test('dlx with catalog', async () => {
prepareEmpty()
await dlx.handler({
...DEFAULT_OPTS,
dir: path.resolve('project'),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
dlxCacheMaxAge: Infinity,
catalogs: {
default: {
shx: '^0.3.4',
},
},
}, ['shx@catalog:'])
verifyDlxCache(createCacheKey('shx@0.3.4'))
})