mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-25 23:17:08 -04:00
* 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
106 lines
4.3 KiB
TypeScript
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
|
|
}
|