mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-14 12:09:33 -04:00
Major cleanup of the config system after migrating settings from `.npmrc` to `pnpm-workspace.yaml`.
### Config reader simplification
- Remove `checkUnknownSetting` (dead code, always `false`)
- Trim `npmConfigTypes` from ~127 to ~67 keys (remove unused npm config keys)
- Replace `rcOptions` iteration over all type keys with direct construction from defaults + auth overlay
- Remove `rcOptionsTypes` parameter from `getConfig()` and its assembly chain
### Rename `rawConfig` to `authConfig`
- `rawConfig` was a confusing mix of auth data and general settings
- Non-auth settings are already on the typed `Config` object — stop duplicating them in `rawConfig`
- Rename `rawConfig` → `authConfig` across the codebase to clarify it only contains auth/registry data from `.npmrc`
### Remove `rawConfig` from non-auth consumers
- **Lifecycle hooks**: replace `rawConfig: object` with `userAgent?: string` — only user-agent was read
- **Fetchers**: remove unused `rawConfig` from git fetcher, binary fetcher, tarball fetcher, prepare-package
- **Update command**: use `opts.production/dev/optional` instead of `rawConfig.*`
- **`pnpm init`**: accept typed init properties instead of parsing `rawConfig`
### Add `nodeDownloadMirrors` setting
- New `nodeDownloadMirrors?: Record<string, string>` on `PnpmSettings` and `Config`
- Replaces the `node-mirror:<channel>` pattern that was stored in `rawConfig`
- Configured in `pnpm-workspace.yaml`:
```yaml
nodeDownloadMirrors:
release: https://my-mirror.example.com/download/release/
```
- Remove unused `rawConfig` from deno-resolver and bun-resolver
### Refactor `pnpm config get/list`
- New `configToRecord()` builds display data from typed Config properties on the fly
- Excludes sensitive internals (`authInfos`, `sslConfigs`, etc.)
- Non-types keys (e.g., `package-extensions`) resolve through `configToRecord` instead of direct property access
- Delete `processConfig.ts` (replaced by `configToRecord.ts`)
### Pre-push hook improvement
- Add `compile-only` (`tsgo --build`) to pre-push hook to catch type errors before push
202 lines
6.6 KiB
TypeScript
202 lines
6.6 KiB
TypeScript
import { existsSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { lifecycleLogger } from '@pnpm/core-loggers'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import { globalWarn } from '@pnpm/logger'
|
|
import { lifecycle } from '@pnpm/npm-lifecycle'
|
|
import type { DependencyManifest, PackageScripts, ProjectManifest } from '@pnpm/types'
|
|
import chalk from 'chalk'
|
|
import isWindows from 'is-windows'
|
|
import { join as shellQuote } from 'shlex'
|
|
|
|
function noop () {} // eslint-disable-line:no-empty
|
|
|
|
export interface RunLifecycleHookOptions {
|
|
args?: string[]
|
|
depPath: string
|
|
extraBinPaths?: string[]
|
|
extraEnv?: Record<string, string>
|
|
initCwd?: string
|
|
optional?: boolean
|
|
pkgRoot: string
|
|
rootModulesDir: string
|
|
scriptShell?: string
|
|
silent?: boolean
|
|
scriptsPrependNodePath?: boolean | 'warn-only'
|
|
shellEmulator?: boolean
|
|
stdio?: string
|
|
unsafePerm: boolean
|
|
userAgent?: string
|
|
}
|
|
|
|
export async function runLifecycleHook (
|
|
stage: string,
|
|
manifest: ProjectManifest | DependencyManifest,
|
|
opts: RunLifecycleHookOptions
|
|
): Promise<boolean> {
|
|
const optional = opts.optional === true
|
|
|
|
// To remediate CVE_2024_27980, Node.js does not allow .bat or .cmd files to
|
|
// be spawned without the "shell: true" option.
|
|
//
|
|
// https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
|
//
|
|
// Unfortunately, setting spawn's shell option also causes arguments to be
|
|
// evaluated before they're passed to the shell, resulting in a surprising
|
|
// behavior difference only with .bat/.cmd files.
|
|
//
|
|
// Instead of showing a "spawn EINVAL" error, let's throw a clearer error that
|
|
// this isn't supported.
|
|
//
|
|
// If this behavior needs to be supported in the future, the arguments would
|
|
// need to be escaped before they're passed to the .bat/.cmd file. For
|
|
// example, scripts such as "echo %PATH%" should be passed verbatim rather
|
|
// than expanded. This is difficult to do correctly. Other open source tools
|
|
// (e.g. Rust) attempted and introduced bugs. The Rust blog has a good
|
|
// high-level explanation of the same security vulnerability Node.js patched.
|
|
//
|
|
// https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html#overview
|
|
//
|
|
// Note that npm (as of version 10.5.0) doesn't support setting script-shell
|
|
// to a .bat or .cmd file either.
|
|
if (opts.scriptShell != null && typeof opts.scriptShell === 'string' && isWindowsBatchFile(opts.scriptShell)) {
|
|
throw new PnpmError('ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS', 'Cannot spawn .bat or .cmd as a script shell.', {
|
|
hint: `\
|
|
The pnpm-workspace.yaml scriptShell option was configured to a .bat or .cmd file. These cannot be used as a script shell reliably.
|
|
|
|
Please unset the scriptShell option, or configure it to a .exe instead.
|
|
`,
|
|
})
|
|
}
|
|
|
|
const m = { _id: getId(manifest), ...manifest }
|
|
m.scripts = { ...m.scripts }
|
|
|
|
switch (stage) {
|
|
case 'start':
|
|
if (!m.scripts.start) {
|
|
if (!existsSync('server.js')) {
|
|
throw new PnpmError('NO_SCRIPT_OR_SERVER', 'Missing script start or file server.js')
|
|
}
|
|
m.scripts.start = 'node server.js'
|
|
}
|
|
break
|
|
case 'install':
|
|
if (!m.scripts.install && !m.scripts.preinstall) {
|
|
checkBindingGyp(opts.pkgRoot, m.scripts)
|
|
}
|
|
break
|
|
}
|
|
if (opts.args?.length && m.scripts?.[stage]) {
|
|
// It is impossible to quote a command line argument that contains newline for Windows cmd.
|
|
const escapedArgs = isWindows()
|
|
? opts.args.map((arg) => JSON.stringify(arg)).join(' ')
|
|
: shellQuote(opts.args)
|
|
m.scripts[stage] = `${m.scripts[stage]} ${escapedArgs}`
|
|
}
|
|
// This script is used to prevent the usage of npm or Yarn.
|
|
// It does nothing, when pnpm is used, so we may skip its execution.
|
|
if (m.scripts[stage] === 'npx only-allow pnpm' || !m.scripts[stage]) return false
|
|
if (opts.stdio !== 'inherit') {
|
|
lifecycleLogger.debug({
|
|
depPath: opts.depPath,
|
|
optional,
|
|
script: m.scripts[stage],
|
|
stage,
|
|
wd: opts.pkgRoot,
|
|
})
|
|
} else if (!opts.silent) {
|
|
process.stderr.write(chalk.dim(`$ ${m.scripts[stage]}`) + '\n')
|
|
}
|
|
const logLevel = (opts.stdio !== 'inherit' || opts.silent)
|
|
? 'silent'
|
|
: undefined
|
|
await lifecycle(m, stage, opts.pkgRoot, {
|
|
config: {},
|
|
dir: opts.rootModulesDir,
|
|
extraBinPaths: opts.extraBinPaths,
|
|
extraEnv: {
|
|
...opts.extraEnv,
|
|
INIT_CWD: opts.initCwd ?? process.cwd(),
|
|
PNPM_SCRIPT_SRC_DIR: opts.pkgRoot,
|
|
...(opts.userAgent ? { npm_config_user_agent: opts.userAgent } : {}),
|
|
},
|
|
log: {
|
|
clearProgress: noop,
|
|
info: noop,
|
|
level: logLevel,
|
|
pause: noop,
|
|
resume: noop,
|
|
showProgress: noop,
|
|
silly: npmLog,
|
|
verbose: npmLog,
|
|
warn: (...msg: string[]) => {
|
|
globalWarn(msg.join(' '))
|
|
},
|
|
},
|
|
runConcurrently: true,
|
|
scriptsPrependNodePath: opts.scriptsPrependNodePath,
|
|
scriptShell: opts.scriptShell,
|
|
shellEmulator: opts.shellEmulator,
|
|
stdio: opts.stdio ?? 'pipe',
|
|
unsafePerm: opts.unsafePerm,
|
|
})
|
|
return true
|
|
|
|
function npmLog (prefix: string, logId: string, stdtype: string, line?: number): void {
|
|
switch (stdtype) {
|
|
case 'stdout':
|
|
case 'stderr':
|
|
lifecycleLogger.debug({
|
|
depPath: opts.depPath,
|
|
line: (line ?? 0).toString(),
|
|
stage,
|
|
stdio: stdtype,
|
|
wd: opts.pkgRoot,
|
|
})
|
|
return
|
|
case 'Returned: code:': {
|
|
if (opts.stdio === 'inherit') {
|
|
// Preventing the pnpm reporter from overriding the project's script output
|
|
return
|
|
}
|
|
const code = line ?? 1
|
|
lifecycleLogger.debug({
|
|
depPath: opts.depPath,
|
|
exitCode: code,
|
|
optional,
|
|
stage,
|
|
wd: opts.pkgRoot,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run node-gyp when binding.gyp is available. Only do this when there are no
|
|
* `install` and `preinstall` scripts (see `npm help scripts`).
|
|
*/
|
|
function checkBindingGyp (
|
|
root: string,
|
|
scripts: PackageScripts
|
|
) {
|
|
if (existsSync(path.join(root, 'binding.gyp'))) {
|
|
scripts.install = 'node-gyp rebuild'
|
|
}
|
|
}
|
|
|
|
function getId (manifest: ProjectManifest | DependencyManifest): string {
|
|
return `${manifest.name ?? ''}@${manifest.version ?? ''}`
|
|
}
|
|
|
|
function isWindowsBatchFile (scriptShell: string) {
|
|
// Node.js performs a similar check to determine whether it should throw
|
|
// EINVAL when spawning a .cmd/.bat file.
|
|
//
|
|
// https://github.com/nodejs/node/commit/6627222409#diff-1e725bfa950eda4d4b5c0c00a2bb6be3e5b83d819872a1adf2ef87c658273903
|
|
const scriptShellLower = scriptShell.toLowerCase()
|
|
return isWindows() && (scriptShellLower.endsWith('.cmd') || scriptShellLower.endsWith('.bat'))
|
|
}
|