Skip to content

Commit

Permalink
fix(dlx): dlx should work when pkg name is not same as cmd name (#4682)
Browse files Browse the repository at this point in the history
close #4672
  • Loading branch information
zkochan committed May 7, 2022
1 parent 293b8e3 commit 00c567a
Show file tree
Hide file tree
Showing 28 changed files with 1,167 additions and 783 deletions.
6 changes: 6 additions & 0 deletions .changeset/funny-tips-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-script-runners": patch
"pnpm": patch
---

`pnpm dlx` should work when the bin name of the executed package isn't the same as the package name [#4672](https://github.com/pnpm/pnpm/issues/4672).
5 changes: 5 additions & 0 deletions .changeset/spicy-forks-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-installation": patch
---

Export AddCommandOptions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@commitlint/prompt-cli": "^16.0.0",
"@pnpm/eslint-config": "workspace:*",
"@pnpm/meta-updater": "0.0.6",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@pnpm/tsconfig": "workspace:*",
"@types/jest": "^27.4.0",
"@types/node": "^14.17.32",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"@pnpm/logger": "^4.0.0",
"@pnpm/package-store": "workspace:12.1.13",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@pnpm/store-path": "^5.0.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/fs-extra": "^9.0.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/headless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@pnpm/package-store": "workspace:12.1.13",
"@pnpm/prepare": "workspace:*",
"@pnpm/read-projects-context": "workspace:5.0.19",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@pnpm/store-path": "^5.0.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/fs-extra": "^9.0.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/package-requester/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"@pnpm/logger": "^4.0.0",
"@pnpm/package-requester": "workspace:17.0.1",
"@pnpm/package-store": "workspace:12.1.13",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/normalize-path": "^3.0.0",
"@types/ramda": "0.27.39",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-installation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@pnpm/modules-yaml": "workspace:9.1.1",
"@pnpm/plugin-commands-installation": "workspace:8.4.12",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/is-ci": "^3.0.0",
"@types/proxyquire": "^1.3.28",
Expand Down
18 changes: 10 additions & 8 deletions packages/plugin-commands-installation/src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,17 @@ For options that may be used with `-r`, see "pnpm help recursive"',
})
}

export type AddCommandOptions = InstallCommandOptions & {
allowNew?: boolean
ignoreWorkspaceRootCheck?: boolean
save?: boolean
update?: boolean
useBetaCli?: boolean
workspaceRoot?: boolean
}

