mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-12 00:48:21 -05:00
* feat: update injected packages after run (wip) close #9081 * refactor: rename field * feat: injectedPackages (wip) * feat: findInjectedPackages (wip) * feat: complete implementation * test: findInjectedPackages * docs: changeset * refactor: be lazy * chore: set `version` to `1000.0.0-0` * feat: use hardlinks for injected packages * refactor: just use `.modules.yaml` * feat: debug logger * refactor: `modulesDir` is unnecessary * test: shouldUpdateInjectedFilesAfterRun * fix(test): remove the test command * test: updateInjectedPackagesAfterRun * fix: eslint * feat: rename config * perf: diff to reduce fs operations * perf: load source map only once * chore(deps): remove unused dependencies * fix: eslint * refactor: use `symlink-dir` * refactor: move type expr to an alias * refactor: simplify types * feat: reuse stats from the directory fetcher * test: directories and symlinks * feat: sort alphabetic * test: diffDir * test: rename a test * test: remove nesting * refactor: rename * feat: remove buggy symlink support * test: applyPatch * docs: correct * docs: fix * test: extendFilesMap * docs: remove outdated comment * docs: remove unneeded comment * test: fix * test: more assertions * test: DirPatcher * test: more assertions * test: more assertions * test: just use `createDir` * test: multiple patchers * test: reuse stat results * docs: consistent grammar * test: workaround * test: fix windows * refactor: remove single-use `makeParent` * refactor: remove nonsense test How could I even misunderstand my own code?! `Patcher.apply()` will never call stat on the files because they have all been loaded to calculate `Patcher.patch`. This test is therefore nonsense. * feat: rename * feat: rename again * feat: remove `boolean` * fix: broken lockfile * test: use a fixture for testing sync injected deps * test: refactor syne injected deps test * test: refactor sync injected deps test * test: refactor sync injected deps test * refactor: rename injected deps to syncer * refactor: change injected deps logger * docs: update changeset --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
237 lines
8.3 KiB
TypeScript
237 lines
8.3 KiB
TypeScript
import assert from 'assert'
|
|
import path from 'path'
|
|
import util from 'util'
|
|
import { throwOnCommandFail } from '@pnpm/cli-utils'
|
|
import { type Config } from '@pnpm/config'
|
|
import { prepareExecutionEnv } from '@pnpm/plugin-commands-env'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import {
|
|
makeNodeRequireOption,
|
|
type RunLifecycleHookOptions,
|
|
} from '@pnpm/lifecycle'
|
|
import { logger } from '@pnpm/logger'
|
|
import { groupStart } from '@pnpm/log.group'
|
|
import { sortPackages } from '@pnpm/sort-packages'
|
|
import pLimit from 'p-limit'
|
|
import realpathMissing from 'realpath-missing'
|
|
import { existsInDir } from './existsInDir'
|
|
import { createEmptyRecursiveSummary, getExecutionDuration, getResumedPackageChunks, writeRecursiveSummary } from './exec'
|
|
import { type RunScriptOptions, runScript } from './run'
|
|
import { tryBuildRegExpFromCommand } from './regexpCommand'
|
|
import { type PackageScripts, type ProjectRootDir } from '@pnpm/types'
|
|
|
|
export type RecursiveRunOpts = Pick<Config,
|
|
| 'bin'
|
|
| 'enablePrePostScripts'
|
|
| 'unsafePerm'
|
|
| 'pnpmHomeDir'
|
|
| 'rawConfig'
|
|
| 'rootProjectManifest'
|
|
| 'scriptsPrependNodePath'
|
|
| 'scriptShell'
|
|
| 'shellEmulator'
|
|
| 'stream'
|
|
| 'syncInjectedDepsAfterScripts'
|
|
| 'workspaceDir'
|
|
> & Required<Pick<Config, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir' | 'dir'>> &
|
|
Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'bail' | 'reporter' | 'reverse' | 'sort' | 'workspaceConcurrency'>> &
|
|
{
|
|
ifPresent?: boolean
|
|
resumeFrom?: string
|
|
reportSummary?: boolean
|
|
}
|
|
|
|
export async function runRecursive (
|
|
params: string[],
|
|
opts: RecursiveRunOpts
|
|
): Promise<void> {
|
|
const [scriptName, ...passedThruArgs] = params
|
|
if (!scriptName) {
|
|
throw new PnpmError('SCRIPT_NAME_IS_REQUIRED', 'You must specify the script you want to run')
|
|
}
|
|
let hasCommand = 0
|
|
|
|
const sortedPackageChunks = opts.sort
|
|
? sortPackages(opts.selectedProjectsGraph)
|
|
: [(Object.keys(opts.selectedProjectsGraph) as ProjectRootDir[]).sort()]
|
|
let packageChunks: ProjectRootDir[][] = opts.reverse ? sortedPackageChunks.reverse() : sortedPackageChunks
|
|
|
|
if (opts.resumeFrom) {
|
|
packageChunks = getResumedPackageChunks({
|
|
resumeFrom: opts.resumeFrom,
|
|
chunks: packageChunks,
|
|
selectedProjectsGraph: opts.selectedProjectsGraph,
|
|
})
|
|
}
|
|
|
|
const limitRun = pLimit(opts.workspaceConcurrency ?? 4)
|
|
const stdio =
|
|
!opts.stream &&
|
|
(opts.workspaceConcurrency === 1 ||
|
|
(packageChunks.length === 1 && packageChunks[0].length === 1))
|
|
? 'inherit'
|
|
: 'pipe'
|
|
const existsPnp = existsInDir.bind(null, '.pnp.cjs')
|
|
const workspacePnpPath = opts.workspaceDir && existsPnp(opts.workspaceDir)
|
|
|
|
const requiredScripts = opts.rootProjectManifest?.pnpm?.requiredScripts ?? []
|
|
if (requiredScripts.includes(scriptName)) {
|
|
const missingScriptPackages: string[] = packageChunks
|
|
.flat()
|
|
.map((prefix) => opts.selectedProjectsGraph[prefix])
|
|
.filter((pkg) => getSpecifiedScripts(pkg.package.manifest.scripts ?? {}, scriptName).length < 1)
|
|
.map((pkg) => pkg.package.manifest.name ?? pkg.package.rootDir)
|
|
if (missingScriptPackages.length) {
|
|
throw new PnpmError('RECURSIVE_RUN_NO_SCRIPT', `Missing script "${scriptName}" in packages: ${missingScriptPackages.join(', ')}`)
|
|
}
|
|
}
|
|
|
|
const result = createEmptyRecursiveSummary(packageChunks)
|
|
|
|
for (const chunk of packageChunks) {
|
|
const selectedScripts = chunk.map(prefix => {
|
|
const pkg = opts.selectedProjectsGraph[prefix]
|
|
const specifiedScripts = getSpecifiedScripts(pkg.package.manifest.scripts ?? {}, scriptName)
|
|
if (!specifiedScripts.length) {
|
|
result[prefix].status = 'skipped'
|
|
}
|
|
return specifiedScripts.map(script => ({ prefix, scriptName: script }))
|
|
}).flat()
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await Promise.all(selectedScripts.map(async ({ prefix, scriptName }) =>
|
|
limitRun(async () => {
|
|
const pkg = opts.selectedProjectsGraph[prefix]
|
|
if (
|
|
!pkg.package.manifest.scripts?.[scriptName] ||
|
|
process.env.npm_lifecycle_event === scriptName &&
|
|
process.env.PNPM_SCRIPT_SRC_DIR === prefix
|
|
) {
|
|
return
|
|
}
|
|
result[prefix].status = 'running'
|
|
const startTime = process.hrtime()
|
|
hasCommand++
|
|
try {
|
|
const lifecycleOpts: RunLifecycleHookOptions = {
|
|
depPath: prefix,
|
|
extraBinPaths: opts.extraBinPaths,
|
|
extraEnv: opts.extraEnv,
|
|
pkgRoot: prefix,
|
|
rawConfig: opts.rawConfig,
|
|
rootModulesDir: await realpathMissing(path.join(prefix, 'node_modules')),
|
|
scriptsPrependNodePath: opts.scriptsPrependNodePath,
|
|
scriptShell: opts.scriptShell,
|
|
silent: opts.reporter === 'silent',
|
|
shellEmulator: opts.shellEmulator,
|
|
stdio,
|
|
unsafePerm: true, // when running scripts explicitly, assume that they're trusted.
|
|
}
|
|
const { executionEnv } = pkg.package.manifest.pnpm ?? {}
|
|
if (executionEnv != null) {
|
|
lifecycleOpts.extraBinPaths = (await prepareExecutionEnv(opts, { executionEnv })).extraBinPaths
|
|
}
|
|
const pnpPath = workspacePnpPath ?? existsPnp(prefix)
|
|
if (pnpPath) {
|
|
lifecycleOpts.extraEnv = {
|
|
...lifecycleOpts.extraEnv,
|
|
...makeNodeRequireOption(pnpPath),
|
|
}
|
|
}
|
|
|
|
const runScriptOptions: RunScriptOptions = {
|
|
enablePrePostScripts: opts.enablePrePostScripts ?? false,
|
|
syncInjectedDepsAfterScripts: opts.syncInjectedDepsAfterScripts,
|
|
workspaceDir: opts.workspaceDir,
|
|
}
|
|
const _runScript = runScript.bind(null, { manifest: pkg.package.manifest, lifecycleOpts, runScriptOptions, passedThruArgs })
|
|
const groupEnd = (opts.workspaceConcurrency ?? 4) > 1
|
|
? undefined
|
|
: groupStart(formatSectionName({
|
|
name: pkg.package.manifest.name,
|
|
script: scriptName,
|
|
version: pkg.package.manifest.version,
|
|
prefix: path.normalize(path.relative(opts.workspaceDir, prefix)),
|
|
}))
|
|
await _runScript(scriptName)
|
|
groupEnd?.()
|
|
result[prefix].status = 'passed'
|
|
result[prefix].duration = getExecutionDuration(startTime)
|
|
} catch (err: unknown) {
|
|
assert(util.types.isNativeError(err))
|
|
result[prefix] = {
|
|
status: 'failure',
|
|
duration: getExecutionDuration(startTime),
|
|
error: err,
|
|
message: err.message,
|
|
prefix,
|
|
}
|
|
|
|
if (!opts.bail) {
|
|
return
|
|
}
|
|
|
|
Object.assign(err, {
|
|
code: 'ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL',
|
|
prefix,
|
|
})
|
|
opts.reportSummary && await writeRecursiveSummary({
|
|
dir: opts.workspaceDir ?? opts.dir,
|
|
summary: result,
|
|
})
|
|
|
|
throw err
|
|
}
|
|
}
|
|
)))
|
|
}
|
|
|
|
if (scriptName !== 'test' && !hasCommand && !opts.ifPresent) {
|
|
const allPackagesAreSelected = Object.keys(opts.selectedProjectsGraph).length === opts.allProjects.length
|
|
if (allPackagesAreSelected) {
|
|
throw new PnpmError('RECURSIVE_RUN_NO_SCRIPT', `None of the packages has a "${scriptName}" script`)
|
|
} else {
|
|
logger.info({
|
|
message: `None of the selected packages has a "${scriptName}" script`,
|
|
prefix: opts.workspaceDir,
|
|
})
|
|
}
|
|
}
|
|
opts.reportSummary && await writeRecursiveSummary({
|
|
dir: opts.workspaceDir ?? opts.dir,
|
|
summary: result,
|
|
})
|
|
throwOnCommandFail('pnpm recursive run', result)
|
|
}
|
|
|
|
function formatSectionName ({
|
|
script,
|
|
name,
|
|
version,
|
|
prefix,
|
|
}: {
|
|
script?: string
|
|
name?: string
|
|
version?: string
|
|
prefix: string
|
|
}) {
|
|
return `${name ?? 'unknown'}${version ? `@${version}` : ''} ${script ? `: ${script}` : ''} ${prefix}`
|
|
}
|
|
|
|
export function getSpecifiedScripts (scripts: PackageScripts, scriptName: string): string[] {
|
|
// if scripts in package.json has script which is equal to scriptName a user passes, return it.
|
|
if (scripts[scriptName]) {
|
|
return [scriptName]
|
|
}
|
|
|
|
const scriptSelector = tryBuildRegExpFromCommand(scriptName)
|
|
|
|
// if scriptName which a user passes is RegExp (like /build:.*/), multiple scripts to execute will be selected with RegExp
|
|
if (scriptSelector) {
|
|
const scriptKeys = Object.keys(scripts)
|
|
return scriptKeys.filter(script => script.match(scriptSelector))
|
|
}
|
|
|
|
return []
|
|
}
|