Skip to content

Commit

Permalink
feat: installation lifecycle scripts (#448)
Browse files Browse the repository at this point in the history
Co-authored-by: 天玎 <[email protected]>
  • Loading branch information
gemwuu and 天玎 authored Feb 13, 2023
1 parent 385b93c commit f48e7e3
Show file tree
Hide file tree
Showing 24 changed files with 138 additions and 314 deletions.
3 changes: 1 addition & 2 deletions lib/download/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const chalk = require('chalk');
const moment = require('moment');
const os = require('os');
const semver = require('semver');
const utility = require('utility');
const { family: getLibcFamily } = require('detect-libc');
const get = require('../get');
const utils = require('../utils');
Expand Down Expand Up @@ -153,7 +152,7 @@ async function _getCacheInfo(fullname, globalOptions) {
if (!globalOptions.cacheDir) {
return info;
}
const hash = utility.md5(info.pkgUrl);
const hash = crypto.createHash('md5').update(info.pkgUrl).digest('hex');
const parentDir = path.join(globalOptions.cacheDir, 'manifests', hash[0], hash[1], hash[2]);
// { etag, age, headers, manifests }
info.cacheFile = path.join(parentDir, `${hash}.json`);
Expand Down
4 changes: 2 additions & 2 deletions lib/format_install_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ module.exports = function formatInstallOptions(options) {
// close EventEmitter memory leak warning
options.events.setMaxListeners(0);
options.events.await = awaitEvent;

options.postInstallTasks = [];
options.runscriptCount = 0;
options.runscriptTime = 0;
// [
// {package: pkg, parentDir: 'parentDir', packageDir: 'packageDir'},
// ...
Expand Down
4 changes: 2 additions & 2 deletions lib/global_install.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
*/

const path = require('path');
const utility = require('utility');
const fs = require('fs/promises');
const crypto = require('crypto');
const chalk = require('chalk');
const npa = require('./npa');
const installLocal = require('./local_install');
Expand All @@ -31,7 +31,7 @@ module.exports = async (options, context) => {
} = pkg;
let name = alias || pkgName;
if (!name) {
name = utility.md5(version);
name = crypto.createHash('md5').update(version).digest('hex');
}

// compatibility: delete old store dir
Expand Down
12 changes: 7 additions & 5 deletions lib/install_package.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ const path = require('path');
const chalk = require('chalk');
const semver = require('semver');
const pMap = require('p-map');
const os = require('os');
const download = require('./download');
const utils = require('./utils');
const postinstall = require('./postinstall');
const preinstall = require('./preinstall');
const npa = require('./npa');
const bin = require('./bin');
const link = require('./link');
Expand All @@ -15,6 +14,7 @@ const resolve = require('./download/npm').resolve;
const {
REGISTRY_TYPES,
} = require('./npa_types');
const { runLifecycleScripts } = require('./lifecycle_scripts');

module.exports = install;

Expand Down Expand Up @@ -51,7 +51,7 @@ async function _install(parentDir, pkg, ancestors, options, context) {
options.progresses.installTasks,
pkg.name, pkg.version, parentDir);
if (options.spinner) {
options.spinner.text = `[${options.progresses.finishedInstallTasks}/${options.progresses.installTasks}] Installing ${pkg.name}@${pkg.version}`;
options.spinner.text = `[${options.progresses.finishedInstallTasks}/${options.progresses.installTasks}] Installing ${pkg.name}@${pkg.version}${os.EOL}`;
}
let p = npa(pkg.name ? `${pkg.name}@${pkg.version}` : pkg.version, { where: options.root, nested: context.nested });
const displayName = p.displayName = utils.getDisplayName(pkg, ancestors);
Expand Down Expand Up @@ -214,7 +214,6 @@ async function _install(parentDir, pkg, ancestors, options, context) {
}
}

await preinstall(realPkg, realPkgDir, displayName, options);
// link bundleDependencies' bin
// npminstall fsevents
const bundledDependencies = await getBundleDependencies(realPkg, realPkgDir);
Expand Down Expand Up @@ -292,7 +291,6 @@ async function _install(parentDir, pkg, ancestors, options, context) {
}
// FIXME: run postinstall before link may cause incompatible error. see
// arborist/reify.js#steps._build, npm runs postinstall after unpacking.
await postinstall(realPkg, realPkgDir, pkg.optional, displayName, options);
} catch (err) {
// delete donefile when install error, make sure this package won't be skipped during next installation.
try {
Expand All @@ -316,6 +314,10 @@ async function _install(parentDir, pkg, ancestors, options, context) {
realPkg.version,
realPkgDir);

if (!options.ignoreScripts) {
await runLifecycleScripts(realPkg, realPkgDir, pkg, displayName, options);
}

return {
exists: false,
dir: realPkgDir,
Expand Down
86 changes: 86 additions & 0 deletions lib/lifecycle_scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const chalk = require('chalk');
const path = require('path');
const os = require('os');
const npa = require('npm-package-arg');
const ms = require('ms');
const { LOCAL_TYPES } = require('./npa_types');
const utils = require('./utils');

// scripts that should run in root package and linked package
exports.DEFAULT_ROOT_SCRIPTS = [
'preinstall',
'install',
'postinstall',
'prepublish',
'preprepare',
'prepare',
'postprepare',
];

// scripts that should run in dependencies
exports.DEFAULT_DEP_SCRIPTS = [
'preinstall',
'install',
'postinstall',
];

exports.runLifecycleScripts = async function runLifecycleScripts(pkg, root, originPkg, displayName, options) {
const scripts = pkg.scripts || {};

// https://docs.npmjs.com/misc/scripts#default-values
// "install": "node-gyp rebuild"
// If there is a binding.gyp file in the root of your package,
// npm will default the install command to compile using node-gyp.
if (!scripts.install && (await utils.exists(path.join(root, 'binding.gyp')))) {
options.console.warn(
'[npminstall:runscript] %s found binding.gyp file, auto run "node-gyp rebuild", root: %j',
displayName, root
);
scripts.install = 'node-gyp rebuild';
}

let scriptList = exports.DEFAULT_DEP_SCRIPTS;

if (root === options.root || LOCAL_TYPES.includes(npa(`${originPkg.name}@${originPkg.version}`).type)) {
scriptList = exports.DEFAULT_ROOT_SCRIPTS;
}

for (const script of scriptList) {
const cmd = scripts[script];
if (!cmd) {
continue;
}

console.info(
'> %s %s %s %s> %s',
displayName,
script,
root,
os.EOL,
cmd
);
const startTime = Date.now();
try {
await utils.runScript(root, cmd, options);
} catch (error) {
options.console.warn('[npminstall:runscript:error] %s run %s %s error: %s', chalk.red(displayName), script, cmd, error);
// If post install execute error, make sure this package won't be skipped during next installation.
try {
await utils.unsetInstallDone(root);
} catch (e) {
options.console.warn(chalk.yellow(`unsetInstallDone: ${root} error: ${e}, ignore it`));
}
if (originPkg.optional) {
options.console.warn(chalk.red('%s optional error: %s'), displayName, error.stack);
continue;
}
error.message = `run ${script} error, please remove node_modules before retry!\n${error.message}`;
throw error;
} finally {
const ts = Date.now() - startTime;
console.info('> %s %s, finished in %s', displayName, script, ms(ts));
options.runscriptCount += 1;
options.runscriptTime += ts;
}
}
};
121 changes: 14 additions & 107 deletions lib/local_install.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ const { writeFileSync } = require('fs');
const moment = require('moment');
const util = require('util');
const utils = require('./utils');
const postinstall = require('./postinstall');
const preinstall = require('./preinstall');
const prepublish = require('./prepublish');
const prepare = require('./prepare');
const installPackage = require('./install_package');
const dependencies = require('./dependencies');
const createResolution = require('./resolution');
const formatInstallOptions = require('./format_install_options');
const Context = require('./context');
const { runLifecycleScripts } = require('./lifecycle_scripts');

/**
* npm install
Expand Down Expand Up @@ -102,8 +99,6 @@ module.exports = async (options, context = new Context()) => {
}
};

exports.runPostInstallTasks = runPostInstallTasks;

async function _install(options, context) {
const rootPkgFile = path.join(options.root, 'package.json');
const rootPkg = await utils.readJSON(rootPkgFile);
Expand Down Expand Up @@ -133,10 +128,6 @@ async function _install(options, context) {
}
}

// trigger root project's preinstall hook only when executing a full installation.
// e.g. calling npminstall with no arguments
if (options.installRoot) await preinstall(rootPkg, options.root, displayName, options);

context.nested.update(pkgs.map(pkg => `${pkg.name}@${pkg.version}`), rootPkg.name && rootPkg.version ? displayName : 'root');
const nodeModulesDir = path.join(options.targetDir, 'node_modules');
await utils.mkdirp(nodeModulesDir);
Expand All @@ -151,7 +142,6 @@ async function _install(options, context) {
// multi-thread installation
await pMap(pkgs, mapper, 10);
options.downloadFinished = Date.now();
options.spinner && options.spinner.succeed(`Installed ${pkgs.length} packages on ${options.root}`);

if (!options.disableFallbackStore) {
// link every packages' latest version to <root>/node_modules/.store/node_modules, fallback for peerDeps
Expand All @@ -173,18 +163,9 @@ async function _install(options, context) {
}
}

// run postinstall script if exist
if (options.installRoot) await postinstall(rootPkg, options.root, false, displayName, options);
// run dependencies' postinstall scripts
await runPostInstallTasks(options);

// local install trigger prepublish / prepare when no packages and non-production mode
// prepare is run after prepublish
// see:
// - https://docs.npmjs.com/misc/scripts
// - https://github.com/npm/npm/issues/3059#issuecomment-32057292
if (options.installRoot && !options.production) await prepublish(rootPkg, options.root, options);
if (options.installRoot && !options.production) await prepare(rootPkg, options.root, options);
if (options.installRoot && !options.ignoreScripts) {
await runLifecycleScripts(rootPkg, options.root, { optional: false }, displayName, options);
}

// link peerDependencies if not match the version in target directory
await linkPeer(options);
Expand All @@ -201,6 +182,16 @@ async function _install(options, context) {
// record dependencies tree resolved from npm
recordDependenciesTree(options);

if (!options.ignoreScripts && options.runscriptCount > 0) {
const runscriptInfo = util.format('Run %s script(s) in %s.', options.runscriptCount, ms(options.runscriptTime));
if (options.spinner) {
options.spinner.succeed(runscriptInfo);
} else {
console.info(runscriptInfo);
}
}

options.spinner?.succeed(`Installed ${pkgs.length} packages on ${options.root}`);
// print install finished
finishInstall(options);

Expand Down Expand Up @@ -398,90 +389,6 @@ async function linkLatestVersion(pkg, storeDir, options, isFallback = false) {
pkg.name, pkg.version, linkDir, relative);
}

async function runPostInstallTasks(options) {
let count = 0;
const total = options.postInstallTasks.length;
if (total && options.ignoreScripts) {
options.console.warn(chalk.yellow('ignore all post install scripts'));
return;
}

if (total) {
options.console.log(chalk.yellow(`execute post install ${total} scripts...`));
}

for (const task of options.postInstallTasks) {
count++;
const pkg = task.pkg;
const root = task.root;
const displayName = task.displayName;
const installScript = pkg.scripts.install;
const postinstallScript = pkg.scripts.postinstall;
try {
if (installScript) {
options.console.warn(
'%s %s run %j, root: %j',
chalk.yellow(`[${count}/${total}] scripts.install`),
chalk.gray(displayName),
installScript,
root
);
const start = Date.now();
try {
await utils.runScript(root, installScript, options);
} catch (err) {
options.console.warn('[npminstall:runscript:error] %s scripts.install run %j error: %s',
chalk.red(displayName), installScript, err);
throw err;
}
options.console.warn(
'%s %s finished in %s',
chalk.yellow(`[${count}/${total}] scripts.install`),
chalk.gray(displayName),
ms(Date.now() - start)
);
}
if (postinstallScript) {
options.console.warn(
'%s %s run %j, root: %j',
chalk.yellow(`[${count}/${total}] scripts.postinstall`),
chalk.gray(displayName),
postinstallScript,
root
);
const start = Date.now();
try {
await utils.runScript(root, postinstallScript, options);
} catch (err) {
options.console.warn('[npminstall:runscript:error] %s scripts.postinstall run %j error: %s',
chalk.red(displayName), postinstallScript, err);
throw err;
}
options.console.warn(
'%s %s finished in %s',
chalk.yellow(`[${count}/${total}] scripts.postinstall`),
chalk.gray(displayName),
ms(Date.now() - start)
);
}
} catch (err) {
// If post install execute error, make sure this package won't be skipped during next installation.
try {
await utils.unsetInstallDone(root);
} catch (e) {
options.console.warn(chalk.yellow(`unsetInstallDone: ${root} error: ${e}, ignore it`));
}
if (task.optional) {
console.warn(chalk.red('%s optional error: %s'), displayName, err.stack);
continue;
}
err.message = `post install error, please remove node_modules before retry!\n${err.message}`;
throw err;
}
}
options.spinner && options.spinner.succeed(`Run ${options.postInstallTasks.length} scripts`);
}

function printPendingMessages(options) {
for (const item of options.pendingMessages) {
if (options.console[item[0]] && options.console[item[0]] !== debug) {
Expand Down
Loading

0 comments on commit f48e7e3

Please sign in to comment.