From e9b4214e1ddb1ad79fe6826cf2ce7ba385f0c274 Mon Sep 17 00:00:00 2001 From: nlf Date: Mon, 11 Jul 2022 13:35:53 -0700 Subject: [PATCH] feat(arborist): add support for dependencies script (#5094) feat: add support for dependencies script this is a new feature that will run the `dependencies` (as well as the `pre` and `post` versions) script any time an npm action makes a change to the installed dependency tree, whether it's adding a new dependency, removing one, or just shuffling things around to dedupe/optimize --- workspaces/arborist/lib/arborist/reify.js | 25 ++++++++++ workspaces/arborist/test/arborist/reify.js | 58 +++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index 4932c17d03667..faf016c704010 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -22,6 +22,7 @@ const moveFile = require('@npmcli/move-file') const rimraf = promisify(require('rimraf')) const PackageJson = require('@npmcli/package-json') const packageContents = require('@npmcli/installed-package-contents') +const runScript = require('@npmcli/run-script') const { checkEngine, checkPlatform } = require('npm-install-checks') const _force = Symbol.for('force') @@ -1516,6 +1517,30 @@ module.exports = cls => class Reifier extends cls { if (!this[_global]) { await this.actualTree.meta.save() + const ignoreScripts = !!this.options.ignoreScripts + // if we aren't doing a dry run or ignoring scripts and we actually made changes to the dep + // tree, then run the dependencies scripts + if (!this[_dryRun] && !ignoreScripts && this.diff && this.diff.children.length) { + const { path, package: pkg } = this.actualTree.target + const stdio = this.options.foregroundScripts ? 'inherit' : 'pipe' + const { scripts = {} } = pkg + for (const event of ['predependencies', 'dependencies', 'postdependencies']) { + if (Object.prototype.hasOwnProperty.call(scripts, event)) { + const timer = `reify:run:${event}` + process.emit('time', timer) + log.info('run', pkg._id, event, scripts[event]) + await runScript({ + event, + path, + pkg, + stdioString: true, + stdio, + scriptShell: this.options.scriptShell, + }) + process.emit('timeEnd', timer) + } + } + } } } } diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index a77b0fbb2c499..406b4281dc5be 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -1,4 +1,4 @@ -const { resolve, basename } = require('path') +const { join, resolve, basename } = require('path') const t = require('tap') const runScript = require('@npmcli/run-script') const localeCompare = require('@isaacs/string-locale-compare')('en') @@ -2467,6 +2467,62 @@ t.test('add local dep with existing dev + peer/optional', async t => { t.equal(tree.children.size, 1, 'children') }) +t.test('runs dependencies script if tree changes', async (t) => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + dependencies: { + abbrev: '^1.1.1', + }, + scripts: { + predependencies: `node -e "require('fs').writeFileSync('ran-predependencies', '')"`, + dependencies: `node -e "require('fs').writeFileSync('ran-dependencies', '')"`, + postdependencies: `node -e "require('fs').writeFileSync('ran-postdependencies', '')"`, + }, + }), + }) + + await reify(path) + + for (const script of ['predependencies', 'dependencies', 'postdependencies']) { + const expectedPath = join(path, `ran-${script}`) + t.ok(fs.existsSync(expectedPath), `ran ${script}`) + // delete the files after we assert they exist + fs.unlinkSync(expectedPath) + } + + // reify again without changing dependencies + await reify(path) + + for (const script of ['predependencies', 'dependencies', 'postdependencies']) { + const expectedPath = join(path, `ran-${script}`) + // and this time we assert that they do _not_ exist + t.not(fs.existsSync(expectedPath), `did not run ${script}`) + } + + // take over console.log as run-script is going to print a banner for these because + // they're running in the foreground + const _log = console.log + t.teardown(() => { + console.log = _log + }) + const logs = [] + console.log = (msg) => logs.push(msg) + // reify again, this time adding a new dependency + await reify(path, { foregroundScripts: true, add: ['once@^1.4.0'] }) + console.log = _log + + t.match(logs, [/predependencies/, /dependencies/, /postdependencies/], 'logged banners') + + // files should exist again + for (const script of ['predependencies', 'dependencies', 'postdependencies']) { + const expectedPath = join(path, `ran-${script}`) + t.ok(fs.existsSync(expectedPath), `ran ${script}`) + fs.unlinkSync(expectedPath) + } +}) + t.test('save package.json on update', t => { t.test('should save many deps in multiple package.json when using save=true', async t => { const path = fixture(t, 'workspaces-need-update')