mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-14 11:35:56 -04:00
* refactor(config): split Config interface into settings + runtime context
Create ConfigContext for runtime state (hooks, finders, workspace graph,
CLI metadata) and keep Config for user-facing settings only. Functions
use Pick<Config, ...> & Pick<ConfigContext, ...> to express which fields
they need from each interface.
getConfig() now returns { config, context, warnings }. The CLI wrapper
returns { config, context } and spreads both when calling command
handlers (to be refactored to separate params in follow-up PRs).
Closes #11195
* fix: address review feedback
- Initialize cliOptions on pnpmConfig so context.cliOptions is never undefined
- Move rootProjectManifestDir assignment before ignoreLocalSettings guard
- Add allProjectsGraph to INTERNAL_CONFIG_KEYS
* refactor: remove INTERNAL_CONFIG_KEYS from configToRecord
configToRecord now accepts Config and ConfigContext separately, so
context fields are never in scope. Only auth-related Config fields
(authConfig, authInfos, sslConfigs) need filtering.
* refactor: eliminate INTERNAL_CONFIG_KEYS from configToRecord
configToRecord now receives the clean Config object and explicitlySetKeys
separately (via opts.config and opts.context), so context fields are
never in scope. main.ts passes the original split objects alongside
the spread for command handlers that need them.
* fix: spelling
* fix: import sorting
* fix: --config.xxx nconf overrides conflicting with --config CLI flag
When `pnpm add` registers `config: Boolean`, nopt captures
--config.xxx=yyy as the --config flag value instead of treating it
as a nconf-style config override. Fix by extracting --config.xxx args
before nopt parsing and re-parsing them separately.
Also rename the split config/context properties on the command opts
object to _config/_context to avoid clashing with the --config CLI option.
443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
import path from 'node:path'
|
|
|
|
import type { CompletionFunc } from '@pnpm/cli.command'
|
|
import { FILTERING, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
|
|
import {
|
|
docsUrl,
|
|
readProjectManifestOnly,
|
|
tryReadProjectManifest,
|
|
} from '@pnpm/cli.utils'
|
|
import { type Config, type ConfigContext, getWorkspaceConcurrency, types as allTypes } from '@pnpm/config.reader'
|
|
import type { CheckDepsStatusOptions } from '@pnpm/deps.status'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import {
|
|
makeNodeRequireOption,
|
|
runLifecycleHook,
|
|
type RunLifecycleHookOptions,
|
|
} from '@pnpm/exec.lifecycle'
|
|
import type { PackageScripts, ProjectManifest } from '@pnpm/types'
|
|
import { syncInjectedDeps } from '@pnpm/workspace.injected-deps-syncer'
|
|
import pLimit from 'p-limit'
|
|
import { pick } from 'ramda'
|
|
import { realpathMissing } from 'realpath-missing'
|
|
import { renderHelp } from 'render-help'
|
|
|
|
import { buildCommandNotFoundHint } from './buildCommandNotFoundHint.js'
|
|
import { handler as exec } from './exec.js'
|
|
import { existsInDir } from './existsInDir.js'
|
|
import { throwOrFilterHiddenScripts } from './hiddenScripts.js'
|
|
import { runDepsStatusCheck } from './runDepsStatusCheck.js'
|
|
import { getSpecifiedScripts as getSpecifiedScriptWithoutStartCommand, type RecursiveRunOpts, runRecursive } from './runRecursive.js'
|
|
|
|
export const IF_PRESENT_OPTION: Record<string, unknown> = {
|
|
'if-present': Boolean,
|
|
}
|
|
|
|
export interface DescriptionItem {
|
|
shortAlias?: string
|
|
name: string
|
|
description?: string
|
|
}
|
|
|
|
export const IF_PRESENT_OPTION_HELP: DescriptionItem = {
|
|
description: 'Avoid exiting with a non-zero exit code when the script is undefined',
|
|
name: '--if-present',
|
|
}
|
|
|
|
export const PARALLEL_OPTION_HELP: DescriptionItem = {
|
|
description: 'Completely disregard concurrency and topological sorting, \
|
|
running a given script immediately in all matching packages \
|
|
with prefixed streaming output. This is the preferred flag \
|
|
for long-running processes such as watch run over many packages.',
|
|
name: '--parallel',
|
|
}
|
|
|
|
export const RESUME_FROM_OPTION_HELP: DescriptionItem = {
|
|
description: 'Command executed from given package',
|
|
name: '--resume-from',
|
|
}
|
|
|
|
export const SEQUENTIAL_OPTION_HELP: DescriptionItem = {
|
|
description: 'Run the specified scripts one by one',
|
|
name: '--sequential',
|
|
}
|
|
|
|
export const REPORT_SUMMARY_OPTION_HELP: DescriptionItem = {
|
|
description: 'Save the execution results of every package to "pnpm-exec-summary.json". Useful to inspect the execution time and status of each package.',
|
|
name: '--report-summary',
|
|
}
|
|
|
|
export const REPORTER_HIDE_PREFIX_HELP: DescriptionItem = {
|
|
description: 'Hide project name prefix from output of running scripts. Useful when running in CI like GitHub Actions and the output from a script may create an annotation.',
|
|
name: '--reporter-hide-prefix',
|
|
}
|
|
|
|
export const shorthands: Record<string, string[]> = {
|
|
parallel: [
|
|
'--workspace-concurrency=Infinity',
|
|
'--no-sort',
|
|
'--stream',
|
|
'--recursive',
|
|
],
|
|
sequential: [
|
|
'--workspace-concurrency=1',
|
|
],
|
|
}
|
|
|
|
export function rcOptionsTypes (): Record<string, unknown> {
|
|
return {
|
|
...pick([
|
|
'npm-path',
|
|
], allTypes),
|
|
}
|
|
}
|
|
|
|
export function cliOptionsTypes (): Record<string, unknown> {
|
|
return {
|
|
...pick([
|
|
'bail',
|
|
'sort',
|
|
'unsafe-perm',
|
|
'workspace-concurrency',
|
|
'scripts-prepend-node-path',
|
|
], allTypes),
|
|
...IF_PRESENT_OPTION,
|
|
recursive: Boolean,
|
|
reverse: Boolean,
|
|
'resume-from': String,
|
|
'report-summary': Boolean,
|
|
'reporter-hide-prefix': Boolean,
|
|
}
|
|
}
|
|
|
|
export const completion: CompletionFunc = async (cliOpts, params) => {
|
|
if (params.length > 0) {
|
|
return []
|
|
}
|
|
const manifest = await readProjectManifestOnly(cliOpts.dir as string ?? process.cwd(), cliOpts)
|
|
return Object.keys(manifest.scripts ?? {}).map((name) => ({ name }))
|
|
}
|
|
|
|
export const commandNames = ['run', 'run-script']
|
|
|
|
export function help (): string {
|
|
return renderHelp({
|
|
aliases: ['run-script'],
|
|
description: 'Runs a defined package script.',
|
|
descriptionLists: [
|
|
{
|
|
title: 'Options',
|
|
|
|
list: [
|
|
{
|
|
description: 'Run the defined package script in every package found in subdirectories \
|
|
or every workspace package, when executed inside a workspace. \
|
|
For options that may be used with `-r`, see "pnpm help recursive"',
|
|
name: '--recursive',
|
|
shortAlias: '-r',
|
|
},
|
|
{
|
|
description: 'The command will exit with a 0 exit code even if the script fails',
|
|
name: '--no-bail',
|
|
},
|
|
IF_PRESENT_OPTION_HELP,
|
|
PARALLEL_OPTION_HELP,
|
|
RESUME_FROM_OPTION_HELP,
|
|
...UNIVERSAL_OPTIONS,
|
|
SEQUENTIAL_OPTION_HELP,
|
|
REPORT_SUMMARY_OPTION_HELP,
|
|
REPORTER_HIDE_PREFIX_HELP,
|
|
],
|
|
},
|
|
FILTERING,
|
|
],
|
|
url: docsUrl('run'),
|
|
usages: ['pnpm run <command> [<args>...]'],
|
|
})
|
|
}
|
|
|
|
export type RunOpts =
|
|
& Omit<RecursiveRunOpts, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir'>
|
|
& { recursive?: boolean }
|
|
& Pick<Config,
|
|
| 'bin'
|
|
| 'verifyDepsBeforeRun'
|
|
| 'dir'
|
|
| 'enablePrePostScripts'
|
|
| 'engineStrict'
|
|
| 'extraBinPaths'
|
|
| 'extraEnv'
|
|
| 'nodeOptions'
|
|
| 'pnpmHomeDir'
|
|
| 'reporter'
|
|
| 'scriptShell'
|
|
| 'scriptsPrependNodePath'
|
|
| 'shellEmulator'
|
|
| 'syncInjectedDepsAfterScripts'
|
|
| 'userAgent'
|
|
>
|
|
& Pick<ConfigContext, 'cliOptions'>
|
|
& (
|
|
| { recursive?: false } & Partial<Pick<ConfigContext, 'allProjects' | 'selectedProjectsGraph'> & Pick<Config, 'workspaceDir'>>
|
|
| { recursive: true } & Required<Pick<ConfigContext, 'allProjects' | 'selectedProjectsGraph'> & Pick<Config, 'workspaceDir'>>
|
|
)
|
|
& {
|
|
argv?: {
|
|
original: string[]
|
|
}
|
|
fallbackCommandUsed?: boolean
|
|
}
|
|
& CheckDepsStatusOptions
|
|
|
|
export async function handler (
|
|
opts: RunOpts,
|
|
params: string[]
|
|
): Promise<string | { exitCode: number } | undefined> {
|
|
let dir: string
|
|
if (opts.fallbackCommandUsed && (params[0] === 't' || params[0] === 'tst')) {
|
|
params[0] = 'test'
|
|
}
|
|
const [scriptName, ...passedThruArgs] = params
|
|
|
|
if (opts.verifyDepsBeforeRun) {
|
|
await runDepsStatusCheck(opts)
|
|
}
|
|
|
|
if (opts.nodeOptions) {
|
|
opts.extraEnv = {
|
|
...opts.extraEnv,
|
|
NODE_OPTIONS: opts.nodeOptions,
|
|
}
|
|
}
|
|
|
|
if (opts.recursive) {
|
|
if (scriptName || Object.keys(opts.selectedProjectsGraph).length > 1) {
|
|
return runRecursive(params, opts) as Promise<undefined>
|
|
}
|
|
dir = Object.keys(opts.selectedProjectsGraph)[0]
|
|
} else {
|
|
dir = opts.dir
|
|
}
|
|
const manifest = await readProjectManifestOnly(dir, opts)
|
|
if (!scriptName) {
|
|
const rootManifest = opts.workspaceDir && opts.workspaceDir !== dir
|
|
? (await tryReadProjectManifest(opts.workspaceDir, opts)).manifest
|
|
: undefined
|
|
return printProjectCommands(manifest, rootManifest ?? undefined)
|
|
}
|
|
|
|
let specifiedScripts = getSpecifiedScripts(manifest.scripts ?? {}, scriptName)
|
|
|
|
if (!process.env.npm_lifecycle_event) {
|
|
specifiedScripts = throwOrFilterHiddenScripts(specifiedScripts, scriptName)
|
|
}
|
|
|
|
if (specifiedScripts.length < 1) {
|
|
if (opts.ifPresent) return
|
|
if (opts.fallbackCommandUsed) {
|
|
if (opts.argv == null) throw new Error('Could not fallback because opts.argv.original was not passed to the script runner')
|
|
const params = opts.argv.original.slice(1)
|
|
while (params.length > 0 && params[0][0] === '-' && params[0] !== '--') {
|
|
params.shift()
|
|
}
|
|
if (params.length > 0 && params[0] === '--') {
|
|
params.shift()
|
|
}
|
|
if (params.length === 0) {
|
|
throw new PnpmError('UNEXPECTED_BEHAVIOR', 'Params should not be an empty array', {
|
|
hint: 'This was a bug caused by programmer error. Please report it',
|
|
})
|
|
}
|
|
return exec({
|
|
selectedProjectsGraph: {},
|
|
implicitlyFellbackFromRun: true,
|
|
...opts,
|
|
}, params)
|
|
}
|
|
if (opts.workspaceDir) {
|
|
const { manifest: rootManifest } = await tryReadProjectManifest(opts.workspaceDir, opts)
|
|
if (getSpecifiedScripts(rootManifest?.scripts ?? {}, scriptName).length > 0 && specifiedScripts.length < 1) {
|
|
throw new PnpmError('NO_SCRIPT', `Missing script: ${scriptName}`, {
|
|
hint: `But script matched with ${scriptName} is present in the root of the workspace,
|
|
so you may run "pnpm -w run ${scriptName}"`,
|
|
})
|
|
}
|
|
}
|
|
|
|
throw new PnpmError('NO_SCRIPT', `Missing script: ${scriptName}`, {
|
|
hint: buildCommandNotFoundHint(scriptName, manifest.scripts),
|
|
})
|
|
}
|
|
const concurrency = getWorkspaceConcurrency(opts.workspaceConcurrency)
|
|
|
|
const lifecycleOpts: RunLifecycleHookOptions = {
|
|
depPath: dir,
|
|
extraBinPaths: opts.extraBinPaths,
|
|
extraEnv: opts.extraEnv,
|
|
pkgRoot: dir,
|
|
rootModulesDir: await realpathMissing(path.join(dir, 'node_modules')),
|
|
scriptsPrependNodePath: opts.scriptsPrependNodePath,
|
|
scriptShell: opts.scriptShell,
|
|
silent: opts.reporter === 'silent',
|
|
shellEmulator: opts.shellEmulator,
|
|
stdio: (specifiedScripts.length > 1 && concurrency > 1) ? 'pipe' : 'inherit',
|
|
unsafePerm: true, // when running scripts explicitly, assume that they're trusted.
|
|
userAgent: opts.userAgent,
|
|
}
|
|
const existsPnp = existsInDir.bind(null, '.pnp.cjs')
|
|
const pnpPath = (opts.workspaceDir && existsPnp(opts.workspaceDir)) ?? existsPnp(dir)
|
|
if (pnpPath) {
|
|
lifecycleOpts.extraEnv = {
|
|
...lifecycleOpts.extraEnv,
|
|
...makeNodeRequireOption(pnpPath),
|
|
}
|
|
}
|
|
try {
|
|
const limitRun = pLimit(concurrency)
|
|
|
|
const runScriptOptions: RunScriptOptions = {
|
|
enablePrePostScripts: opts.enablePrePostScripts ?? false,
|
|
syncInjectedDepsAfterScripts: opts.syncInjectedDepsAfterScripts,
|
|
workspaceDir: opts.workspaceDir,
|
|
}
|
|
const _runScript = runScript.bind(null, { manifest, lifecycleOpts, runScriptOptions, passedThruArgs })
|
|
|
|
await Promise.all(specifiedScripts.map(script => limitRun(() => _runScript(script))))
|
|
} catch (err: unknown) {
|
|
if (opts.bail !== false) {
|
|
throw err
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const ALL_LIFECYCLE_SCRIPTS = new Set([
|
|
'prepublish',
|
|
'prepare',
|
|
'prepublishOnly',
|
|
'prepack',
|
|
'postpack',
|
|
'publish',
|
|
'postpublish',
|
|
'preinstall',
|
|
'install',
|
|
'postinstall',
|
|
'preuninstall',
|
|
'uninstall',
|
|
'postuninstall',
|
|
'preversion',
|
|
'version',
|
|
'postversion',
|
|
'pretest',
|
|
'test',
|
|
'posttest',
|
|
'prestop',
|
|
'stop',
|
|
'poststop',
|
|
'prestart',
|
|
'start',
|
|
'poststart',
|
|
'prerestart',
|
|
'restart',
|
|
'postrestart',
|
|
'preshrinkwrap',
|
|
'shrinkwrap',
|
|
'postshrinkwrap',
|
|
])
|
|
|
|
function printProjectCommands (
|
|
manifest: ProjectManifest,
|
|
rootManifest?: ProjectManifest
|
|
): string {
|
|
const lifecycleScripts = [] as string[][]
|
|
const otherScripts = [] as string[][]
|
|
|
|
for (const [scriptName, script] of Object.entries(manifest.scripts ?? {})) {
|
|
if (scriptName.startsWith('.')) continue
|
|
if (ALL_LIFECYCLE_SCRIPTS.has(scriptName)) {
|
|
lifecycleScripts.push([scriptName, script])
|
|
} else {
|
|
otherScripts.push([scriptName, script])
|
|
}
|
|
}
|
|
|
|
if (lifecycleScripts.length === 0 && otherScripts.length === 0) {
|
|
return 'There are no scripts specified.'
|
|
}
|
|
|
|
let output = ''
|
|
if (lifecycleScripts.length > 0) {
|
|
output += `Lifecycle scripts:\n${renderCommands(lifecycleScripts)}`
|
|
}
|
|
if (otherScripts.length > 0) {
|
|
if (output !== '') output += '\n\n'
|
|
output += `Commands available via "pnpm run":\n${renderCommands(otherScripts)}`
|
|
}
|
|
if ((rootManifest?.scripts) == null) {
|
|
return output
|
|
}
|
|
const rootScripts = Object.entries(rootManifest.scripts)
|
|
if (rootScripts.length === 0) {
|
|
return output
|
|
}
|
|
if (output !== '') output += '\n\n'
|
|
output += `Commands of the root workspace project (to run them, use "pnpm -w run"):
|
|
${renderCommands(rootScripts)}`
|
|
return output
|
|
}
|
|
|
|
export interface RunScriptOptions {
|
|
enablePrePostScripts: boolean
|
|
syncInjectedDepsAfterScripts: string[] | undefined
|
|
workspaceDir: string | undefined
|
|
}
|
|
|
|
export async function runScript (opts: {
|
|
manifest: ProjectManifest
|
|
lifecycleOpts: RunLifecycleHookOptions
|
|
runScriptOptions: RunScriptOptions
|
|
passedThruArgs: string[]
|
|
}, scriptName: string): Promise<void> {
|
|
if (
|
|
opts.runScriptOptions.enablePrePostScripts &&
|
|
opts.manifest.scripts?.[`pre${scriptName}`] &&
|
|
!opts.manifest.scripts[scriptName].includes(`pre${scriptName}`)
|
|
) {
|
|
await runLifecycleHook(`pre${scriptName}`, opts.manifest, opts.lifecycleOpts)
|
|
}
|
|
await runLifecycleHook(scriptName, opts.manifest, { ...opts.lifecycleOpts, args: opts.passedThruArgs })
|
|
if (
|
|
opts.runScriptOptions.enablePrePostScripts &&
|
|
opts.manifest.scripts?.[`post${scriptName}`] &&
|
|
!opts.manifest.scripts[scriptName].includes(`post${scriptName}`)
|
|
) {
|
|
await runLifecycleHook(`post${scriptName}`, opts.manifest, opts.lifecycleOpts)
|
|
}
|
|
if (opts.runScriptOptions.syncInjectedDepsAfterScripts?.includes(scriptName)) {
|
|
await syncInjectedDeps({
|
|
pkgName: opts.manifest.name,
|
|
pkgRootDir: opts.lifecycleOpts.pkgRoot,
|
|
workspaceDir: opts.runScriptOptions.workspaceDir,
|
|
})
|
|
}
|
|
}
|
|
|
|
function renderCommands (commands: string[][]): string {
|
|
return commands.map(([scriptName, script]) => ` ${scriptName}\n ${script}`).join('\n')
|
|
}
|
|
|
|
function getSpecifiedScripts (scripts: PackageScripts, scriptName: string): string[] {
|
|
const specifiedSelector = getSpecifiedScriptWithoutStartCommand(scripts, scriptName)
|
|
|
|
if (specifiedSelector.length > 0) {
|
|
return specifiedSelector
|
|
}
|
|
|
|
// if a user passes start command as scriptName, `node server.js` will be executed as a fallback, so return start command even if start command is not defined in package.json
|
|
if (scriptName === 'start') {
|
|
return [scriptName]
|
|
}
|
|
|
|
return []
|
|
}
|