feat(dlx): add an option to dlx for providing a list of deps that are allowed to run install scripts (#9026)

This commit is contained in:
Zoltan Kochan
2025-02-03 14:46:28 +01:00
committed by GitHub
parent a08a5a9896
commit b5ba5350bf
7 changed files with 124 additions and 29 deletions

View File

@@ -0,0 +1,14 @@
---
"@pnpm/plugin-commands-script-runners": minor
"pnpm": minor
---
Packages executed via `pnpm dlx` and `pnpm create` are allowed to be built (run postinstall scripts) by default.
If the packages executed by `dlx` or `create` have dependencies that have to be built, they should be listed via the `--allow-build` flag. For instance, if you want to run a package called `bundle` that has `esbuild` in dependencies and want to allow `esbuild` to run postinstall scripts, run:
```
pnpm --allow-build=esbuild dlx bundle
```
Related PR: [#9026](https://github.com/pnpm/pnpm/pull/9026).

View File

@@ -33,12 +33,24 @@ export function rcOptionsTypes (): Record<string, unknown> {
export function cliOptionsTypes (): Record<string, unknown> {
return {
...rcOptionsTypes(),
'allow-build': [String, Array],
}
}
export function help (): string {
return renderHelp({
description: 'Creates a project from a `create-*` starter kit.',
descriptionLists: [
{
title: 'Options',
list: [
{
description: 'A list of package names that are allowed to run postinstall scripts during installation',
name: '--allow-build',
},
],
},
],
url: docsUrl('create'),
usages: [
'pnpm create <name>',

View File

@@ -39,6 +39,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
export const cliOptionsTypes = (): Record<string, unknown> => ({
...rcOptionsTypes(),
package: [String, Array],
'allow-build': [String, Array],
})
export function help (): string {
@@ -52,6 +53,10 @@ export function help (): string {
description: 'The package to install before running the command',
name: '--package',
},
{
description: 'A list of package names that are allowed to run postinstall scripts during installation',
name: '--allow-build',
},
{
description: 'Runs the script inside of a shell. Uses /bin/sh on UNIX and \\cmd.exe on Windows.',
name: '--shell-mode',
@@ -69,6 +74,7 @@ export function help (): string {
export type DlxCommandOptions = {
package?: string[]
shellMode?: boolean
allowBuild?: string[]
} & Pick<Config, 'extraBinPaths' | 'registries' | 'reporter' | 'userAgent' | 'cacheDir' | 'dlxCacheMaxAge' | 'useNodeVersion' | 'symlink'> & add.AddCommandOptions
export async function handler (
@@ -80,9 +86,11 @@ export async function handler (
...opts,
authConfig: opts.rawConfig,
})
const resolvedPkgAliases: string[] = []
const resolvedPkgs = await Promise.all(pkgs.map(async (pkg) => {
const { alias, pref } = parseWantedDependency(pkg) || {}
if (alias == null) return pkg
resolvedPkgAliases.push(alias)
const resolved = await resolve({ alias, pref }, {
lockfileDir: opts.lockfileDir ?? opts.dir,
preferredVersions: {},
@@ -95,6 +103,7 @@ export async function handler (
dlxCacheMaxAge: opts.dlxCacheMaxAge,
cacheDir: opts.cacheDir,
registries: opts.registries,
allowBuild: opts.allowBuild ?? [],
})
if (!cacheExists) {
fs.mkdirSync(cachedDir, { recursive: true })
@@ -106,6 +115,11 @@ export async function handler (
dir: cachedDir,
lockfileDir: cachedDir,
rootProjectManifestDir: cachedDir, // This property won't be used as rootProjectManifest will be undefined
rootProjectManifest: {
pnpm: {
onlyBuiltDependencies: [...resolvedPkgAliases, ...(opts.allowBuild ?? [])],
},
},
saveProd: true, // dlx will be looking for the package in the "dependencies" field!
saveDev: false,
saveOptional: false,
@@ -192,6 +206,7 @@ function findCache (pkgs: string[], opts: {
cacheDir: string
dlxCacheMaxAge: number
registries: Record<string, string>
allowBuild: string[]
}): { cacheLink: string, cacheExists: boolean, cachedDir: string } {
const dlxCommandCacheDir = createDlxCommandCacheDir(pkgs, opts)
const cacheLink = path.join(dlxCommandCacheDir, 'pkg')
@@ -208,19 +223,24 @@ function createDlxCommandCacheDir (
opts: {
registries: Record<string, string>
cacheDir: string
allowBuild: string[]
}
): string {
const dlxCacheDir = path.resolve(opts.cacheDir, 'dlx')
const cacheKey = createCacheKey(pkgs, opts.registries)
const cacheKey = createCacheKey(pkgs, opts.registries, opts.allowBuild)
const cachePath = path.join(dlxCacheDir, cacheKey)
fs.mkdirSync(cachePath, { recursive: true })
return cachePath
}
export function createCacheKey (pkgs: string[], registries: Record<string, string>): string {
export function createCacheKey (pkgs: string[], registries: Record<string, string>, allowBuild?: string[]): string {
const sortedPkgs = [...pkgs].sort((a, b) => a.localeCompare(b))
const sortedRegistries = Object.entries(registries).sort(([k1], [k2]) => k1.localeCompare(k2))
const hashStr = JSON.stringify([sortedPkgs, sortedRegistries])
const args: unknown[] = [sortedPkgs, sortedRegistries]
if (allowBuild?.length) {
args.push({ allowBuild: allowBuild.sort((pkg1, pkg2) => pkg1.localeCompare(pkg2)) })
}
const hashStr = JSON.stringify(args)
return createHexHash(hashStr)
}

View File

@@ -296,3 +296,52 @@ test('dlx still saves cache even if execution fails', async () => {
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,
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,
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(['@pnpm.e2e/has-bin-and-needs-build@1.0.0'], DEFAULT_OPTS.registries, allowBuild), '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()
})

View File

@@ -268,7 +268,7 @@ when running add/update with the --workspace option')
const installOpts: Omit<MutateModulesOptions, 'allProjects'> = {
...opts,
...getOptionsFromRootManifest(opts.dir, manifest),
...getOptionsFromRootManifest(opts.dir, (opts.dir === opts.rootProjectManifestDir ? opts.rootProjectManifest ?? manifest : manifest)),
forceHoistPattern,
forcePublicHoistPattern,
// In case installation is done in a multi-package repository

48
pnpm-lock.yaml generated
View File

@@ -55,8 +55,8 @@ catalogs:
specifier: 0.0.0
version: 0.0.0
'@pnpm/registry-mock':
specifier: 3.48.0
version: 3.48.0
specifier: 3.50.0
version: 3.50.0
'@pnpm/semver-diff':
specifier: ^1.1.0
version: 1.1.0
@@ -846,7 +846,7 @@ importers:
version: link:../../pkg-manager/modules-yaml
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -880,7 +880,7 @@ importers:
dependencies:
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store.cafs':
specifier: workspace:*
version: link:../../store/cafs
@@ -955,7 +955,7 @@ importers:
dependencies:
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/worker':
specifier: workspace:*
version: link:../../worker
@@ -1138,7 +1138,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@types/ramda':
specifier: 'catalog:'
version: 0.29.12
@@ -2116,7 +2116,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -2387,7 +2387,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -2532,7 +2532,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-ipc-server':
specifier: workspace:*
version: link:../../__utils__/test-ipc-server
@@ -4181,7 +4181,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -4471,7 +4471,7 @@ importers:
version: link:../../pkg-manifest/read-package-json
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store-path':
specifier: workspace:*
version: link:../../store/store-path
@@ -4747,7 +4747,7 @@ importers:
version: link:../read-projects-context
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store-path':
specifier: workspace:*
version: link:../../store/store-path
@@ -5107,7 +5107,7 @@ importers:
version: 'link:'
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -5321,7 +5321,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -5925,7 +5925,7 @@ importers:
version: link:../pkg-manifest/read-project-manifest
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/run-npm':
specifier: workspace:*
version: link:../exec/run-npm
@@ -6241,7 +6241,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -6365,7 +6365,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-ipc-server':
specifier: workspace:*
version: link:../../__utils__/test-ipc-server
@@ -6964,7 +6964,7 @@ importers:
version: link:../../pkg-manifest/read-package-json
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -7028,7 +7028,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/workspace.filter-packages-from-dir':
specifier: workspace:*
version: link:../../workspace/filter-packages-from-dir
@@ -7113,7 +7113,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -7470,7 +7470,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 3.48.0(encoding@0.1.13)(typanion@3.14.0)
version: 3.50.0(encoding@0.1.13)(typanion@3.14.0)
'@types/archy':
specifier: 'catalog:'
version: 0.0.33
@@ -9187,8 +9187,8 @@ packages:
resolution: {integrity: sha512-LFTWzfJbu6+l86bw/uUAsPU05n1oTqg6jzqyTXYDJPfVclqTfPnHiZoC1nvVvQlE7iVg3bhJ7SXg9IyzK7RWDQ==}
engines: {node: '>=18.12'}
'@pnpm/registry-mock@3.48.0':
resolution: {integrity: sha512-5d5GFyz+DlGe50SLJjMWcEKkzr7EvbKQq71hwcjOUAr3+mPEz7+Ul+wFkw0gJ/3mYjk4pmFZerP8qLdd8SBZPQ==}
'@pnpm/registry-mock@3.50.0':
resolution: {integrity: sha512-/YGi3OCMWXkk8JwUbVOQo5Ws89yA9ub+jnYboAgxUJ84K6a2m3xZiRn1im0zxC7L6F63Xx++ZuGrlwKEWHqf+A==}
engines: {node: '>=10.13'}
hasBin: true
@@ -16116,7 +16116,7 @@ snapshots:
sort-keys: 4.2.0
strip-bom: 4.0.0
'@pnpm/registry-mock@3.48.0(encoding@0.1.13)(typanion@3.14.0)':
'@pnpm/registry-mock@3.50.0(encoding@0.1.13)(typanion@3.14.0)':
dependencies:
anonymous-npm-registry-client: 0.2.0
execa: 5.1.1

View File

@@ -56,7 +56,7 @@ catalog:
"@pnpm/npm-package-arg": ^1.0.0
"@pnpm/os.env.path-extender": ^2.0.0
"@pnpm/patch-package": 0.0.0
"@pnpm/registry-mock": 3.48.0
"@pnpm/registry-mock": 3.50.0
"@pnpm/semver-diff": ^1.1.0
"@pnpm/tabtab": ^0.5.4
"@pnpm/util.lex-comparator": 3.0.0