blob: 8817a7fbdd4a1344735ad98d3d55eee5a5604f51 [file]
const fs = require('fs')
const path = require('path')
const { runPostinstallHooks } = require('@pnpm/lifecycle')
const runLifecycleHook = require('@pnpm/lifecycle').default
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 (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 packageJson = ''
// Catch invalid JSON errors and retry or throw a more detailed Error.
// See: https://github.com/aspect-build/rules_js/issues/483
const json = await fs.promises.readFile(packageJsonPath)
try {
packageJson = JSON.parse(json)
} catch (e) {
console.error(
`Error parsing ${packageName}/package.json: ${e}\n\n""""\n${json}\n""""\nRetrying...`
)
const json2 = await fs.promises.readFile(packageJsonPath)
try {
packageJson = JSON.parse(json2)
} catch (e2) {
throw new Error(
`Error parsing ${packageName}/package.json: ${e2}\n\n""""\n${json2}\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 (binName of Object.keys(bin)) {
binPath = normalizeBinPath(bin[binName])
binBash = `#!/usr/bin/env bash\nexec node "${path.join(
...segmentsUp,
packageName,
binPath
)}" "$@"`
binEntryPath = path.join(nodeModulesPath, '.bin', binName)
await fs.promises.writeFile(binEntryPath, binBash)
await fs.promises.chmod(binEntryPath, '755') // executable
}
}
}
}
}
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.run_lifecycle_hooks) {
// Runs preinstall, install, and postinstall hooks
await runPostinstallHooks(opts)
}
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)
contents.forEach(async (file) => {
await 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)
contents.forEach(async (fileName) => {
await copyRecursive(
path.join(src, fileName),
path.join(dest, fileName)
)
})
} else {
await fs.promises.copyFile(src, dest)
}
}
;(async () => {
try {
await main(process.argv.slice(2))
} catch (e) {
console.error(e)
process.exit(1)
}
})()