Skip to content
This repository was archived by the owner on Aug 11, 2021. It is now read-only.

Commit 67adc2d

Browse files
mikesherovzkat
authored andcommitted
fix(hooks): run .hooks scripts even if package.json script is not present (#13)
Fixes: npm/npm#19258
1 parent 5c70e52 commit 67adc2d

File tree

4 files changed

+97
-35
lines changed

4 files changed

+97
-35
lines changed

index.js

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const byline = require('byline')
1616
const resolveFrom = require('resolve-from')
1717

1818
const DEFAULT_NODE_GYP_PATH = resolveFrom(__dirname, 'node-gyp/bin/node-gyp')
19+
const hookStatCache = new Map()
1920

2021
let PATH = 'PATH'
2122

@@ -33,6 +34,20 @@ function logid (pkg, stage) {
3334
return pkg._id + '~' + stage + ':'
3435
}
3536

37+
function hookStat (dir, stage, cb) {
38+
const hook = path.join(dir, '.hooks', stage)
39+
const cachedStatError = hookStatCache.get(hook)
40+
41+
if (cachedStatError === undefined) {
42+
return fs.stat(hook, function (statError) {
43+
hookStatCache.set(hook, statError)
44+
cb(statError)
45+
})
46+
}
47+
48+
return setImmediate(() => cb(cachedStatError))
49+
}
50+
3651
function lifecycle (pkg, stage, wd, opts) {
3752
return new Promise((resolve, reject) => {
3853
while (pkg && pkg._data) pkg = pkg._data
@@ -46,32 +61,36 @@ function lifecycle (pkg, stage, wd, opts) {
4661
delete pkg.scripts.prepublish
4762
}
4863

49-
if (!pkg.scripts[stage]) return resolve()
50-
51-
validWd(wd || path.resolve(opts.dir, pkg.name), function (er, wd) {
52-
if (er) return reject(er)
64+
hookStat(opts.dir, stage, function (statError) {
65+
// makeEnv is a slow operation. This guard clause prevents makeEnv being called
66+
// and avoids a ton of unnecessary work, and results in a major perf boost.
67+
if (!pkg.scripts[stage] && statError) return resolve()
5368

54-
if ((wd.indexOf(opts.dir) !== 0 || _incorrectWorkingDirectory(wd, pkg)) &&
55-
!opts.unsafePerm && pkg.scripts[stage]) {
56-
opts.log.warn('lifecycle', logid(pkg, stage), 'cannot run in wd', pkg._id, pkg.scripts[stage], `(wd=${wd})`)
57-
return resolve()
58-
}
59-
60-
// set the env variables, then run scripts as a child process.
61-
var env = makeEnv(pkg, opts)
62-
env.npm_lifecycle_event = stage
63-
env.npm_node_execpath = env.NODE = env.NODE || process.execPath
64-
env.npm_execpath = require.main.filename
65-
env.INIT_CWD = process.cwd()
66-
env.npm_config_node_gyp = env.npm_config_node_gyp || DEFAULT_NODE_GYP_PATH
69+
validWd(wd || path.resolve(opts.dir, pkg.name), function (er, wd) {
70+
if (er) return reject(er)
6771

68-
// 'nobody' typically doesn't have permission to write to /tmp
69-
// even if it's never used, sh freaks out.
70-
if (!opts.unsafePerm) env.TMPDIR = wd
72+
if ((wd.indexOf(opts.dir) !== 0 || _incorrectWorkingDirectory(wd, pkg)) &&
73+
!opts.unsafePerm && pkg.scripts[stage]) {
74+
opts.log.warn('lifecycle', logid(pkg, stage), 'cannot run in wd', pkg._id, pkg.scripts[stage], `(wd=${wd})`)
75+
return resolve()
76+
}
7177

72-
lifecycle_(pkg, stage, wd, opts, env, (er) => {
73-
if (er) return reject(er)
74-
return resolve()
78+
// set the env variables, then run scripts as a child process.
79+
var env = makeEnv(pkg, opts)
80+
env.npm_lifecycle_event = stage
81+
env.npm_node_execpath = env.NODE = env.NODE || process.execPath
82+
env.npm_execpath = require.main.filename
83+
env.INIT_CWD = process.cwd()
84+
env.npm_config_node_gyp = env.npm_config_node_gyp || DEFAULT_NODE_GYP_PATH
85+
86+
// 'nobody' typically doesn't have permission to write to /tmp
87+
// even if it's never used, sh freaks out.
88+
if (!opts.unsafePerm) env.TMPDIR = wd
89+
90+
lifecycle_(pkg, stage, wd, opts, env, (er) => {
91+
if (er) return reject(er)
92+
return resolve()
93+
})
7594
})
7695
})
7796
})
@@ -131,8 +150,8 @@ function lifecycle_ (pkg, stage, wd, opts, env, cb) {
131150

132151
chain(
133152
[
134-
packageLifecycle && [runPackageLifecycle, pkg, env, wd, opts],
135-
[runHookLifecycle, pkg, env, wd, opts]
153+
packageLifecycle && [runPackageLifecycle, pkg, stage, env, wd, opts],
154+
[runHookLifecycle, pkg, stage, env, wd, opts]
136155
],
137156
done
138157
)
@@ -185,9 +204,8 @@ function validWd (d, cb) {
185204
})
186205
}
187206

188-
function runPackageLifecycle (pkg, env, wd, opts, cb) {
207+
function runPackageLifecycle (pkg, stage, env, wd, opts, cb) {
189208
// run package lifecycle scripts in the package root, or the nearest parent.
190-
var stage = env.npm_lifecycle_event
191209
var cmd = env.npm_lifecycle_script
192210

193211
var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd +
@@ -329,17 +347,13 @@ function runCmd_ (cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb_) {
329347
}
330348
}
331349

332-
function runHookLifecycle (pkg, env, wd, opts, cb) {
333-
// check for a hook script, run if present.
334-
var stage = env.npm_lifecycle_event
335-
var hook = path.join(opts.dir, '.hooks', stage)
336-
var cmd = hook
337-
338-
fs.stat(hook, function (er) {
350+
function runHookLifecycle (pkg, stage, env, wd, opts, cb) {
351+
hookStat(opts.dir, stage, function (er) {
339352
if (er) return cb()
353+
var cmd = path.join(opts.dir, '.hooks', stage)
340354
var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd +
341355
'\n> ' + cmd
342-
runCmd(note, hook, pkg, env, stage, wd, opts, cb)
356+
runCmd(note, cmd, pkg, env, stage, wd, opts, cb)
343357
})
344358
}
345359

test/fixtures/has-hooks/node_modules/.hooks/postinstall

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/has-hooks/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "has-hooks",
3+
"version": "1.0.0"
4+
}

test/index.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,48 @@ test('_incorrectWorkingDirectory: rejects wd from other packages', function (t)
7474
t.end()
7575
})
7676

77+
test('runs scripts from .hooks directory even if no script is present in package.json', function (t) {
78+
const fixture = path.join(__dirname, 'fixtures', 'has-hooks')
79+
80+
const verbose = sinon.spy()
81+
const silly = sinon.spy()
82+
const log = {
83+
level: 'silent',
84+
info: noop,
85+
warn: noop,
86+
silly,
87+
verbose,
88+
pause: noop,
89+
resume: noop,
90+
clearProgress: noop,
91+
showProgress: noop
92+
}
93+
const dir = path.join(fixture, 'node_modules')
94+
95+
const pkg = require(path.join(fixture, 'package.json'))
96+
97+
lifecycle(pkg, 'postinstall', fixture, {
98+
stdio: 'pipe',
99+
log,
100+
dir,
101+
config: {}
102+
})
103+
.then(() => {
104+
t.ok(
105+
verbose.calledWithMatch(
106+
'lifecycle',
107+
'undefined~postinstall:',
108+
'stdout',
109+
'ran hook'
110+
),
111+
'ran postinstall hook'
112+
)
113+
114+
t.end()
115+
})
116+
.catch(t.end)
117+
})
118+
77119
test("reports child's output", function (t) {
78120
const fixture = path.join(__dirname, 'fixtures', 'count-to-10')
79121

0 commit comments

Comments
 (0)