blob: 92501be4e8ea58fbd38977b62a18575144ffe06e [file]
const fs = require('fs')
const exists = require('path-exists')
const path = require('path')
const { safeReadPackageJsonFromDir } = require('@pnpm/read-package-json')
const { runLifecycleHook } = require('@pnpm/lifecycle')
async function mkdirp(p) {
if (p && !fs.existsSync(p)) {
await mkdirp(path.dirname(p))
await fs.promises.mkdir(p)
}
}
function normalizeBinPath(p) {
let result = p.replace(/\\/g, '/')
if (result.startsWith('./')) {
result = result.substring(2)
}
return result
}
async function makeBins(nodeModulesPath, scope, segmentsUp) {
const packages = await fs.promises.readdir(
path.join(nodeModulesPath, scope)
)
for (const _package of packages) {
if (!scope && _package.startsWith('@')) {
await makeBins(nodeModulesPath, _package, segmentsUp)
continue
}
const packageName = path.join(scope, _package)
const packageJsonPath = path.join(
nodeModulesPath,
packageName,
'package.json'
)
if (fs.existsSync(packageJsonPath)) {
let packageJsonStr = await fs.promises.readFile(packageJsonPath)
let packageJson
try {
packageJson = JSON.parse(packageJsonStr)
} catch (e) {
// Catch and throw a more detailed error message.
throw new Error(
`Error parsing ${packageName}/package.json: ${e}\n\n""""\n${packageJsonStr}\n""""`
)
}
// https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bin
if (packageJson.bin) {
await mkdirp(path.join(nodeModulesPath, '.bin'))
let bin = packageJson.bin
if (typeof bin == 'string') {
bin = { [_package]: bin }
}
for (const binName of Object.keys(bin)) {
if (binName.includes('/') || binName.includes('\\')) {
// multi-segment bin names are not supported; pnpm itself
// also does not make .bin entries in this case as of pnpm v8.3.1
continue
}
const binPath = normalizeBinPath(bin[binName])
const binBash = `#!/usr/bin/env bash\nexec node "${path.join(
...segmentsUp,
packageName,
binPath
)}" "$@"`
const binEntryPath = path.join(
nodeModulesPath,
'.bin',
binName
)
await fs.promises.writeFile(binEntryPath, binBash)
await fs.promises.chmod(binEntryPath, '755') // executable
}
}
}
}
}
// Helper which is exported from @pnpm/lifecycle:
// https://github.com/pnpm/pnpm/blob/bc18d33fe00d9ed43f1562d4cc6d37f49d9c2c38/exec/lifecycle/src/index.ts#L52
async function checkBindingGyp(root, scripts) {
if (await exists(path.join(root, 'binding.gyp'))) {
scripts['install'] = 'node-gyp rebuild'
}
}
// Like runPostinstallHooks from @pnpm/lifecycle at
// https://github.com/pnpm/pnpm/blob/bc18d33fe00d9ed43f1562d4cc6d37f49d9c2c38/exec/lifecycle/src/index.ts#L20
// but also runs a customizable list of lifecycle hooks.
async function runLifecycleHooks(opts, hooks) {
const pkg = await safeReadPackageJsonFromDir(opts.pkgRoot)
if (pkg == null) {
return
}
if (pkg.scripts == null) {
pkg.scripts = {}
}
const runInstallScripts =
hooks.includes('preinstall') ||
hooks.includes('install') ||
hooks.includes('postinstall')
if (runInstallScripts && !pkg.scripts.install) {
await checkBindingGyp(opts.pkgRoot, pkg.scripts)
}
for (const hook of hooks) {
if (pkg.scripts[hook]) {
await runLifecycleHook(hook, pkg, opts)
}
}
}
async function main(args) {
if (args.length !== 3) {
console.error(
'Usage: node lifecycle-hooks.js [packageName] [packageDir] [outputDir]'
)
process.exit(1)
}
const packageName = args[0]
const packageDir = args[1]
const outputDir = args[2]
await copyPackageContents(packageDir, outputDir)
// Resolve the path to the node_modules folder for this package in the symlinked node_modules
// tree. Output path is of the format,
// .../node_modules/.aspect_rules_js/package@version/node_modules/package
// .../node_modules/.aspect_rules_js/@scope+package@version/node_modules/@scope/package
// Path to node_modules is one or two segments up from the output path depending on the packageName
const segmentsUp = Array(packageName.split('/').length).fill('..')
const nodeModulesPath = path.resolve(path.join(outputDir, ...segmentsUp))
// Create .bin entry point files for all packages in node_modules
await makeBins(nodeModulesPath, '', segmentsUp)
// export interface RunLifecycleHookOptions {
// args?: string[];
// depPath: string;
// extraBinPaths?: string[];
// extraEnv?: Record<string, string>;
// initCwd?: string;
// optional?: boolean;
// pkgRoot: string;
// rawConfig: object;
// rootModulesDir: string;
// scriptShell?: string;
// silent?: boolean;
// scriptsPrependNodePath?: boolean | 'warn-only';
// shellEmulator?: boolean;
// stdio?: string;
// unsafePerm: boolean;
// }
const opts = {
pkgRoot: path.resolve(outputDir),
rawConfig: {
stdio: 'inherit',
},
silent: false,
stdio: 'inherit',
rootModulesDir: nodeModulesPath,
unsafePerm: true, // Don't run under a specific user/group
}
const rulesJsJson = JSON.parse(
await fs.promises.readFile(
path.join(packageDir, 'aspect_rules_js_metadata.json')
)
)
if (rulesJsJson.lifecycle_hooks) {
// Runs configured lifecycle hooks
await runLifecycleHooks(opts, rulesJsJson.lifecycle_hooks.split(','))
}
if (rulesJsJson.scripts?.custom_postinstall) {
// Run user specified custom postinstall hook
await runLifecycleHook('custom_postinstall', rulesJsJson, opts)
}
}
// Copy contents of a package dir to a destination dir (without copying the package dir itself)
async function copyPackageContents(packageDir, destDir) {
const contents = await fs.promises.readdir(packageDir)
await Promise.all(
contents.map((file) =>
copyRecursive(path.join(packageDir, file), path.join(destDir, file))
)
)
}
// Recursively copy files and folders
async function copyRecursive(src, dest) {
const stats = await fs.promises.stat(src)
if (stats.isDirectory()) {
await mkdirp(dest)
const contents = await fs.promises.readdir(src)
await Promise.all(
contents.map((file) =>
copyRecursive(path.join(src, file), path.join(dest, file))
)
)
} else {
await fs.promises.copyFile(src, dest)
}
}
;(async () => {
try {
await main(process.argv.slice(2))
} catch (e) {
// Note: use .log rather than .error. The former is deferred and the latter is immediate.
// The error is harder to spot and parse when it appears in the middle of the other logs.
if (e.code === 'ELIFECYCLE' && !!e.pkgid && !!e.stage && !!e.script) {
console.log(
'==============================================================='
)
console.log(
`Failure while running lifecycle hook for package '${e.pkgid}':\n`
)
console.log(` Script: '${e.stage}'`)
console.log(` Command: \`${e.script}\``)
console.log(`\nStack trace:\n`)
// First line of error is always the message, which is redundant with the above logging.
console.log(e.stack.replace(/^.*?\n/, ''))
console.log(
'==============================================================='
)
} else {
console.log(e)
}
process.exit(1)
}
})()