├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── index.js ├── lib └── spawn.js ├── node-gyp-bin ├── node-gyp └── node-gyp.cmd ├── package-lock.json ├── package.json ├── tap-snapshots ├── test-get-spawn-args.js-TAP.test.js └── test-path-munging.js-TAP.test.js └── test ├── fixtures ├── count-to-10-working-postinstall │ ├── package.json │ └── postinstall.js ├── count-to-10 │ ├── package.json │ └── postinstall.js └── has-hooks │ ├── node_modules │ └── .hooks │ │ └── postinstall │ └── package.json ├── get-spawn-args.js ├── index.js └── path-munging.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.nyc_output 3 | /coverage 4 | /test/cache 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "12" 5 | - "10" 6 | - "8" 7 | - "6" 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [3.1.5](https://github.com/npm/lifecycle/compare/v3.1.4...v3.1.5) (2020-03-26) 7 | 8 | 9 | 10 | 11 | ## [3.1.4](https://github.com/npm/lifecycle/compare/v3.1.3...v3.1.4) (2019-09-18) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * filter functions and undefined out of makeEnv ([10c0c08](https://github.com/npm/lifecycle/commit/10c0c08)) 17 | 18 | 19 | 20 | 21 | ## [3.1.3](https://github.com/npm/lifecycle/compare/v3.1.2...v3.1.3) (2019-08-12) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * fail properly if uid-number raises an error ([e0e1b62](https://github.com/npm/lifecycle/commit/e0e1b62)) 27 | 28 | 29 | 30 | 31 | ## [3.1.2](https://github.com/npm/lifecycle/compare/v3.1.1...v3.1.2) (2019-07-22) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * do not exclude /path/ from process.env copying ([53e6318](https://github.com/npm/lifecycle/commit/53e6318)) 37 | 38 | 39 | 40 | 41 | # [3.1.0](https://github.com/npm/lifecycle/compare/v3.0.0...v3.1.0) (2019-07-17) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * remove procInterrupt listener on SIGINT in procError ([ea18ed2](https://github.com/npm/lifecycle/commit/ea18ed2)), closes [#36](https://github.com/npm/lifecycle/issues/36) [#11](https://github.com/npm/lifecycle/issues/11) [#18](https://github.com/npm/lifecycle/issues/18) 47 | * set only one PATH env variable for child proc ([3aaf954](https://github.com/npm/lifecycle/commit/3aaf954)), closes [#22](https://github.com/npm/lifecycle/issues/22) [#25](https://github.com/npm/lifecycle/issues/25) 48 | 49 | 50 | ### Features 51 | 52 | * **process.env.path:** Use platform specific path casing if present ([5523951](https://github.com/npm/lifecycle/commit/5523951)), closes [#29](https://github.com/npm/lifecycle/issues/29) [#30](https://github.com/npm/lifecycle/issues/30) 53 | 54 | 55 | 56 | 57 | # [3.0.0](https://github.com/npm/lifecycle/compare/v2.1.1...v3.0.0) (2019-07-10) 58 | 59 | 60 | * node-gyp@5.0.2 ([3c5aae6](https://github.com/npm/lifecycle/commit/3c5aae6)) 61 | 62 | 63 | ### BREAKING CHANGES 64 | 65 | * requires modifying the version of node-gyp in npm cli. 66 | 67 | Required for https://github.com/npm/cli/pull/208 68 | Fix: https://github.com/npm/npm-lifecycle/issues/37 69 | Close: https://github.com/npm/npm-lifecycle/issues/38 70 | PR-URL: https://github.com/npm/npm-lifecycle/pull/38 71 | Credit: @irega 72 | Reviewed-by: @isaacs 73 | 74 | 75 | 76 | 77 | ## [2.1.1](https://github.com/npm/lifecycle/compare/v2.1.0...v2.1.1) (2019-05-08) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * **test:** update postinstall script for fixture ([220cd70](https://github.com/npm/lifecycle/commit/220cd70)) 83 | 84 | 85 | 86 | 87 | # [2.1.0](https://github.com/npm/lifecycle/compare/v2.0.3...v2.1.0) (2018-08-13) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **windows:** revert writing all possible cases of PATH variables ([#22](https://github.com/npm/lifecycle/issues/22)) ([8fcaa21](https://github.com/npm/lifecycle/commit/8fcaa21)), closes [#20](https://github.com/npm/lifecycle/issues/20) 93 | 94 | 95 | ### Features 96 | 97 | * **ci:** add node@10 to CI ([e206aa0](https://github.com/npm/lifecycle/commit/e206aa0)) 98 | 99 | 100 | 101 | 102 | ## [2.0.3](https://github.com/npm/lifecycle/compare/v2.0.2...v2.0.3) (2018-05-16) 103 | 104 | 105 | 106 | 107 | ## [2.0.2](https://github.com/npm/lifecycle/compare/v2.0.1...v2.0.2) (2018-05-16) 108 | 109 | 110 | ### Bug Fixes 111 | 112 | * **hooks:** run .hooks scripts even if package.json script is not present ([#13](https://github.com/npm/lifecycle/issues/13)) ([67adc2d](https://github.com/npm/lifecycle/commit/67adc2d)) 113 | * **windows:** Write to all possible cases of PATH variables ([#17](https://github.com/npm/lifecycle/issues/17)) ([e4ecc54](https://github.com/npm/lifecycle/commit/e4ecc54)) 114 | 115 | 116 | 117 | 118 | ## [2.0.1](https://github.com/npm/lifecycle/compare/v2.0.0...v2.0.1) (2018-03-08) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * **log:** Fix formatting of invalid wd warning ([#12](https://github.com/npm/lifecycle/issues/12)) ([ced38f3](https://github.com/npm/lifecycle/commit/ced38f3)) 124 | 125 | 126 | 127 | 128 | # [2.0.0](https://github.com/npm/lifecycle/compare/v1.0.3...v2.0.0) (2017-11-17) 129 | 130 | 131 | ### Features 132 | 133 | * **node-gyp:** use own node-gyp ([ae94ed2](https://github.com/npm/lifecycle/commit/ae94ed2)) 134 | * **nodeOptions:** add "nodeOptions" option to set NODE_OPTIONS for child ([#7](https://github.com/npm/lifecycle/issues/7)) ([2eb7a38](https://github.com/npm/lifecycle/commit/2eb7a38)) 135 | * **stdio:** add child process io options and default logging of piped stdout/err ([#3](https://github.com/npm/lifecycle/issues/3)) ([7b8281a](https://github.com/npm/lifecycle/commit/7b8281a)) 136 | 137 | 138 | ### BREAKING CHANGES 139 | 140 | * **node-gyp:** Previously you had to bring your own node-gyp AND you had 141 | to provide access the way npm does, by having a `bin` dir with a 142 | `node-gyp-bin` in it. 143 | 144 | Fixes: #4 145 | 146 | 147 | 148 | 149 | ## [1.0.3](https://github.com/npm/lifecycle/compare/v1.0.2...v1.0.3) (2017-09-01) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * **runCmd:** add missing option to runCmd recursive queue call ([1a69ce8](https://github.com/npm/lifecycle/commit/1a69ce8)) 155 | 156 | 157 | 158 | 159 | ## [1.0.2](https://github.com/npm/lifecycle/compare/v1.0.1...v1.0.2) (2017-08-17) 160 | 161 | 162 | 163 | 164 | ## [1.0.1](https://github.com/npm/lifecycle/compare/v1.0.0...v1.0.1) (2017-08-16) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **license:** fix up license documentation ([a784ca0](https://github.com/npm/lifecycle/commit/a784ca0)) 170 | 171 | 172 | 173 | 174 | # 1.0.0 (2017-08-16) 175 | 176 | 177 | ### Bug Fixes 178 | 179 | * **misc:** use strict to fix node[@4](https://github.com/4) ([#2](https://github.com/npm/lifecycle/issues/2)) ([961ceb9](https://github.com/npm/lifecycle/commit/961ceb9)) 180 | 181 | 182 | ### Features 183 | 184 | * **api:** Extract from npm proper ([#1](https://github.com/npm/lifecycle/issues/1)) ([27d9930](https://github.com/npm/lifecycle/commit/27d9930)) 185 | 186 | 187 | ### BREAKING CHANGES 188 | 189 | * **api:** this is the initial implementation 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The npm application 2 | Copyright (c) npm, Inc. and Contributors 3 | Licensed on the terms of The Artistic License 2.0 4 | 5 | Node package dependencies of the npm application 6 | Copyright (c) their respective copyright owners 7 | Licensed on their respective license terms 8 | 9 | The npm public registry at https://registry.npmjs.org 10 | and the npm website at https://www.npmjs.com 11 | Operated by npm, Inc. 12 | Use governed by terms published on https://www.npmjs.com 13 | 14 | "Node.js" 15 | Trademark Joyent, Inc., https://joyent.com 16 | Neither npm nor npm, Inc. are affiliated with Joyent, Inc. 17 | 18 | The Node.js application 19 | Project of Node Foundation, https://nodejs.org 20 | 21 | The npm Logo 22 | Copyright (c) Mathias Pettersson and Brian Hammond 23 | 24 | "Gubblebum Blocky" typeface 25 | Copyright (c) Tjarda Koster, https://jelloween.deviantart.com 26 | Used with permission 27 | 28 | 29 | -------- 30 | 31 | 32 | The Artistic License 2.0 33 | 34 | Copyright (c) 2000-2006, The Perl Foundation. 35 | 36 | Everyone is permitted to copy and distribute verbatim copies 37 | of this license document, but changing it is not allowed. 38 | 39 | Preamble 40 | 41 | This license establishes the terms under which a given free software 42 | Package may be copied, modified, distributed, and/or redistributed. 43 | The intent is that the Copyright Holder maintains some artistic 44 | control over the development of that Package while still keeping the 45 | Package available as open source and free software. 46 | 47 | You are always permitted to make arrangements wholly outside of this 48 | license directly with the Copyright Holder of a given Package. If the 49 | terms of this license do not permit the full use that you propose to 50 | make of the Package, you should contact the Copyright Holder and seek 51 | a different licensing arrangement. 52 | 53 | Definitions 54 | 55 | "Copyright Holder" means the individual(s) or organization(s) 56 | named in the copyright notice for the entire Package. 57 | 58 | "Contributor" means any party that has contributed code or other 59 | material to the Package, in accordance with the Copyright Holder's 60 | procedures. 61 | 62 | "You" and "your" means any person who would like to copy, 63 | distribute, or modify the Package. 64 | 65 | "Package" means the collection of files distributed by the 66 | Copyright Holder, and derivatives of that collection and/or of 67 | those files. A given Package may consist of either the Standard 68 | Version, or a Modified Version. 69 | 70 | "Distribute" means providing a copy of the Package or making it 71 | accessible to anyone else, or in the case of a company or 72 | organization, to others outside of your company or organization. 73 | 74 | "Distributor Fee" means any fee that you charge for Distributing 75 | this Package or providing support for this Package to another 76 | party. It does not mean licensing fees. 77 | 78 | "Standard Version" refers to the Package if it has not been 79 | modified, or has been modified only in ways explicitly requested 80 | by the Copyright Holder. 81 | 82 | "Modified Version" means the Package, if it has been changed, and 83 | such changes were not explicitly requested by the Copyright 84 | Holder. 85 | 86 | "Original License" means this Artistic License as Distributed with 87 | the Standard Version of the Package, in its current version or as 88 | it may be modified by The Perl Foundation in the future. 89 | 90 | "Source" form means the source code, documentation source, and 91 | configuration files for the Package. 92 | 93 | "Compiled" form means the compiled bytecode, object code, binary, 94 | or any other form resulting from mechanical transformation or 95 | translation of the Source form. 96 | 97 | 98 | Permission for Use and Modification Without Distribution 99 | 100 | (1) You are permitted to use the Standard Version and create and use 101 | Modified Versions for any purpose without restriction, provided that 102 | you do not Distribute the Modified Version. 103 | 104 | 105 | Permissions for Redistribution of the Standard Version 106 | 107 | (2) You may Distribute verbatim copies of the Source form of the 108 | Standard Version of this Package in any medium without restriction, 109 | either gratis or for a Distributor Fee, provided that you duplicate 110 | all of the original copyright notices and associated disclaimers. At 111 | your discretion, such verbatim copies may or may not include a 112 | Compiled form of the Package. 113 | 114 | (3) You may apply any bug fixes, portability changes, and other 115 | modifications made available from the Copyright Holder. The resulting 116 | Package will still be considered the Standard Version, and as such 117 | will be subject to the Original License. 118 | 119 | 120 | Distribution of Modified Versions of the Package as Source 121 | 122 | (4) You may Distribute your Modified Version as Source (either gratis 123 | or for a Distributor Fee, and with or without a Compiled form of the 124 | Modified Version) provided that you clearly document how it differs 125 | from the Standard Version, including, but not limited to, documenting 126 | any non-standard features, executables, or modules, and provided that 127 | you do at least ONE of the following: 128 | 129 | (a) make the Modified Version available to the Copyright Holder 130 | of the Standard Version, under the Original License, so that the 131 | Copyright Holder may include your modifications in the Standard 132 | Version. 133 | 134 | (b) ensure that installation of your Modified Version does not 135 | prevent the user installing or running the Standard Version. In 136 | addition, the Modified Version must bear a name that is different 137 | from the name of the Standard Version. 138 | 139 | (c) allow anyone who receives a copy of the Modified Version to 140 | make the Source form of the Modified Version available to others 141 | under 142 | 143 | (i) the Original License or 144 | 145 | (ii) a license that permits the licensee to freely copy, 146 | modify and redistribute the Modified Version using the same 147 | licensing terms that apply to the copy that the licensee 148 | received, and requires that the Source form of the Modified 149 | Version, and of any works derived from it, be made freely 150 | available in that license fees are prohibited but Distributor 151 | Fees are allowed. 152 | 153 | 154 | Distribution of Compiled Forms of the Standard Version 155 | or Modified Versions without the Source 156 | 157 | (5) You may Distribute Compiled forms of the Standard Version without 158 | the Source, provided that you include complete instructions on how to 159 | get the Source of the Standard Version. Such instructions must be 160 | valid at the time of your distribution. If these instructions, at any 161 | time while you are carrying out such distribution, become invalid, you 162 | must provide new instructions on demand or cease further distribution. 163 | If you provide valid instructions or cease distribution within thirty 164 | days after you become aware that the instructions are invalid, then 165 | you do not forfeit any of your rights under this license. 166 | 167 | (6) You may Distribute a Modified Version in Compiled form without 168 | the Source, provided that you comply with Section 4 with respect to 169 | the Source of the Modified Version. 170 | 171 | 172 | Aggregating or Linking the Package 173 | 174 | (7) You may aggregate the Package (either the Standard Version or 175 | Modified Version) with other packages and Distribute the resulting 176 | aggregation provided that you do not charge a licensing fee for the 177 | Package. Distributor Fees are permitted, and licensing fees for other 178 | components in the aggregation are permitted. The terms of this license 179 | apply to the use and Distribution of the Standard or Modified Versions 180 | as included in the aggregation. 181 | 182 | (8) You are permitted to link Modified and Standard Versions with 183 | other works, to embed the Package in a larger work of your own, or to 184 | build stand-alone binary or bytecode versions of applications that 185 | include the Package, and Distribute the result without restriction, 186 | provided the result does not expose a direct interface to the Package. 187 | 188 | 189 | Items That are Not Considered Part of a Modified Version 190 | 191 | (9) Works (including, but not limited to, modules and scripts) that 192 | merely extend or make use of the Package, do not, by themselves, cause 193 | the Package to be a Modified Version. In addition, such works are not 194 | considered parts of the Package itself, and are not subject to the 195 | terms of this license. 196 | 197 | 198 | General Provisions 199 | 200 | (10) Any use, modification, and distribution of the Standard or 201 | Modified Versions is governed by this Artistic License. By using, 202 | modifying or distributing the Package, you accept this license. Do not 203 | use, modify, or distribute the Package, if you do not accept this 204 | license. 205 | 206 | (11) If your Modified Version has been derived from a Modified 207 | Version made by someone other than you, you are nevertheless required 208 | to ensure that your Modified Version complies with the requirements of 209 | this license. 210 | 211 | (12) This license does not grant you the right to use any trademark, 212 | service mark, tradename, or logo of the Copyright Holder. 213 | 214 | (13) This license includes the non-exclusive, worldwide, 215 | free-of-charge patent license to make, have made, use, offer to sell, 216 | sell, import and otherwise transfer the Package with respect to any 217 | patent claims licensable by the Copyright Holder that are necessarily 218 | infringed by the Package. If you institute patent litigation 219 | (including a cross-claim or counterclaim) against any party alleging 220 | that the Package constitutes direct or contributory patent 221 | infringement, then this Artistic License to you shall terminate on the 222 | date that such litigation is filed. 223 | 224 | (14) Disclaimer of Warranty: 225 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 226 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 227 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 228 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 229 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 230 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 231 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 232 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 233 | 234 | 235 | -------- 236 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note: pending imminent deprecation 2 | 3 | **This module will be deprecated once npm v7 is released. Please do not rely 4 | on it more than absolutely necessary.** 5 | 6 | The lifecycle script runner used in npm v7 is 7 | [@npmcli/run-script](http://npm.im/@npmcli/run-script). Please use that 8 | module moving forward. 9 | 10 | ----- 11 | 12 | # npm-lifecycle 13 | 14 | [`npm-lifecycle`](https://github.com/npm/npm-lifecycle) is a standalone library for 15 | executing packages' lifecycle scripts. It is extracted from npm itself and 16 | intended to be fully compatible with the way npm executes individual scripts. 17 | 18 | ## Install 19 | 20 | `$ npm install npm-lifecycle` 21 | 22 | ## Table of Contents 23 | 24 | * [Example](#example) 25 | * [Features](#features) 26 | * [Contributing](#contributing) 27 | * [API](#api) 28 | * [`lifecycle`](#lifecycle) 29 | 30 | ### Example 31 | 32 | ```javascript 33 | // idk yet 34 | ``` 35 | 36 | ### API 37 | 38 | #### `> lifecycle(name, pkg, wd, [opts]) -> Promise` 39 | 40 | ##### Arguments 41 | 42 | * `opts.stdio` - the [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) 43 | passed to the child process. `[0, 1, 2]` by default. 44 | 45 | ##### Example 46 | 47 | ```javascript 48 | lifecycle() 49 | ``` 50 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "10" 4 | - nodejs_version: "8" 5 | - nodejs_version: "6" 6 | - nodejs_version: "11" 7 | 8 | platform: 9 | - x64 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version $env:platform 13 | - npm config set spin false 14 | - npm i -g npm@latest 15 | - npm install 16 | 17 | test_script: 18 | - npm test 19 | 20 | matrix: 21 | fast_finish: true 22 | 23 | build: off 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports = module.exports = lifecycle 4 | exports.makeEnv = makeEnv 5 | exports._incorrectWorkingDirectory = _incorrectWorkingDirectory 6 | 7 | // for testing 8 | const platform = process.env.__TESTING_FAKE_PLATFORM__ || process.platform 9 | const isWindows = platform === 'win32' 10 | const spawn = require('./lib/spawn') 11 | const path = require('path') 12 | const Stream = require('stream').Stream 13 | const fs = require('graceful-fs') 14 | const chain = require('slide').chain 15 | const uidNumber = require('uid-number') 16 | const umask = require('umask') 17 | const which = require('which') 18 | const byline = require('byline') 19 | const resolveFrom = require('resolve-from') 20 | 21 | const DEFAULT_NODE_GYP_PATH = resolveFrom(__dirname, 'node-gyp/bin/node-gyp') 22 | const hookStatCache = new Map() 23 | 24 | let PATH = isWindows ? 'Path' : 'PATH' 25 | exports._pathEnvName = PATH 26 | const delimiter = path.delimiter 27 | 28 | // windows calls its path 'Path' usually, but this is not guaranteed. 29 | // merge them all together in the order they appear in the object. 30 | const mergePath = env => 31 | Object.keys(env).filter(p => /^path$/i.test(p) && env[p]) 32 | .map(p => env[p].split(delimiter)) 33 | .reduce((set, p) => set.concat(p.filter(p => !set.includes(p))), []) 34 | .join(delimiter) 35 | exports._mergePath = mergePath 36 | 37 | const setPathEnv = (env, path) => { 38 | // first ensure that the canonical value is set. 39 | env[PATH] = path 40 | // also set any other case values, because windows. 41 | Object.keys(env) 42 | .filter(p => p !== PATH && /^path$/i.test(p)) 43 | .forEach(p => { env[p] = path }) 44 | } 45 | exports._setPathEnv = setPathEnv 46 | 47 | function logid (pkg, stage) { 48 | return pkg._id + '~' + stage + ':' 49 | } 50 | 51 | function hookStat (dir, stage, cb) { 52 | const hook = path.join(dir, '.hooks', stage) 53 | const cachedStatError = hookStatCache.get(hook) 54 | 55 | if (cachedStatError === undefined) { 56 | return fs.stat(hook, function (statError) { 57 | hookStatCache.set(hook, statError) 58 | cb(statError) 59 | }) 60 | } 61 | 62 | return setImmediate(() => cb(cachedStatError)) 63 | } 64 | 65 | function lifecycle (pkg, stage, wd, opts) { 66 | return new Promise((resolve, reject) => { 67 | while (pkg && pkg._data) pkg = pkg._data 68 | if (!pkg) return reject(new Error('Invalid package data')) 69 | 70 | opts.log.info('lifecycle', logid(pkg, stage), pkg._id) 71 | if (!pkg.scripts) pkg.scripts = {} 72 | 73 | if (stage === 'prepublish' && opts.ignorePrepublish) { 74 | opts.log.info('lifecycle', logid(pkg, stage), 'ignored because ignore-prepublish is set to true', pkg._id) 75 | delete pkg.scripts.prepublish 76 | } 77 | 78 | hookStat(opts.dir, stage, function (statError) { 79 | // makeEnv is a slow operation. This guard clause prevents makeEnv being called 80 | // and avoids a ton of unnecessary work, and results in a major perf boost. 81 | if (!pkg.scripts[stage] && statError) return resolve() 82 | 83 | validWd(wd || path.resolve(opts.dir, pkg.name), function (er, wd) { 84 | if (er) return reject(er) 85 | 86 | if ((wd.indexOf(opts.dir) !== 0 || _incorrectWorkingDirectory(wd, pkg)) && 87 | !opts.unsafePerm && pkg.scripts[stage]) { 88 | opts.log.warn('lifecycle', logid(pkg, stage), 'cannot run in wd', pkg._id, pkg.scripts[stage], `(wd=${wd})`) 89 | return resolve() 90 | } 91 | 92 | // set the env variables, then run scripts as a child process. 93 | var env = makeEnv(pkg, opts) 94 | env.npm_lifecycle_event = stage 95 | env.npm_node_execpath = env.NODE = env.NODE || process.execPath 96 | env.npm_execpath = require.main.filename 97 | env.INIT_CWD = process.cwd() 98 | env.npm_config_node_gyp = env.npm_config_node_gyp || DEFAULT_NODE_GYP_PATH 99 | 100 | // 'nobody' typically doesn't have permission to write to /tmp 101 | // even if it's never used, sh freaks out. 102 | if (!opts.unsafePerm) env.TMPDIR = wd 103 | 104 | lifecycle_(pkg, stage, wd, opts, env, (er) => { 105 | if (er) return reject(er) 106 | return resolve() 107 | }) 108 | }) 109 | }) 110 | }) 111 | } 112 | 113 | function _incorrectWorkingDirectory (wd, pkg) { 114 | return wd.lastIndexOf(pkg.name) !== wd.length - pkg.name.length 115 | } 116 | 117 | function lifecycle_ (pkg, stage, wd, opts, env, cb) { 118 | var pathArr = [] 119 | var p = wd.split(/[\\/]node_modules[\\/]/) 120 | var acc = path.resolve(p.shift()) 121 | 122 | p.forEach(function (pp) { 123 | pathArr.unshift(path.join(acc, 'node_modules', '.bin')) 124 | acc = path.join(acc, 'node_modules', pp) 125 | }) 126 | pathArr.unshift(path.join(acc, 'node_modules', '.bin')) 127 | 128 | // we also unshift the bundled node-gyp-bin folder so that 129 | // the bundled one will be used for installing things. 130 | pathArr.unshift(path.join(__dirname, 'node-gyp-bin')) 131 | 132 | if (shouldPrependCurrentNodeDirToPATH(opts)) { 133 | // prefer current node interpreter in child scripts 134 | pathArr.push(path.dirname(process.execPath)) 135 | } 136 | 137 | const existingPath = mergePath(env) 138 | if (existingPath) pathArr.push(existingPath) 139 | const envPath = pathArr.join(isWindows ? ';' : ':') 140 | setPathEnv(env, envPath) 141 | 142 | var packageLifecycle = pkg.scripts && pkg.scripts.hasOwnProperty(stage) 143 | 144 | if (opts.ignoreScripts) { 145 | opts.log.info('lifecycle', logid(pkg, stage), 'ignored because ignore-scripts is set to true', pkg._id) 146 | packageLifecycle = false 147 | } else if (packageLifecycle) { 148 | // define this here so it's available to all scripts. 149 | env.npm_lifecycle_script = pkg.scripts[stage] 150 | } else { 151 | opts.log.silly('lifecycle', logid(pkg, stage), 'no script for ' + stage + ', continuing') 152 | } 153 | 154 | function done (er) { 155 | if (er) { 156 | if (opts.force) { 157 | opts.log.info('lifecycle', logid(pkg, stage), 'forced, continuing', er) 158 | er = null 159 | } else if (opts.failOk) { 160 | opts.log.warn('lifecycle', logid(pkg, stage), 'continuing anyway', er.message) 161 | er = null 162 | } 163 | } 164 | cb(er) 165 | } 166 | 167 | chain( 168 | [ 169 | packageLifecycle && [runPackageLifecycle, pkg, stage, env, wd, opts], 170 | [runHookLifecycle, pkg, stage, env, wd, opts] 171 | ], 172 | done 173 | ) 174 | } 175 | 176 | function shouldPrependCurrentNodeDirToPATH (opts) { 177 | const cfgsetting = opts.scriptsPrependNodePath 178 | if (cfgsetting === false) return false 179 | if (cfgsetting === true) return true 180 | 181 | var isDifferentNodeInPath 182 | 183 | var foundExecPath 184 | try { 185 | foundExecPath = which.sync(path.basename(process.execPath), { pathExt: isWindows ? ';' : ':' }) 186 | // Apply `fs.realpath()` here to avoid false positives when `node` is a symlinked executable. 187 | isDifferentNodeInPath = fs.realpathSync(process.execPath).toUpperCase() !== 188 | fs.realpathSync(foundExecPath).toUpperCase() 189 | } catch (e) { 190 | isDifferentNodeInPath = true 191 | } 192 | 193 | if (cfgsetting === 'warn-only') { 194 | if (isDifferentNodeInPath && !shouldPrependCurrentNodeDirToPATH.hasWarned) { 195 | if (foundExecPath) { 196 | opts.log.warn('lifecycle', 'The node binary used for scripts is', foundExecPath, 'but npm is using', process.execPath, 'itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.') 197 | } else { 198 | opts.log.warn('lifecycle', 'npm is using', process.execPath, 'but there is no node binary in the current PATH. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.') 199 | } 200 | shouldPrependCurrentNodeDirToPATH.hasWarned = true 201 | } 202 | 203 | return false 204 | } 205 | 206 | return isDifferentNodeInPath 207 | } 208 | 209 | function validWd (d, cb) { 210 | fs.stat(d, function (er, st) { 211 | if (er || !st.isDirectory()) { 212 | var p = path.dirname(d) 213 | if (p === d) { 214 | return cb(new Error('Could not find suitable wd')) 215 | } 216 | return validWd(p, cb) 217 | } 218 | return cb(null, d) 219 | }) 220 | } 221 | 222 | function runPackageLifecycle (pkg, stage, env, wd, opts, cb) { 223 | // run package lifecycle scripts in the package root, or the nearest parent. 224 | var cmd = env.npm_lifecycle_script 225 | 226 | var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd + 227 | '\n> ' + cmd + '\n' 228 | runCmd(note, cmd, pkg, env, stage, wd, opts, cb) 229 | } 230 | 231 | var running = false 232 | var queue = [] 233 | function dequeue () { 234 | running = false 235 | if (queue.length) { 236 | var r = queue.shift() 237 | runCmd.apply(null, r) 238 | } 239 | } 240 | 241 | function runCmd (note, cmd, pkg, env, stage, wd, opts, cb) { 242 | if (running) { 243 | queue.push([note, cmd, pkg, env, stage, wd, opts, cb]) 244 | return 245 | } 246 | 247 | running = true 248 | opts.log.pause() 249 | var unsafe = opts.unsafePerm 250 | var user = unsafe ? null : opts.user 251 | var group = unsafe ? null : opts.group 252 | 253 | if (opts.log.level !== 'silent') { 254 | opts.log.clearProgress() 255 | console.log(note) 256 | opts.log.showProgress() 257 | } 258 | opts.log.verbose('lifecycle', logid(pkg, stage), 'unsafe-perm in lifecycle', unsafe) 259 | 260 | if (isWindows) { 261 | unsafe = true 262 | } 263 | 264 | if (unsafe) { 265 | runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, 0, 0, cb) 266 | } else { 267 | uidNumber(user, group, function (er, uid, gid) { 268 | if (er) { 269 | er.code = 'EUIDLOOKUP' 270 | opts.log.resume() 271 | process.nextTick(dequeue) 272 | return cb(er) 273 | } 274 | runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb) 275 | }) 276 | } 277 | } 278 | 279 | const getSpawnArgs = ({ cmd, wd, opts, uid, gid, unsafe, env }) => { 280 | const conf = { 281 | cwd: wd, 282 | env: env, 283 | stdio: opts.stdio || [ 0, 1, 2 ] 284 | } 285 | 286 | if (!unsafe) { 287 | conf.uid = uid ^ 0 288 | conf.gid = gid ^ 0 289 | } 290 | 291 | const customShell = opts.scriptShell 292 | 293 | let sh = 'sh' 294 | let shFlag = '-c' 295 | if (customShell) { 296 | sh = customShell 297 | } else if (isWindows || opts._TESTING_FAKE_WINDOWS_) { 298 | sh = process.env.comspec || 'cmd' 299 | // '/d /s /c' is used only for cmd.exe. 300 | if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(sh)) { 301 | shFlag = '/d /s /c' 302 | conf.windowsVerbatimArguments = true 303 | } 304 | } 305 | 306 | return [sh, [shFlag, cmd], conf] 307 | } 308 | 309 | exports._getSpawnArgs = getSpawnArgs 310 | 311 | function runCmd_ (cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb_) { 312 | function cb (er) { 313 | cb_.apply(null, arguments) 314 | opts.log.resume() 315 | process.nextTick(dequeue) 316 | } 317 | 318 | const [sh, args, conf] = getSpawnArgs({ cmd, wd, opts, uid, gid, unsafe, env }) 319 | 320 | opts.log.verbose('lifecycle', logid(pkg, stage), 'PATH:', env[PATH]) 321 | opts.log.verbose('lifecycle', logid(pkg, stage), 'CWD:', wd) 322 | opts.log.silly('lifecycle', logid(pkg, stage), 'Args:', args) 323 | 324 | var proc = spawn(sh, args, conf, opts.log) 325 | 326 | proc.on('error', procError) 327 | proc.on('close', function (code, signal) { 328 | opts.log.silly('lifecycle', logid(pkg, stage), 'Returned: code:', code, ' signal:', signal) 329 | if (signal) { 330 | process.kill(process.pid, signal) 331 | } else if (code) { 332 | var er = new Error('Exit status ' + code) 333 | er.errno = code 334 | } 335 | procError(er) 336 | }) 337 | byline(proc.stdout).on('data', function (data) { 338 | opts.log.verbose('lifecycle', logid(pkg, stage), 'stdout', data.toString()) 339 | }) 340 | byline(proc.stderr).on('data', function (data) { 341 | opts.log.verbose('lifecycle', logid(pkg, stage), 'stderr', data.toString()) 342 | }) 343 | process.once('SIGTERM', procKill) 344 | process.once('SIGINT', procInterupt) 345 | 346 | function procError (er) { 347 | if (er) { 348 | opts.log.info('lifecycle', logid(pkg, stage), 'Failed to exec ' + stage + ' script') 349 | er.message = pkg._id + ' ' + stage + ': `' + cmd + '`\n' + 350 | er.message 351 | if (er.code !== 'EPERM') { 352 | er.code = 'ELIFECYCLE' 353 | } 354 | fs.stat(opts.dir, function (statError, d) { 355 | if (statError && statError.code === 'ENOENT' && opts.dir.split(path.sep).slice(-1)[0] === 'node_modules') { 356 | opts.log.warn('', 'Local package.json exists, but node_modules missing, did you mean to install?') 357 | } 358 | }) 359 | er.pkgid = pkg._id 360 | er.stage = stage 361 | er.script = cmd 362 | er.pkgname = pkg.name 363 | } 364 | process.removeListener('SIGTERM', procKill) 365 | process.removeListener('SIGTERM', procInterupt) 366 | process.removeListener('SIGINT', procKill) 367 | process.removeListener('SIGINT', procInterupt) 368 | return cb(er) 369 | } 370 | function procKill () { 371 | proc.kill() 372 | } 373 | function procInterupt () { 374 | proc.kill('SIGINT') 375 | proc.on('exit', function () { 376 | process.exit() 377 | }) 378 | process.once('SIGINT', procKill) 379 | } 380 | } 381 | 382 | function runHookLifecycle (pkg, stage, env, wd, opts, cb) { 383 | hookStat(opts.dir, stage, function (er) { 384 | if (er) return cb() 385 | var cmd = path.join(opts.dir, '.hooks', stage) 386 | var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd + 387 | '\n> ' + cmd 388 | runCmd(note, cmd, pkg, env, stage, wd, opts, cb) 389 | }) 390 | } 391 | 392 | function makeEnv (data, opts, prefix, env) { 393 | prefix = prefix || 'npm_package_' 394 | if (!env) { 395 | env = {} 396 | for (var i in process.env) { 397 | if (!i.match(/^npm_/)) { 398 | env[i] = process.env[i] 399 | } 400 | } 401 | 402 | // express and others respect the NODE_ENV value. 403 | if (opts.production) env.NODE_ENV = 'production' 404 | } else if (!data.hasOwnProperty('_lifecycleEnv')) { 405 | Object.defineProperty(data, '_lifecycleEnv', 406 | { 407 | value: env, 408 | enumerable: false 409 | } 410 | ) 411 | } 412 | 413 | if (opts.nodeOptions) env.NODE_OPTIONS = opts.nodeOptions 414 | 415 | for (i in data) { 416 | if (i.charAt(0) !== '_') { 417 | var envKey = (prefix + i).replace(/[^a-zA-Z0-9_]/g, '_') 418 | if (i === 'readme') { 419 | continue 420 | } 421 | if (data[i] && typeof data[i] === 'object') { 422 | try { 423 | // quick and dirty detection for cyclical structures 424 | JSON.stringify(data[i]) 425 | makeEnv(data[i], opts, envKey + '_', env) 426 | } catch (ex) { 427 | // usually these are package objects. 428 | // just get the path and basic details. 429 | var d = data[i] 430 | makeEnv( 431 | { name: d.name, version: d.version, path: d.path }, 432 | opts, 433 | envKey + '_', 434 | env 435 | ) 436 | } 437 | } else { 438 | env[envKey] = String(data[i]) 439 | env[envKey] = env[envKey].indexOf('\n') !== -1 440 | ? JSON.stringify(env[envKey]) 441 | : env[envKey] 442 | } 443 | } 444 | } 445 | 446 | if (prefix !== 'npm_package_') return env 447 | 448 | prefix = 'npm_config_' 449 | var pkgConfig = {} 450 | var pkgVerConfig = {} 451 | var namePref = data.name + ':' 452 | var verPref = data.name + '@' + data.version + ':' 453 | 454 | Object.keys(opts.config).forEach(function (i) { 455 | // in some rare cases (e.g. working with nerf darts), there are segmented 456 | // "private" (underscore-prefixed) config names -- don't export 457 | if ((i.charAt(0) === '_' && i.indexOf('_' + namePref) !== 0) || i.match(/:_/)) { 458 | return 459 | } 460 | var value = opts.config[i] 461 | if (value instanceof Stream || Array.isArray(value) || typeof value === 'function') return 462 | if (i.match(/umask/)) value = umask.toString(value) 463 | 464 | if (!value) value = '' 465 | else if (typeof value === 'number') value = '' + value 466 | else if (typeof value !== 'string') value = JSON.stringify(value) 467 | 468 | if (typeof value !== 'string') { 469 | return 470 | } 471 | 472 | value = value.indexOf('\n') !== -1 473 | ? JSON.stringify(value) 474 | : value 475 | i = i.replace(/^_+/, '') 476 | var k 477 | if (i.indexOf(namePref) === 0) { 478 | k = i.substr(namePref.length).replace(/[^a-zA-Z0-9_]/g, '_') 479 | pkgConfig[k] = value 480 | } else if (i.indexOf(verPref) === 0) { 481 | k = i.substr(verPref.length).replace(/[^a-zA-Z0-9_]/g, '_') 482 | pkgVerConfig[k] = value 483 | } 484 | var envKey = (prefix + i).replace(/[^a-zA-Z0-9_]/g, '_') 485 | env[envKey] = value 486 | }) 487 | 488 | prefix = 'npm_package_config_' 489 | ;[pkgConfig, pkgVerConfig].forEach(function (conf) { 490 | for (var i in conf) { 491 | var envKey = (prefix + i) 492 | env[envKey] = conf[i] 493 | } 494 | }) 495 | 496 | return env 497 | } 498 | -------------------------------------------------------------------------------- /lib/spawn.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = spawn 4 | 5 | const _spawn = require('child_process').spawn 6 | const EventEmitter = require('events').EventEmitter 7 | 8 | let progressEnabled 9 | let running = 0 10 | 11 | function startRunning (log) { 12 | if (progressEnabled == null) progressEnabled = log.progressEnabled 13 | if (progressEnabled) log.disableProgress() 14 | ++running 15 | } 16 | 17 | function stopRunning (log) { 18 | --running 19 | if (progressEnabled && running === 0) log.enableProgress() 20 | } 21 | 22 | function willCmdOutput (stdio) { 23 | if (stdio === 'inherit') return true 24 | if (!Array.isArray(stdio)) return false 25 | for (let fh = 1; fh <= 2; ++fh) { 26 | if (stdio[fh] === 'inherit') return true 27 | if (stdio[fh] === 1 || stdio[fh] === 2) return true 28 | } 29 | return false 30 | } 31 | 32 | function spawn (cmd, args, options, log) { 33 | const cmdWillOutput = willCmdOutput(options && options.stdio) 34 | 35 | if (cmdWillOutput) startRunning(log) 36 | const raw = _spawn(cmd, args, options) 37 | const cooked = new EventEmitter() 38 | 39 | raw.on('error', function (er) { 40 | if (cmdWillOutput) stopRunning(log) 41 | er.file = cmd 42 | cooked.emit('error', er) 43 | }).on('close', function (code, signal) { 44 | if (cmdWillOutput) stopRunning(log) 45 | // Create ENOENT error because Node.js v8.0 will not emit 46 | // an `error` event if the command could not be found. 47 | if (code === 127) { 48 | const er = new Error('spawn ENOENT') 49 | er.code = 'ENOENT' 50 | er.errno = 'ENOENT' 51 | er.syscall = 'spawn' 52 | er.file = cmd 53 | cooked.emit('error', er) 54 | } else { 55 | cooked.emit('close', code, signal) 56 | } 57 | }) 58 | 59 | cooked.stdin = raw.stdin 60 | cooked.stdout = raw.stdout 61 | cooked.stderr = raw.stderr 62 | cooked.kill = function (sig) { return raw.kill(sig) } 63 | 64 | return cooked 65 | } 66 | -------------------------------------------------------------------------------- /node-gyp-bin/node-gyp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | if [ "x$npm_config_node_gyp" = "x" ]; then 3 | node "`dirname "$0"`/../../node_modules/node-gyp/bin/node-gyp.js" "$@" 4 | else 5 | "$npm_config_node_gyp" "$@" 6 | fi 7 | -------------------------------------------------------------------------------- /node-gyp-bin/node-gyp.cmd: -------------------------------------------------------------------------------- 1 | if not defined npm_config_node_gyp ( 2 | node "%~dp0\..\..\node_modules\node-gyp\bin\node-gyp.js" %* 3 | ) else ( 4 | node "%npm_config_node_gyp%" %* 5 | ) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-lifecycle", 3 | "version": "3.1.5", 4 | "description": "JavaScript package lifecycle hook runner", 5 | "main": "index.js", 6 | "scripts": { 7 | "prerelease": "npm t", 8 | "postrelease": "npm publish && git push --follow-tags", 9 | "pretest": "standard", 10 | "release": "standard-version -s", 11 | "test": "tap -J --cov test/*.js", 12 | "snap": "TAP_SNAPSHOT=1 npm test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/npm/lifecycle.git" 17 | }, 18 | "keywords": [ 19 | "npm", 20 | "lifecycle", 21 | "hook", 22 | "runner" 23 | ], 24 | "author": "Mike Sherov", 25 | "license": "Artistic-2.0", 26 | "bugs": { 27 | "url": "https://github.com/npm/lifecycle/issues" 28 | }, 29 | "homepage": "https://github.com/npm/lifecycle#readme", 30 | "dependencies": { 31 | "byline": "^5.0.0", 32 | "graceful-fs": "^4.1.15", 33 | "node-gyp": "^5.0.2", 34 | "resolve-from": "^4.0.0", 35 | "slide": "^1.1.6", 36 | "uid-number": "0.0.6", 37 | "umask": "^1.1.0", 38 | "which": "^1.3.1" 39 | }, 40 | "devDependencies": { 41 | "nyc": "^14.1.0", 42 | "sinon": "^7.2.3", 43 | "standard": "^12.0.1", 44 | "standard-version": "^4.4.0", 45 | "tap": "^12.7.0" 46 | }, 47 | "files": [ 48 | "index.js", 49 | "lib/spawn.js", 50 | "node-gyp-bin" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /tap-snapshots/test-get-spawn-args.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/get-spawn-args.js TAP > custom windows script shell 1`] = ` 9 | [ 10 | "flerbbyderb", 11 | [ 12 | "-c", 13 | "cmd" 14 | ], 15 | { 16 | "cwd": "/working/dir", 17 | "env": { 18 | "env": "iron", 19 | "men": "tal" 20 | }, 21 | "stdio": [ 22 | 0, 23 | 1, 24 | 2 25 | ], 26 | "uid": 123, 27 | "gid": 432 28 | } 29 | ] 30 | ` 31 | 32 | exports[`test/get-spawn-args.js TAP > just basics 1`] = ` 33 | [ 34 | "sh", 35 | [ 36 | "-c", 37 | "cmd" 38 | ], 39 | { 40 | "cwd": "/working/dir", 41 | "env": { 42 | "env": "iron", 43 | "men": "tal" 44 | }, 45 | "stdio": [ 46 | 0, 47 | 1, 48 | 2 49 | ], 50 | "uid": 123, 51 | "gid": 432 52 | } 53 | ] 54 | ` 55 | 56 | exports[`test/get-spawn-args.js TAP > stdio and numeric string uid 1`] = ` 57 | [ 58 | "sh", 59 | [ 60 | "-c", 61 | "cmd" 62 | ], 63 | { 64 | "cwd": "/working/dir", 65 | "env": { 66 | "env": "iron", 67 | "men": "tal" 68 | }, 69 | "stdio": [ 70 | 3, 71 | 2, 72 | 1 73 | ], 74 | "uid": 123, 75 | "gid": 432 76 | } 77 | ] 78 | ` 79 | 80 | exports[`test/get-spawn-args.js TAP > unsafe numeric string uid 1`] = ` 81 | [ 82 | "sh", 83 | [ 84 | "-c", 85 | "cmd" 86 | ], 87 | { 88 | "cwd": "/working/dir", 89 | "env": { 90 | "env": "iron", 91 | "men": "tal" 92 | }, 93 | "stdio": [ 94 | 3, 95 | 2, 96 | 1 97 | ] 98 | } 99 | ] 100 | ` 101 | 102 | exports[`test/get-spawn-args.js TAP > weird comspec 1`] = ` 103 | [ 104 | "flerbbyderb", 105 | [ 106 | "-c", 107 | "cmd" 108 | ], 109 | { 110 | "cwd": "/working/dir", 111 | "env": { 112 | "env": "iron", 113 | "men": "tal" 114 | }, 115 | "stdio": [ 116 | 0, 117 | 1, 118 | 2 119 | ], 120 | "uid": 123, 121 | "gid": 432 122 | } 123 | ] 124 | ` 125 | 126 | exports[`test/get-spawn-args.js TAP > windows 1`] = ` 127 | [ 128 | "CMD.exe", 129 | [ 130 | "/d /s /c", 131 | "cmd" 132 | ], 133 | { 134 | "cwd": "/working/dir", 135 | "env": { 136 | "env": "iron", 137 | "men": "tal" 138 | }, 139 | "stdio": [ 140 | 0, 141 | 1, 142 | 2 143 | ], 144 | "uid": 123, 145 | "gid": 432, 146 | "windowsVerbatimArguments": true 147 | } 148 | ] 149 | ` 150 | -------------------------------------------------------------------------------- /tap-snapshots/test-path-munging.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/path-munging.js TAP mergePath > merge all paths together 1`] = ` 9 | "a:b:c:d:x:y:z" 10 | ` 11 | 12 | exports[`test/path-munging.js TAP setPathEnv > set all path envs 1`] = ` 13 | { 14 | "Path": "a:b:c", 15 | "PATH": "a:b:c", 16 | "pAtH": "a:b:c", 17 | "a": "b" 18 | } 19 | ` 20 | -------------------------------------------------------------------------------- /test/fixtures/count-to-10-working-postinstall/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "count-to-10-working-postinstall", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "postinstall": "node postinstall.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/count-to-10-working-postinstall/postinstall.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | console.log('line 1') 4 | console.log('line 2') 5 | console.error('some error') 6 | -------------------------------------------------------------------------------- /test/fixtures/count-to-10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "count-to-10", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "postinstall": "scripts/postinstall.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/count-to-10/postinstall.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | console.log('line 1') 4 | console.log('line 2') 5 | console.error('some error') 6 | -------------------------------------------------------------------------------- /test/fixtures/has-hooks/node_modules/.hooks/postinstall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | console.log('ran hook'); -------------------------------------------------------------------------------- /test/fixtures/has-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-hooks", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/get-spawn-args.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const getSpawnArgs = require('../')._getSpawnArgs 3 | 4 | // these show up in the result, but aren't forked on 5 | // just set the same for every case. 6 | const cmd = 'cmd' 7 | const wd = '/working/dir' 8 | const env = { env: 'iron', men: 'tal' } 9 | const b = { cmd, wd, env, uid: 123, gid: 432, opts: {} } 10 | 11 | const snap = (o, m) => t.matchSnapshot(JSON.stringify(o, null, 2), m) 12 | 13 | snap(getSpawnArgs(b), 'just basics') 14 | 15 | snap(getSpawnArgs(Object.assign({}, b, { 16 | opts: { stdio: [3, 2, 1] }, 17 | uid: '123' 18 | })), 'stdio and numeric string uid') 19 | 20 | snap(getSpawnArgs(Object.assign({}, b, { 21 | opts: { stdio: [3, 2, 1] }, 22 | uid: '123', 23 | unsafe: true 24 | })), 'unsafe numeric string uid') 25 | 26 | process.env.comspec = 'CMD.exe' 27 | snap(getSpawnArgs(Object.assign({}, b, { 28 | opts: { 29 | _TESTING_FAKE_WINDOWS_: true 30 | } 31 | })), 'windows') 32 | 33 | snap(getSpawnArgs(Object.assign({}, b, { 34 | opts: { 35 | _TESTING_FAKE_WINDOWS_: true, 36 | scriptShell: 'flerbbyderb' 37 | } 38 | })), 'custom windows script shell') 39 | 40 | process.env.comspec = 'flerbbyderb' 41 | snap(getSpawnArgs(Object.assign({}, b, { 42 | opts: { 43 | _TESTING_FAKE_WINDOWS_: true 44 | } 45 | })), 'weird comspec') 46 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const sinon = require('sinon') 5 | const lifecycle = require('../index.js') 6 | const path = require('path') 7 | 8 | function noop () {} 9 | 10 | test('makeEnv', function (t) { 11 | const pkg = { 12 | name: 'myPackage', 13 | version: '1.0.0', 14 | contributors: [{ name: 'Mike Sherov', email: 'beep@boop.com' }] 15 | } 16 | const config = { 17 | enteente: Infinity, 18 | '_privateVar': 1, 19 | '_myPackage:myPrivateVar': 1, 20 | 'myPackage:bar': 2, 21 | 'myPackage:foo': 3, 22 | 'myPackage@1.0.0:baz': 4, 23 | 'myPackage@1.0.0:foo': 5, 24 | ignoreThisFunction: () => {}, 25 | jsonsAsUndefined: { toJSON: () => undefined }, 26 | jsonsAsFunction: { toJSON: () => () => {} } 27 | } 28 | 29 | const env = lifecycle.makeEnv(pkg, { 30 | config, 31 | nodeOptions: '--inspect-brk --abort-on-uncaught-exception' 32 | }, null, Object.assign({}, process.env)) 33 | 34 | t.equal('myPackage', env.npm_package_name, 'package data is included') 35 | t.equal('Mike Sherov', env.npm_package_contributors_0_name, 'nested package data is included') 36 | 37 | t.equal('Infinity', env.npm_config_enteente, 'public config is included') 38 | t.equal(undefined, env.npm_config_privateVar, 'private config is excluded') 39 | 40 | t.equal('1', env.npm_config_myPackage_myPrivateVar, 'private package config is included by name') 41 | t.equal('2', env.npm_config_myPackage_bar, 'public package config is included by name') 42 | 43 | t.equal('5', env.npm_config_myPackage_1_0_0_foo, 'public package@version config is included by name') 44 | 45 | t.equal('1', env.npm_package_config_myPrivateVar, 'package private config is included') 46 | t.equal('2', env.npm_package_config_bar, 'package config is included') 47 | t.equal('4', env.npm_package_config_baz, 'package@version config is included') 48 | t.equal('5', env.npm_package_config_foo, 'package@version config overrides package config') 49 | 50 | t.equal(env.npm_package_config_ignoreThisFunction, undefined) 51 | t.equal(env.npm_package_config_jsonsAsUndefined, undefined) 52 | t.equal(env.npm_package_config_jsonsAsFunction, undefined) 53 | 54 | t.equal('--inspect-brk --abort-on-uncaught-exception', env.NODE_OPTIONS, 'nodeOptions sets NODE_OPTIONS') 55 | t.end() 56 | }) 57 | 58 | test('_incorrectWorkingDirectory: accepts wd for package that matches project\'s name', function (t) { 59 | const wd = '/opt/my-time/node_modules/time' 60 | const pkg = { name: 'time' } 61 | 62 | t.equal(lifecycle._incorrectWorkingDirectory(wd, pkg), false) 63 | t.end() 64 | }) 65 | 66 | test('_incorrectWorkingDirectory: accepts wd for package that doesn\'t match project\'s name', function (t) { 67 | const wd = '/opt/my-project/node_modules/time' 68 | const pkg = { name: 'time' } 69 | 70 | t.equal(lifecycle._incorrectWorkingDirectory(wd, pkg), false) 71 | t.end() 72 | }) 73 | 74 | test('_incorrectWorkingDirectory: rejects wd from other packages', function (t) { 75 | const wd = '/opt/my-time/node_modules/time/invalid' 76 | const pkg = { 77 | name: 'time' 78 | } 79 | 80 | t.equal(lifecycle._incorrectWorkingDirectory(wd, pkg), true) 81 | t.end() 82 | }) 83 | 84 | test('runs scripts from .hooks directory even if no script is present in package.json', function (t) { 85 | const fixture = path.join(__dirname, 'fixtures', 'has-hooks') 86 | 87 | const verbose = sinon.spy() 88 | const silly = sinon.spy() 89 | const log = { 90 | level: 'silent', 91 | info: noop, 92 | warn: noop, 93 | silly, 94 | verbose, 95 | pause: noop, 96 | resume: noop, 97 | clearProgress: noop, 98 | showProgress: noop 99 | } 100 | const dir = path.join(fixture, 'node_modules') 101 | 102 | const pkg = require(path.join(fixture, 'package.json')) 103 | 104 | lifecycle(pkg, 'postinstall', fixture, { 105 | stdio: 'pipe', 106 | log, 107 | dir, 108 | config: {} 109 | }) 110 | .then(() => { 111 | t.ok( 112 | verbose.calledWithMatch( 113 | 'lifecycle', 114 | 'undefined~postinstall:', 115 | 'stdout', 116 | 'ran hook' 117 | ), 118 | 'ran postinstall hook' 119 | ) 120 | 121 | t.end() 122 | }) 123 | .catch(t.end) 124 | }) 125 | 126 | test("reports child's output", function (t) { 127 | const fixture = path.join(__dirname, 'fixtures', 'count-to-10') 128 | 129 | const verbose = sinon.spy() 130 | const silly = sinon.spy() 131 | const log = { 132 | level: 'silent', 133 | info: noop, 134 | warn: noop, 135 | silly, 136 | verbose, 137 | pause: noop, 138 | resume: noop, 139 | clearProgress: noop, 140 | showProgress: noop 141 | } 142 | const dir = path.join(__dirname, '..') 143 | 144 | const pkg = require(path.join(fixture, 'package.json')) 145 | 146 | lifecycle(pkg, 'postinstall', fixture, { 147 | stdio: 'pipe', 148 | log, 149 | dir, 150 | config: {} 151 | }) 152 | .then(() => { 153 | t.ok( 154 | verbose.calledWithMatch( 155 | 'lifecycle', 156 | 'undefined~postinstall:', 157 | 'stdout', 158 | 'line 1' 159 | ), 160 | 'stdout reported' 161 | ) 162 | t.ok( 163 | verbose.calledWithMatch( 164 | 'lifecycle', 165 | 'undefined~postinstall:', 166 | 'stdout', 167 | 'line 2' 168 | ), 169 | 'stdout reported' 170 | ) 171 | t.ok( 172 | verbose.calledWithMatch( 173 | 'lifecycle', 174 | 'undefined~postinstall:', 175 | 'stderr', 176 | 'some error' 177 | ), 178 | 'stderr reported' 179 | ) 180 | t.ok( 181 | silly.calledWithMatch( 182 | 'lifecycle', 183 | 'undefined~postinstall:', 184 | 'Returned: code:', 185 | 0, 186 | ' signal:', 187 | null 188 | ), 189 | 'exit code reported' 190 | ) 191 | 192 | t.end() 193 | }) 194 | .catch(t.end) 195 | }) 196 | 197 | test('fails when given an invalid user id', { 198 | skip: process.platform === 'win32' ? 'windows always in unsafe mode' : false 199 | }, function (t) { 200 | const fixture = path.join(__dirname, 'fixtures', 'count-to-10-working-postinstall') 201 | const dir = fixture 202 | 203 | const pkg = require(path.join(fixture, 'package.json')) 204 | t.rejects(lifecycle(pkg, 'postinstall', fixture, { 205 | stdio: 'pipe', 206 | log: { 207 | level: 'silent', 208 | info: noop, 209 | warn: noop, 210 | silly: noop, 211 | verbose: noop, 212 | pause: noop, 213 | resume: noop, 214 | clearProgress: noop, 215 | showProgress: noop 216 | }, 217 | dir, 218 | user: 'this-user-name-does-not-exist-at-least-i-sure-hope-not', 219 | group: 'maybe-there-are-a-bunch-of-us-who-do-not-exist', 220 | unsafePerm: false, 221 | config: {} 222 | }), { code: 'EUIDLOOKUP' }) 223 | t.end() 224 | }) 225 | -------------------------------------------------------------------------------- /test/path-munging.js: -------------------------------------------------------------------------------- 1 | require('path').delimiter = ':' 2 | const { _setPathEnv, _mergePath } = require('../') 3 | const t = require('tap') 4 | 5 | const snap = (t, o, m) => t.matchSnapshot(JSON.stringify(o, 0, 2), m) 6 | 7 | t.test('setPathEnv', t => { 8 | const e = { 9 | Path: 1, 10 | PATH: 2, 11 | pAtH: 3, 12 | a: 'b' 13 | } 14 | _setPathEnv(e, 'a:b:c') 15 | snap(t, e, 'set all path envs') 16 | t.end() 17 | }) 18 | 19 | t.test('mergePath', t => { 20 | const e = { 21 | Path: 'a:b:c', 22 | PATH: 'a:b:c:d', 23 | PaTh: 'd:c:x:y:z', 24 | pAtH: '' 25 | } 26 | snap(t, _mergePath(e), 'merge all paths together') 27 | t.end() 28 | }) 29 | --------------------------------------------------------------------------------