forked from exogen/postinstall-build
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
393 lines (357 loc) · 14 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
var fs = require('fs')
var path = require('path')
var spawn = require('child_process').spawn
// Use `spawn` to make a better `exec` without stdio buffering.
function exec (command, options, callback) {
// Copied from npm's lib/utils/lifecycle.js
var sh = 'sh'
var shFlag = '-c'
// Doesn't need to support all options, we only ever pass these two.
var shOpts = {
env: options.env,
stdio: options.stdio || 'inherit'
}
// Copied from npm's lib/utils/lifecycle.js
if (process.platform === 'win32') {
sh = process.env.comspec || 'cmd'
shFlag = '/d /s /c'
shOpts.windowsVerbatimArguments = true
// Even though `command` may properly quote arguments that contain spaces,
// cmd.exe might strip them. Wrap the entire command in quotes, and the /s
// flag will remove them and leave the rest.
if (options.quote) {
command = '"' + command + '"'
}
}
var proc = spawn(sh, [shFlag, command], shOpts)
var done = false
function procKill () {
proc.kill()
}
function procDone (err) {
if (!done) {
done = true
process.removeListener('SIGTERM', procKill)
callback(err)
}
}
process.once('SIGTERM', procKill)
proc.on('error', procDone)
proc.on('close', function (code, signal) {
var err
if (code) {
// Behave like `exec` and construct an Error object.
var cmdStr = [sh, shFlag, command].join(' ')
err = new Error('Command failed: ' + cmdStr + '\n')
err.killed = signal != null
err.code = code
err.signal = signal
err.cmd = cmdStr
}
procDone(err)
})
return proc
}
// If we call `process.exit` immediately after logging to the console, then
// any process that is reading our output through a pipe would potentially get
// truncated output, because the pipe would be closed before it could be read.
// See: https://github.com/nodejs/node-v0.x-archive/issues/3737
function safeExit (code) {
process.on('exit', function () {
process.exit(code)
})
}
function postinstallBuild () {
var CWD = process.cwd()
var POSTINSTALL_BUILD_CWD = process.env.POSTINSTALL_BUILD_CWD || ''
// If we didn't have this check, then we'd be stuck in an infinite
// `postinstall` loop, since we run `npm install --only=dev` below,
// triggering another `postinstall`. We can't use `--ignore-scripts` because
// that ignores scripts in all the modules that get installed, too, which
// would break stuff. So instead, we set an environment variable,
// `POSTINSTALL_BUILD_CWD`, that keeps track of what we're installing. It's
// more than just a yes/no flag because the dev dependencies we're installing
// might use `postinstall-build` too, and we don't want the flag to prevent
// them from running.
if (POSTINSTALL_BUILD_CWD === CWD) {
return
}
var buildArtifact
var buildCommand
var flags = {
quote: false,
verbosity: 1
}
for (var i = 2; i < process.argv.length; i++) {
var arg = process.argv[i]
if (arg === '--silent') {
flags.verbosity = 0
} else if (arg === '--verbose') {
flags.verbosity = 2
} else if (arg === '--only-as-dependency') {
flags.onlyAsDependency = true
} else if (arg === '--script') {
// Consume the next argument.
flags.script = process.argv[++i]
} else if (arg.indexOf('--script=') === 0) {
flags.script = arg.slice(9)
} else if (buildArtifact == null) {
buildArtifact = arg
} else if (buildCommand == null) {
buildCommand = arg
}
}
// Some packages (e.g. `ember-cli`) install their own version of npm, which
// can shadow the expected version and break the package trying to use
// `postinstall-build`.
var npm = 'npm'
var execPath = process.env.npm_execpath
var userAgent = process.env.npm_config_user_agent || ''
var npmVersion = (userAgent.match(/^npm\/([^\s]+)/) || [])[1]
// If the user agent doesn't start with `npm/`, just fall back to running
// `npm` since alternative agents (e.g. Yarn) may not support the same
// commands (like `prune`).
if (execPath && npmVersion) {
npm = '"' + process.argv[0] + '" "' + execPath + '"'
}
if (flags.script != null) {
// Hopefully people aren't putting special characters that need escaping
// in their script names. If they are, they can take care of escaping it
// themselves when they supply `--script`.
flags.quote = true
buildCommand = npm + ' run ' + flags.script
} else if (buildCommand == null) {
// If no command or script was given, run the 'build' script.
flags.quote = true
buildCommand = npm + ' run build'
}
var _write = function (verbosity, message) {
if (flags.verbosity >= verbosity) {
process.stderr.write(message + '\n')
}
}
var log = {
info: _write.bind(this, 2),
warn: _write.bind(this, 1),
error: _write.bind(this, 0)
}
var handleError = function (err) {
log.error('postinstall-build:\n ' + err + '\n')
safeExit(1)
}
if (buildArtifact == null) {
return handleError('A build artifact must be supplied.')
} else if (/^(npm|yarn) /.test(buildArtifact)) {
log.warn(
"postinstall-build:\n '" + buildArtifact + "' is being passed as the " +
'build artifact, not the build command.\n If your build artifact is ' +
"a file or folder named '" + buildArtifact + "', you may ignore\n " +
'this warning. Otherwise, you probably meant to pass this as the build ' +
'command\n instead, and must supply a build artifact.\n'
)
}
// After building, we almost always want to prune back to the production
// dependencies, so that the transient development dependencies aren't
// left behind. The only reason we wouldn't want to prune is if we're the
// top-level package being `npm install`ed with no arguments for
// development or testing purposes. If we're the `postinstall` script of a
// dependency, we should always prune. Unfortunately, npm doesn't set
// any helpful environment variables to indicate whether we're being
// installed as a dependency or not. The best we can do is check whether
// the parent directory is `node_modules` but the package can be nested
// in multiple parent directories so we split apart the name of the module.
var parentDirs = CWD.split(path.sep)
var isDependency = parentDirs.indexOf('node_modules') !== -1
if (flags.onlyAsDependency && !isDependency) {
log.info(
'postinstall-build:\n Not installed as a dependency, skipping build.\n'
)
return
}
// If we're the top-level package being `npm install`ed with no arguments,
// we still might want to prune if certain flags indicate that only production
// dependencies were requested.
var isProduction = (process.env.npm_config_production === 'true' &&
process.env.npm_config_only !== 'development' &&
process.env.npm_config_only !== 'dev')
var isOnlyProduction = (process.env.npm_config_only === 'production' ||
process.env.npm_config_only === 'prod')
var shouldPrune = isDependency || isProduction || isOnlyProduction
var getInstallArgs = function () {
var packageFile = path.join(CWD, 'package.json')
var packageInfo = require(packageFile)
var devDependencies = packageInfo.devDependencies || {}
var buildDependencies = packageInfo.buildDependencies
var installArgs = ' --only=dev'
if (buildDependencies && Array.isArray(buildDependencies)) {
installArgs = buildDependencies.map(function (name) {
var spec = devDependencies[name]
// If a name specified in `buildDependencies` doesn't actually exist in
// `devDependencies`, it may be a global, peer, or production dependency.
// Assume it is already available. If a user puts something in
// `buildDependencies` and expects it to be installed by
// `postinstall-build`, then it must also be in `devDependencies`.
if (typeof spec === 'undefined') {
log.warn(
"postinstall-build:\n The dependency '" + name + "' appears in " +
'buildDependencies but not devDependencies.\n Instead of ' +
'installing it, postinstall-build will assume it is already ' +
'available.\n'
)
return ''
} else if (spec) {
// This previously used `npm-package-arg` to determine which specs are
// appropriate to use with the @ syntax and which aren't, but the latest
// version no longer works on Node <4, and older versions don't have the
// properties we want. So instead of trying to parse `spec`, just assume
// npm is okay with always using @ syntax.
return ' "' + name + '@' + spec + '"'
} else {
// We shouldn't really expect to find a null or empty spec, but it's
// technically possible, and npm will interpret it as `latest`.
return ' "' + name + '"'
}
}).join('')
}
return installArgs
}
var warnUserAgent = function () {
if (npmVersion && !/^(3\.|4\.0\.)/.test(npmVersion)) {
log.warn(
'postinstall-build:\n This version of npm (' + npmVersion + ') may ' +
'not be compatible with postinstall-build! There\n are outstanding ' +
'bugs in certain versions of npm that prevent it from working with\n ' +
'postinstall-build. See https://github.com/exogen/postinstall-build#bugs-in-npm\n ' +
'for more information.\n'
)
}
}
var checkBuildArtifact = function (callback) {
fs.stat(buildArtifact, function (err, stats) {
if (err || !(stats.isFile() || stats.isDirectory())) {
log.info(
'postinstall-build:\n Build artifact not found, proceeding to ' +
'build.\n'
)
callback(null, true)
} else {
log.info(
'postinstall-build:\n Build artifact found, skipping build.\n'
)
callback(null, false)
}
})
}
var checkUserAgent = function (callback) {
// If we know an incompatible version of npm triggered `postinstall-build`,
// print a warning. Note that if the original user agent is NOT npm, then we
// don't know what version `postinstall-build` will actually run, since the
// `$npm_config_user_agent` string didn't tell us, so run `npm --version` to
// find out.
if (npmVersion) {
warnUserAgent()
return callback(null, npmVersion)
}
var output = ''
var command = npm + ' --version'
log.info('postinstall-build:\n ' + command + '\n')
var proc = exec(command, { stdio: 'pipe' }, function (err) {
npmVersion = (output.match(/^(\d[^\s]*)/) || [])[1]
warnUserAgent()
callback(err, npmVersion)
})
proc.stdout.setEncoding('utf8')
proc.stdout.on('data', function (chunk) {
output += chunk
})
}
var installBuildDependencies = function (execOpts, callback) {
// We only need to install dependencies if `shouldPrune` is true. Why?
// Because this flag detects whether `devDependencies` were already
// installed in order to determine whether they need to be pruned at the
// end or not. And if we already have `devDependencies` then installing
// here isn't going to get us anything.
if (shouldPrune) {
// If `installArgs` is empty, the build doesn't depend on installing any
// extra dependencies.
var installArgs = getInstallArgs()
if (installArgs) {
var command = npm + ' install' + installArgs
log.info('postinstall-build:\n ' + command + '\n')
return exec(command, execOpts, callback)
}
log.info(
'postinstall-build:\n No install arguments, skipping install.\n'
)
}
log.info(
'postinstall-build:\n Already have devDependencies, skipping install.\n'
)
callback(null)
}
var runBuildCommand = function (execOpts, callback) {
// Only quote the build command if necessary, otherwise run it exactly
// as npm would.
execOpts.quote = flags.quote
log.info('postinstall-build:\n ' + buildCommand + '\n')
exec(buildCommand, execOpts, callback)
}
var cleanUp = function (execOpts, callback) {
if (shouldPrune) {
execOpts.quote = true
var command = npm + ' prune --production'
log.info('postinstall-build:\n ' + command + '\n')
return exec(command, execOpts, callback)
}
log.info('postinstall-build:\n Skipping prune.\n')
callback(null)
}
checkBuildArtifact(function (err, shouldBuild) {
if (err) {
return handleError(err)
}
if (shouldBuild) {
checkUserAgent(function (err) {
// If an error occurred, the only consequence is that we don't know
// which version of npm is going to be run, and can't issue a warning.
// If there's an actual problem running npm we'll find out soon enough.
if (err) {
log.warn('postinstall-build:\n ' + err + '\n')
}
// If `npm install` ends up being run by `installBuildDependencies`, this
// script will be run again. Set an environment variable to tell it to
// skip the check. Really we just want the spawned child's `env` to be
// modified, but it's easier just modify and pass along our entire
// `process.env`.
process.env.POSTINSTALL_BUILD_CWD = CWD
var execOpts = {
env: process.env,
quote: true
}
if (flags.verbosity === 0) {
execOpts.stdio = 'ignore'
}
installBuildDependencies(execOpts, function (err) {
if (err) {
return handleError(err)
}
// Don't need this flag anymore as `postinstall` was already run.
// Change it back so the environment is minimally changed for the
// remaining commands.
process.env.POSTINSTALL_BUILD_CWD = POSTINSTALL_BUILD_CWD
runBuildCommand(execOpts, function (err) {
if (err) {
return handleError(err)
}
cleanUp(execOpts, function (err) {
if (err) {
return handleError(err)
}
})
})
})
})
}
})
}
module.exports = postinstallBuild