export async function handler (
opts: InstallCommandOptions & {
allowNew?: boolean
ignoreWorkspaceRootCheck?: boolean
save?: boolean
update?: boolean
useBetaCli?: boolean
workspaceRoot?: boolean
},
opts: AddCommandOptions,
params: string[]
) {
if (opts.cliOptions['save'] === false) {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-listing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@pnpm/plugin-commands-installation": "workspace:8.4.12",
"@pnpm/plugin-commands-listing": "workspace:4.1.14",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@types/ramda": "0.27.39",
"execa": "npm:safe-execa@^0.1.1",
"strip-ansi": "^6.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-outdated/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@pnpm/plugin-commands-installation": "workspace:8.4.12",
"@pnpm/plugin-commands-outdated": "workspace:5.1.13",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@types/lru-cache": "^5.1.0",
"@types/ramda": "0.27.39",
"@types/wrap-ansi": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-publishing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@pnpm/logger": "^4.0.0",
"@pnpm/plugin-commands-publishing": "workspace:4.5.6",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@types/cross-spawn": "^6.0.2",
"@types/is-ci": "^3.0.0",
"@types/is-windows": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-rebuild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@pnpm/logger": "^4.0.0",
"@pnpm/plugin-commands-rebuild": "workspace:5.4.18",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/ramda": "0.27.39",
"@types/semver": "^7.3.4",
Expand Down
5 changes: 4 additions & 1 deletion packages/plugin-commands-script-runners/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@pnpm/logger": "^4.0.0",
"@pnpm/plugin-commands-script-runners": "workspace:4.6.5",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "2.12.1",
"@pnpm/registry-mock": "2.16.0",
"@types/is-windows": "^1.0.0",
"@types/ramda": "0.27.39",
"is-windows": "^1.0.2",
Expand All @@ -51,6 +51,9 @@
"@pnpm/config": "workspace:13.13.3",
"@pnpm/error": "workspace:2.1.0",
"@pnpm/lifecycle": "workspace:12.1.7",
"@pnpm/package-bins": "workspace:5.0.12",
"@pnpm/plugin-commands-installation": "workspace:8.4.12",
"@pnpm/read-package-json": "workspace:5.0.12",
"@pnpm/read-project-manifest": "workspace:2.0.13",
"@pnpm/sort-packages": "workspace:2.1.8",
"@pnpm/store-path": "^5.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-commands-script-runners/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as dlx from './dlx'

export const commandNames = ['create']

export async function handler (_opts: Record<string, never>, params: string[]) {
export async function handler (_opts: dlx.DlxCommandOptions, params: string[]) {
const [packageName, ...packageArgs] = params
if (packageName === undefined) {
throw new PnpmError(
Expand All @@ -17,7 +17,7 @@ export async function handler (_opts: Record<string, never>, params: string[]) {
}

const createPackageName = convertToCreateName(packageName)
return dlx.handler({}, [createPackageName, ...packageArgs])
return dlx.handler(_opts, [createPackageName, ...packageArgs])
}

export function rcOptionsTypes () {
Expand Down
83 changes: 60 additions & 23 deletions packages/plugin-commands-script-runners/src/dlx.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import { docsUrl } from '@pnpm/cli-utils'
import { OUTPUT_OPTIONS } from '@pnpm/common-cli-options-help'
import { Config } from '@pnpm/config'
import rimraf from '@zkochan/rimraf'
import PnpmError from '@pnpm/error'
import { add } from '@pnpm/plugin-commands-installation'
import { fromDir as readPkgFromDir } from '@pnpm/read-package-json'
import packageBins from '@pnpm/package-bins'
import storePath from '@pnpm/store-path'
import execa from 'execa'
import renderHelp from 'render-help'
import { makeEnv } from './makeEnv'
Expand Down Expand Up @@ -40,16 +43,21 @@ export function help () {
})
}

export type DlxCommandOptions = {
package?: string[]
} & Pick<Config, 'reporter' | 'userAgent'> & add.AddCommandOptions

export async function handler (
opts: {
package?: string[]
} & Pick<Config, 'reporter' | 'userAgent'>,
params: string[]
opts: DlxCommandOptions,
[command, ...args]: string[]
) {
const prefix = path.join(fs.realpathSync(os.tmpdir()), `dlx-${process.pid.toString()}`)
const bins = process.platform === 'win32'
? prefix
: path.join(prefix, 'bin')
const dlxDir = await getDlxDir({
dir: opts.dir,
storeDir: opts.storeDir,
})
const prefix = path.join(dlxDir, `dlx-${process.pid.toString()}`)
const modulesDir = path.join(prefix, 'node_modules')
const binsDir = path.join(modulesDir, '.bin')
fs.mkdirSync(prefix, { recursive: true })
process.on('exit', () => {
try {
Expand All @@ -59,28 +67,57 @@ export async function handler (
})
} catch (err) {}
})
await rimraf(bins)
const pkgs = opts.package ?? params.slice(0, 1)
const pnpmArgs = ['add', ...pkgs, '--global', '--global-dir', prefix, '--dir', prefix]
if (opts.reporter) {
pnpmArgs.push(`--reporter=${opts.reporter}`)
}
await execa('pnpm', pnpmArgs, {
stdio: 'inherit',
})
await execa(versionless(scopeless(params[0])), params.slice(1), {
env: makeEnv({ userAgent: opts.userAgent, prependPaths: [bins] }),
const pkgs = opts.package ?? [command]
const env = makeEnv({ userAgent: opts.userAgent, prependPaths: [binsDir] })
await add.handler({
...opts,
dir: prefix,
bin: binsDir,
}, pkgs)
const binName = opts.package
? command
: await getBinName(modulesDir, versionless(command))
await execa(binName, args, {
env,
stdio: 'inherit',
})
}

async function getBinName (modulesDir: string, pkgName: string): Promise<string> {
const pkgDir = path.join(modulesDir, pkgName)
const manifest = await readPkgFromDir(pkgDir)
const bins = await packageBins(manifest, pkgDir)
if (bins.length === 0) {
throw new PnpmError('DLX_NO_BIN', `No binaries found in ${pkgName}`)
}
if (bins.length === 1) {
return bins[0].name
}
const scopelessPkgName = scopeless(manifest.name)
const defaultBin = bins.find(({ name }) => name === scopelessPkgName)
if (defaultBin) return defaultBin.name
throw new PnpmError('DLX_MULTIPLE_BINS', `Multiple binaries found in ${pkgName}`)
}

function scopeless (pkgName: string) {
if (pkgName.startsWith('@')) {
return pkgName.split('/')[1]
}
return pkgName
}

function versionless (scopelessPkgName: string) {
return scopelessPkgName.split('@')[0]
function versionless (pkgName: string) {
const index = pkgName.indexOf('@', 1)
if (index === -1) return pkgName
return pkgName.substring(0, index)
}

async function getDlxDir (
opts: {
dir: string
storeDir?: string
}
): Promise<string> {
const storeDir = await storePath(opts.dir, opts.storeDir)
return path.join(storeDir, 'tmp')
}
76 changes: 55 additions & 21 deletions packages/plugin-commands-script-runners/test/create.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,99 @@
import PnpmError from '@pnpm/error'
import { create, dlx } from '../src'
import { DLX_DEFAULT_OPTS as DEFAULT_OPTS } from './utils'

jest.mock('../src/dlx', () => ({ handler: jest.fn() }))

beforeEach((dlx.handler as jest.Mock).mockClear)

it('throws an error if called without arguments', async () => {
await expect(create.handler({}, [])).rejects.toThrow(PnpmError)
await expect(create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, [])).rejects.toThrow(PnpmError)
expect(dlx.handler).not.toBeCalled()
})

it(
'appends `create-` to an unscoped package that doesn\'t start with `create-`',
async () => {
await create.handler({}, ['some-app'])
expect(dlx.handler).toBeCalledWith({}, ['create-some-app'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['some-app'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['create-some-app'])

await create.handler({}, ['create_no_dash'])
expect(dlx.handler).toBeCalledWith({}, ['create-create_no_dash'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['create_no_dash'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['create-create_no_dash'])
}
)

it(
'does not append `create-` to an unscoped package that starts with `create-`',
async () => {
await create.handler({}, ['create-some-app'])
expect(dlx.handler).toBeCalledWith({}, ['create-some-app'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['create-some-app'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['create-some-app'])

await create.handler({}, ['create-'])
expect(dlx.handler).toBeCalledWith({}, ['create-'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['create-'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['create-'])
}
)

it(
'appends `create-` to a scoped package that doesn\'t start with `create-`',
async () => {
await create.handler({}, ['@scope/some-app'])
expect(dlx.handler).toBeCalledWith({}, ['@scope/create-some-app'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['@scope/some-app'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['@scope/create-some-app'])

await create.handler({}, ['@scope/create_no_dash'])
expect(dlx.handler).toBeCalledWith({}, ['@scope/create-create_no_dash'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['@scope/create_no_dash'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['@scope/create-create_no_dash'])
}
)

it(
'does not append `create-` to a scoped package that starts with `create-`',
async () => {
await create.handler({}, ['@scope/create-some-app'])
expect(dlx.handler).toBeCalledWith({}, ['@scope/create-some-app'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['@scope/create-some-app'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['@scope/create-some-app'])

await create.handler({}, ['@scope/create-'])
expect(dlx.handler).toBeCalledWith({}, ['@scope/create-'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['@scope/create-'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['@scope/create-'])
}
)

it('infers a package name from a plain scope', async () => {
await create.handler({}, ['@scope'])
expect(dlx.handler).toBeCalledWith({}, ['@scope/create'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['@scope'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['@scope/create'])
})

it('passes the remaining arguments to `dlx`', async () => {
await create.handler({}, ['some-app', 'directory/', '--silent'])
expect(dlx.handler).toBeCalledWith({}, ['create-some-app', 'directory/', '--silent'])
await create.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, ['some-app', 'directory/', '--silent'])
expect(dlx.handler).toBeCalledWith(expect.anything(), ['create-some-app', 'directory/', '--silent'])
})

0 comments on commit 00c567a

Please sign in to comment.