Files
pnpm/exec/prepare-package/src/index.ts
Brandon Cheng 01914345d5 build: enable @typescript-eslint/no-import-type-side-effects (#10630)
* build: enable `@typescript-eslint/no-import-type-side-effects`

* build: disable `@typescript-eslint/consistent-type-imports`

* chore: apply fixes for `no-import-type-side-effects`

pnpm exec eslint "**/src/**/*.ts" "**/test/**/*.ts" --fix
2026-03-08 00:02:48 +01:00

106 lines
4.3 KiB
TypeScript

import assert from 'assert'
import fs from 'fs'
import path from 'path'
import util from 'util'
import { PnpmError } from '@pnpm/error'
import { runLifecycleHook, type RunLifecycleHookOptions } from '@pnpm/lifecycle'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import type { AllowBuild, PackageManifest } from '@pnpm/types'
import rimraf from '@zkochan/rimraf'
import preferredPM from 'preferred-pm'
import { omit } from 'ramda'
// We don't run prepublishOnly to prepare the dependency.
// This might be counterintuitive as prepublishOnly is where a lot of packages put their build scripts.
// However, neither npm nor Yarn run prepublishOnly of git-hosted dependencies (checked on npm v10 and Yarn v3).
const PREPUBLISH_SCRIPTS = [
'prepublish',
'prepack',
'publish',
]
export interface PreparePackageOptions {
allowBuild?: AllowBuild
ignoreScripts?: boolean
rawConfig: Record<string, unknown>
unsafePerm?: boolean
}
export async function preparePackage (opts: PreparePackageOptions, gitRootDir: string, subDir: string): Promise<{ shouldBeBuilt: boolean, pkgDir: string }> {
const pkgDir = safeJoinPath(gitRootDir, subDir)
const manifest = await safeReadPackageJsonFromDir(pkgDir)
if (manifest?.scripts == null || !packageShouldBeBuilt(manifest, pkgDir)) return { shouldBeBuilt: false, pkgDir }
if (opts.ignoreScripts) return { shouldBeBuilt: true, pkgDir }
// Check if the package is allowed to run build scripts
// If allowBuild is undefined or returns false, block the build
if (!opts.allowBuild?.(manifest.name, manifest.version)) {
throw new PnpmError(
'GIT_DEP_PREPARE_NOT_ALLOWED',
`The git-hosted package "${manifest.name}@${manifest.version}" needs to execute build scripts but is not in the "allowBuilds" allowlist.`,
{
hint: `Add the package to "allowBuilds" in your project's pnpm-workspace.yaml to allow it to run scripts. For example:
allowBuilds:
${manifest.name}: true`,
}
)
}
const pm = (await preferredPM(gitRootDir))?.name ?? 'npm'
const execOpts: RunLifecycleHookOptions = {
depPath: `${manifest.name}@${manifest.version}`,
pkgRoot: pkgDir,
// We can't prepare a package without running its lifecycle scripts.
// An alternative solution could be to throw an exception.
rawConfig: omit(['ignore-scripts'], opts.rawConfig),
rootModulesDir: pkgDir, // We don't need this property but there is currently no way to not set it.
unsafePerm: Boolean(opts.unsafePerm),
}
try {
const installScriptName = `${pm}-install`
manifest.scripts[installScriptName] = `${pm} install`
await runLifecycleHook(installScriptName, manifest, execOpts)
for (const scriptName of PREPUBLISH_SCRIPTS) {
if (manifest.scripts[scriptName] == null || manifest.scripts[scriptName] === '') continue
let newScriptName
if (pm !== 'pnpm') {
newScriptName = `${pm}-run-${scriptName}`
manifest.scripts[newScriptName] = `${pm} run ${scriptName}`
} else {
newScriptName = scriptName
}
// eslint-disable-next-line no-await-in-loop
await runLifecycleHook(newScriptName, manifest, execOpts)
}
} catch (err: unknown) {
assert(util.types.isNativeError(err))
Object.assign(err, {
code: 'ERR_PNPM_PREPARE_PACKAGE',
})
throw err
}
await rimraf(path.join(pkgDir, 'node_modules'))
return { shouldBeBuilt: true, pkgDir }
}
function packageShouldBeBuilt (manifest: PackageManifest, pkgDir: string): boolean {
if (manifest.scripts == null) return false
const scripts = manifest.scripts
if (scripts.prepare != null && scripts.prepare !== '') return true
const hasPrepublishScript = PREPUBLISH_SCRIPTS.some((scriptName) => scripts[scriptName] != null && scripts[scriptName] !== '')
if (!hasPrepublishScript) return false
const mainFile = manifest.main ?? 'index.js'
return !fs.existsSync(path.join(pkgDir, mainFile))
}
function safeJoinPath (root: string, sub: string): string {
const joined = path.join(root, sub)
// prevent the dir traversal attack
const relative = path.relative(root, joined)
if (relative.startsWith('..')) {
throw new PnpmError('INVALID_PATH', `Path "${sub}" should be a sub directory`)
}
if (!fs.existsSync(joined) || !fs.lstatSync(joined).isDirectory()) {
throw new PnpmError('INVALID_PATH', `Path "${sub}" is not a directory`)
}
return joined
}