Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/heavy-brooms-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/exec.build-commands": major
"pnpm": minor
---

Added a new command for printing the list of dependencies with ignored build scripts: `pnpm ignored-builds` [#8963](https://github.com/pnpm/pnpm/pull/8963).
7 changes: 7 additions & 0 deletions .changeset/heavy-brooms-thank2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@pnpm/exec.build-commands": major
"pnpm/default-reporter": minor
"pnpm": minor
---

Added a new command for approving dependencies for running scripts during installation: `pnpm approve-builds` [#8963](https://github.com/pnpm/pnpm/pull/8963).
1 change: 1 addition & 0 deletions .meta-updater/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest,
case '@pnpm/lockfile.types':
scripts = { ...manifest.scripts }
break
case '@pnpm/exec.build-commands':
case '@pnpm/headless':
case '@pnpm/outdated':
case '@pnpm/package-requester':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,7 @@ export function reportSummary (
}
if (ignoredScripts.packageNames && ignoredScripts.packageNames.length > 0) {
msg += EOL
msg += `The following dependencies have build scripts that were ignored: ${Array.from(ignoredScripts.packageNames).sort().join(', ')}.`
msg += EOL
msg += 'To allow the execution of build scripts for these packages, add their names to "pnpm.onlyBuiltDependencies" in your "package.json", then run "pnpm rebuild".'
msg += EOL
msg += 'If you don\'t want to build the package and see this message, add the package to the "pnpm.ignoredBuiltDependencies" list.'
msg += `Ignored build scripts: ${Array.from(ignoredScripts.packageNames).sort().join(', ')}. Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.`
msg += EOL
}
return Rx.of({ msg })
Expand Down
15 changes: 15 additions & 0 deletions exec/build-commands/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @pnpm/exec.build-commands

> Commands for managing dependency builds

[![npm version](https://img.shields.io/npm/v/@pnpm/exec.build-commands.svg)](https://www.npmjs.com/package/@pnpm/exec.build-commands)

## Installation

```sh
pnpm add @pnpm/exec.build-commands
```

## License

MIT
63 changes: 63 additions & 0 deletions exec/build-commands/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@pnpm/exec.build-commands",
"version": "1000.0.0-0",
"description": "Commands for managing dependency builds",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=18.12"
},
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "cross-env PNPM_REGISTRY_MOCK_PORT=7771 jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/exec/build-commands",
"keywords": [
"pnpm10",
"pnpm",
"rebuild"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/exec/build-commands#readme",
"devDependencies": {
"@pnpm/config": "workspace:*",
"@pnpm/exec.build-commands": "workspace:*",
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "catalog:",
"@pnpm/types": "workspace:*",
"@types/ramda": "catalog:",
"load-json-file": "catalog:",
"ramda": "catalog:"
},
"dependencies": {
"@pnpm/config": "workspace:*",
"@pnpm/modules-yaml": "workspace:*",
"@pnpm/plugin-commands-rebuild": "workspace:*",
"@pnpm/prepare-temp-dir": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"chalk": "catalog:",
"enquirer": "catalog:",
"render-help": "catalog:"
},
"peerDependencies": {
"@pnpm/logger": ">=5.1.0 <1001.0.0"
},
"funding": "https://opencollective.com/pnpm",
"exports": {
".": "./lib/index.js"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}
107 changes: 107 additions & 0 deletions exec/build-commands/src/approveBuilds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { type Config } from '@pnpm/config'
import { readProjectManifest } from '@pnpm/read-project-manifest'
import renderHelp from 'render-help'
import { prompt } from 'enquirer'
import chalk from 'chalk'
import { rebuild, type RebuildCommandOpts } from '@pnpm/plugin-commands-rebuild'
import { getAutomaticallyIgnoredBuilds } from './getAutomaticallyIgnoredBuilds'

export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest' | 'rootProjectManifestDir'>

export const commandNames = ['approve-builds']

export function help (): string {
return renderHelp({
description: 'Approve dependencies for running scripts during installation',
usages: [],
})
}

export function cliOptionsTypes (): Record<string, unknown> {
return {}
}

export function rcOptionsTypes (): Record<string, unknown> {
return {}
}

export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOpts): Promise<void> {
if (opts.rootProjectManifest == null) return
const automaticallyIgnoredBuilds = await getAutomaticallyIgnoredBuilds(opts)
if (automaticallyIgnoredBuilds == null) return
const { result } = await prompt({
choices: [...automaticallyIgnoredBuilds],
indicator (state: any, choice: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
return ` ${choice.enabled ? '●' : '○'}`
},
message: 'Choose which packages to build ' +
`(Press ${chalk.cyan('<space>')} to select, ` +
`${chalk.cyan('<a>')} to toggle all, ` +
`${chalk.cyan('<i>')} to invert selection)`,
name: 'result',
pointer: '❯',
result () {
return this.selected
},
styles: {
dark: chalk.reset,
em: chalk.bgBlack.whiteBright,
success: chalk.reset,
},
type: 'multiselect',

// For Vim users (related: https://github.com/enquirer/enquirer/pull/163)
j () {
return this.down()
},
k () {
return this.up()
},
cancel () {
// By default, canceling the prompt via Ctrl+c throws an empty string.
// The custom cancel function prevents that behavior.
// Otherwise, pnpm CLI would print an error and confuse users.
// See related issue: https://github.com/enquirer/enquirer/issues/225
process.exit(0)
},
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
const buildPackages = result.map(({ value }: { value: string }) => value)
const ignoredPackages = automaticallyIgnoredBuilds.filter((automaticallyIgnoredBuild) => !buildPackages.includes(automaticallyIgnoredBuild))
if (ignoredPackages.length) {
if (opts.rootProjectManifest.pnpm?.ignoredBuiltDependencies == null) {
opts.rootProjectManifest.pnpm = {
...opts.rootProjectManifest.pnpm,
ignoredBuiltDependencies: ignoredPackages,
}
} else {
opts.rootProjectManifest.pnpm.ignoredBuiltDependencies.push(...ignoredPackages)
}
}
if (buildPackages.length) {
if (opts.rootProjectManifest.pnpm?.onlyBuiltDependencies == null) {
opts.rootProjectManifest.pnpm = {
...opts.rootProjectManifest.pnpm,
onlyBuiltDependencies: buildPackages,
}
} else {
opts.rootProjectManifest.pnpm.onlyBuiltDependencies.push(...buildPackages)
}
}
if (buildPackages.length) {
const confirmed = await prompt<{ build: boolean }>({
type: 'confirm',
name: 'build',
message: `The next packages will now be built: ${buildPackages.join(', ')}.
Do you approve?`,
initial: false,
})
if (!confirmed.build) {
return
}
}
const { writeProjectManifest } = await readProjectManifest(opts.rootProjectManifestDir)
await writeProjectManifest(opts.rootProjectManifest)
if (buildPackages.length) {
return rebuild.handler(opts, buildPackages)
}
}
11 changes: 11 additions & 0 deletions exec/build-commands/src/getAutomaticallyIgnoredBuilds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import path from 'path'
import { readModulesManifest } from '@pnpm/modules-yaml'
import { type IgnoredBuildsCommandOpts } from './ignoredBuilds'

export async function getAutomaticallyIgnoredBuilds (opts: IgnoredBuildsCommandOpts): Promise<null | string[]> {
const modulesManifest = await readModulesManifest(opts.modulesDir ?? path.join(opts.dir, 'node_modules'))
if (modulesManifest == null) {
return null
}
return modulesManifest?.ignoredBuilds ?? []
}
42 changes: 42 additions & 0 deletions exec/build-commands/src/ignoredBuilds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type Config } from '@pnpm/config'
import renderHelp from 'render-help'
import { getAutomaticallyIgnoredBuilds } from './getAutomaticallyIgnoredBuilds'

export type IgnoredBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest'>

export const commandNames = ['ignored-builds']

export function help (): string {
return renderHelp({
description: 'Print the list of packages with blocked build scripts',
usages: [],
})
}

export function cliOptionsTypes (): Record<string, unknown> {
return {}
}

export function rcOptionsTypes (): Record<string, unknown> {
return {}
}

export async function handler (opts: IgnoredBuildsCommandOpts): Promise<string> {
const ignoredBuiltDependencies = opts.rootProjectManifest?.pnpm?.ignoredBuiltDependencies ?? []
const automaticallyIgnoredBuilds = (await getAutomaticallyIgnoredBuilds(opts))?.filter((automaticallyIgnoredBuild) => !ignoredBuiltDependencies.includes(automaticallyIgnoredBuild))
let output = 'Automatically ignored builds during installation:\n'
if (automaticallyIgnoredBuilds == null) {
output += ' Cannot identify as no node_modules found'
} else if (automaticallyIgnoredBuilds.length === 0) {
output += ' None'
} else {
output += ` ${automaticallyIgnoredBuilds.join('\n ')}
hint: To allow the execution of build scripts for a package, add its name to "pnpm.onlyBuiltDependencies" in your "package.json", then run "pnpm rebuild".
hint: If you don't want to build a package, add it to the "pnpm.ignoredBuiltDependencies" list.`
}
output += '\n'
if (ignoredBuiltDependencies.length) {
output += `\nExplicitly ignored package builds (via pnpm.ignoredBuiltDependencies):\n ${ignoredBuiltDependencies.join('\n ')}\n`
}
return output
}
4 changes: 4 additions & 0 deletions exec/build-commands/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as approveBuilds from './approveBuilds'
import * as ignoredBuilds from './ignoredBuilds'

export { approveBuilds, ignoredBuilds }
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ignoredBuilds lists automatically ignored dependencies 1`] = `
"Automatically ignored builds during installation:
foo
hint: To allow the execution of build scripts for a package, add its name to "pnpm.onlyBuiltDependencies" in your "package.json", then run "pnpm rebuild".
hint: If you don't want to build a package, add it to the "pnpm.ignoredBuiltDependencies" list.
"
`;

exports[`ignoredBuilds lists both automatically and explicitly ignored dependencies 1`] = `
"Automatically ignored builds during installation:
foo
bar
hint: To allow the execution of build scripts for a package, add its name to "pnpm.onlyBuiltDependencies" in your "package.json", then run "pnpm rebuild".
hint: If you don't want to build a package, add it to the "pnpm.ignoredBuiltDependencies" list.

Explicitly ignored package builds (via pnpm.ignoredBuiltDependencies):
qar
zoo
"
`;

exports[`ignoredBuilds lists explicitly ignored dependencies 1`] = `
"Automatically ignored builds during installation:
None

Explicitly ignored package builds (via pnpm.ignoredBuiltDependencies):
bar
"
`;

exports[`ignoredBuilds prints an info message when there is no node_modules 1`] = `
"Automatically ignored builds during installation:
Cannot identify as no node_modules found

Explicitly ignored package builds (via pnpm.ignoredBuiltDependencies):
qar
zoo
"
`;
60 changes: 60 additions & 0 deletions exec/build-commands/test/approveBuilds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from 'fs'
import path from 'path'
import * as enquirer from 'enquirer'
import { approveBuilds } from '@pnpm/exec.build-commands'
import { install } from '@pnpm/plugin-commands-installation'
import { prepare } from '@pnpm/prepare'
import { type ProjectManifest } from '@pnpm/types'
import { getConfig } from '@pnpm/config'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import { sync as loadJsonFile } from 'load-json-file'
import omit from 'ramda/src/omit'

jest.mock('enquirer', () => ({ prompt: jest.fn() }))

// eslint-disable-next-line
const prompt = enquirer.prompt as any

test('approve selected build', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
'@pnpm.e2e/install-script-example': '*',
},
})
const cliOptions = {
argv: [],
dir: process.cwd(),
registry: `http://localhost:${REGISTRY_MOCK_PORT}`,
}
const config = {
...omit(['reporter'], (await getConfig({
cliOptions,
packageManager: { name: 'pnpm', version: '' },
})).config),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
}
await install.handler({ ...config, argv: { original: [] } })

prompt.mockResolvedValueOnce({
result: [
{
value: '@pnpm.e2e/pre-and-postinstall-scripts-example',
},
],
})
prompt.mockResolvedValueOnce({
build: true,
})

await approveBuilds.handler(config)

const manifest = loadJsonFile<ProjectManifest>(path.resolve('package.json'))
expect(manifest.pnpm?.onlyBuiltDependencies).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
expect(manifest.pnpm?.ignoredBuiltDependencies).toStrictEqual(['@pnpm.e2e/install-script-example'])

expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeFalsy()
})
Loading