-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathindex.ts
More file actions
103 lines (97 loc) · 4.15 KB
/
Copy pathindex.ts
File metadata and controls
103 lines (97 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import assert from 'node:assert'
import fs from 'node:fs'
import path from 'node:path'
import util from 'node:util'
import { PnpmError } from '@pnpm/error'
import { runLifecycleHook, type RunLifecycleHookOptions } from '@pnpm/exec.lifecycle'
import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader'
import type { AllowBuild, PackageManifest } from '@pnpm/types'
import { rimraf } from '@zkochan/rimraf'
import { preferredPM } from 'preferred-pm'
// 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
unsafePerm?: boolean
userAgent?: string
}
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,
rootModulesDir: pkgDir, // We don't need this property but there is currently no way to not set it.
unsafePerm: Boolean(opts.unsafePerm),
userAgent: opts.userAgent,
}
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
}