├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── circle.yml ├── cli.js ├── npm-git-lock-latest.tgz ├── package.json ├── src └── checkout-node-modules.es6 └── test ├── checkout-node-modules.spec.es6 └── fixtures ├── fake-module └── package.json └── fake-platform-specific-module ├── install.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | .idea 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | npm-git-lock-*.tgz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Konstantin Raev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-git-lock 2 | 3 | [![Circle CI](https://circleci.com/gh/bestander/npm-git-lock.svg?style=svg)](https://circleci.com/gh/bestander/npm-git-lock) 4 | 5 | A CLI tool to lock all node_modules dependencies to a separate git repository. 6 | 7 | Read a [post](https://medium.com/@bestander_nz/my-node-modules-are-in-git-again-4fb18f5671a) why you may need it. 8 | 9 | # Update 10 | 11 | I npm-git-lock was created a few years ago before Yarn and offline mirror feature: https://yarnpkg.com/blog/2016/11/24/offline-mirror/. 12 | 13 | There is even a feature to [store built artifacts](https://github.com/yarnpkg/yarn/pull/5314), so I would suggest switching to Yarn as a more scalable solution. 14 | 15 | ## Features 16 | 17 | - Tracks changes in package.json file 18 | - When a change is found makes a clean install of all dependencies and commits and pushes node_modules to a remote repository 19 | - Works independently from your npm workflow and may be used on a CI server only keeping your dev environment simpler 20 | 21 | ## How to use 22 | 23 | ``` 24 | sudo npm install -g npm-git-lock 25 | cd [your work directory] 26 | npm-git-lock --repo [git@bitbucket.org:your/dedicated/node_modules/git/repository.git] -v 27 | 28 | ``` 29 | 30 | If you don't want to depend on NPM connectivity when installing this module, you can install directly from github: 31 | 32 | ``` 33 | sudo npm install -g https://raw.githubusercontent.com/bestander/npm-git-lock/master/npm-git-lock-latest.tgz 34 | ``` 35 | - Beware of possible breaking changes in the future, if you seek stability, obtain a link to a particular commit with the 36 | .tgz file on GitHub. 37 | 38 | ### Options: 39 | 40 | --verbose [-v] Print progress log messages 41 | --repo Git URL to repository with node_modules content [required] 42 | --cross-platform Run in cross-platform mode (npm 3 only) 43 | --incremental-install Keep previous modules instead of always performing a fresh npm install (npm 3 only) 44 | --production Runs npm install with production flag 45 | --check-all-json-elements Sha-1 calculated from all elements in package.json instead of only dependencies and devDependencies 46 | 47 | `npm-git-lock` works with both npm 2 and 3, although the options `--cross-platform` and `--incremental-install` are only supported on npm 3. 48 | 49 | 50 | ## Why you need it 51 | 52 | You need it to get reliable and reproducible builds of your Node.js/io.js projects. 53 | 54 | ### [Shrinkwrapping](https://docs.npmjs.com/cli/shrinkwrap) 55 | is the recommended option to "lock down" dependency tree of your application. 56 | I have been using it throughout 2014 and there are too many inconveniences that accompany this technique: 57 | 1. Dependency on npm servers availability at every CI build. NPM [availability](http://status.npmjs.org/) is quite good in 2015 [watch a good talk](http://nodesummit.com/media/node-js-at-scale/) but you don't want to have another moving part when doing an urgent production build. 58 | 2. Managing of npm-shrinkwrap.json is not straightforward as of npm@2.4. It is promising to improve though. 59 | 3. Even though npm does not allow force updating packages without changing the version, packages can still be removed from the repository and you don't want to find that out when doing a production deployment. 60 | 4. There are lots of other complex variable things about shrinkwrapping like optional dependencies and the coming changes in npm@3.0 like flat node_modules folder structure. 61 | 62 | 63 | ### Committing packages 64 | to your source version control system was recommended before shrinkwrapping but it is not anymore. 65 | Nonetheless I think it is a more reliable option though with a few annoying details: 66 | 1. A change in any dependency can generate a humongous commit diff which may get your Pull Requests unreadable 67 | 2. node_modules often contains binary dependencies which are platform specific and large and don't play well across dev and CI environments 68 | 69 | `npm-git-lock` is like committing your dependencies to git but without the above disadvantages. 70 | 71 | ## How it works 72 | 73 | The algorithm is simple: 74 | 1. Check if node_modules folder is present in the working directory 75 | 2. If node_modules exists check if there is a separate git repository in it 76 | 3. Calculate sha1 hash from package.json in base64 format 77 | 4. If remote repo from [2] has a commit tagged with sha1 from [3] then check it out clean, no `npm install` is required 78 | 5. Otherwise remove everything from node_modules (unless `--incremental-install` is set, in which case only uncommitted changes will be stashed away), do a clean `npm install`, commit, tag with sha1 from [3] and push to remote repo 79 | 6. Next time you build with the same package.json, it is guaranteed that you get node_modules from the first run 80 | 81 | After this you end up with a reliable and reproducible source controlled node_modules folder. 82 | If there is any change in package.json, a fresh `npm install` will be done once. 83 | If there is no change, npm command is not touched and your CI build is fast. 84 | 85 | ### Cross-platform mode 86 | 87 | When `npm-git-lock` is run with the `--cross-platform` option, it does not commit "platform-specific" build artifacts into the remote repository. Instead, it builds them using `npm rebuild` when checking out the repository (step 4) or when doing a clean `npm install` (step 5). Platform-specific files are taken to be those files that are generated by build scripts. 88 | 89 | Inspired by [this post](https://medium.com/@g_syner/for-the-most-part-i-really-like-your-solution-664c8248ec30#.4ekcegbww), this is how step 5 is modified in cross-platform mode: 90 | 91 | 1. Run `npm install --ignore-scripts` to prevent platform-specific compilation of any files. 92 | 2. Run `git add .` to capture the current "clean" cross-platform state. 93 | 3. Run `npm rebuild` to create any platform-specific files. 94 | 4. Run `git status --untracked-files=all` to list all files that have been generated in the previous step. Add these files to `.gitignore`. 95 | 96 | `--cross-platform` is only supported on `npm` version >= 3, since npm 2 doesn't run custom install scripts as it should during `npm rebuild` (cf. [this CI failure](https://circleci.com/gh/bestander/npm-git-lock/11)). 97 | 98 | ### Incremental installs 99 | 100 | By default, `npm-git-lock` will perform a completely fresh `npm install` whenever there is any change to package.json (i.e., there is no commit in the node_modules repository tagged with the sha1 of package.json). However, that might not always be desired, since all dependencies might change (as long as their version is still within the range specified in package.json). 101 | 102 | To get a behavior more similar to `npm shrinkwrap`, you can use the option `--incremental-install`. When installing modules, it will reuse modules that have already been committed to the node_modules repository and only run `npm install` "on top of them". 103 | 104 | A potential caveat is that modules are always fetched from the latest state (the master branch) of the node_modules repository. If there are dependencies introduced in a previous commit but not on the latest master HEAD, they will be freshly installed. 105 | 106 | *Example*: In your project, you work on a branch *A* that declares a new dependency *depA* in package.json. In parallel, you have a branch *B* declaring a new dependency *depB*. Then you merge them both back into master. Assuming you run `npm-git-lock` in each step, the history of your central node_modules repository will look like this (from recent to older): 107 | 108 | * [commit3, master HEAD] *depA* + *depB* 109 | * [commit2] *depB* 110 | * [commit1] *depA* 111 | 112 | Note that when running `npm-git-lock` after the merge (to produce commit3), only *depB* was fetched from the previous state. For *depA*, a fresh install was performed. 113 | 114 | 115 | ## Amazing features 116 | 117 | With this package you get: 118 | 119 | 1. Minimum dependency on npm servers availability for repeated builds which is very common for CI systems. 120 | 2. No noise in your main project Pull Requests, all packages are committed to a separate git repository that does not need to be reviewed or maintained. 121 | 3. If the separate git repository for packages gets too large and slows down your builds after a few years, you can just create a new one, saving the old one for patches if you need. 122 | 4. Using it does not interfere with the recommended npm workflow, you can use it only on your CI system with no side effects for your dev environment or mix it with shrinkwrapping. 123 | 5. You can have different node_modules repositories for different OS. Your CI is likely to be linux while your dev machines may be mac or windows. You can set up 3 repositories for them and use them independently. 124 | 6. And it is blazing fast. 125 | 126 | ## Troubleshoot 127 | 128 | If you see this kind of error in your CI: 129 | 130 | ``` 131 | Cloning into 'node_modules'... 132 | done. 133 | *** Please tell me who you are. 134 | Run 135 | git config --global user.email "you@example.com" 136 | git config --global user.name "Your Name" 137 | to set your account's default identity. 138 | Omit --global to set the identity only in this repository. 139 | fatal: empty ident name (for ) not allowed 140 | ``` 141 | 142 | You need to configure `user.email` and `user.name` in the environment as shown in the error message. 143 | Just add those two commands before `npm-git-lock` call. 144 | 145 | If you see this kind of error: 146 | 147 | ``` 148 | fatal: bad revision 'HEAD' 149 | fatal: bad revision 'HEAD' 150 | fatal: Needed a single revision 151 | You do not have the initial commit yet 152 | ``` 153 | 154 | You need to commit and push to the repote repository at least once before using `npm-git-lock` 155 | 156 | If you see this kind of error: 157 | 158 | ``` 159 | Git command 'tag -l --points-at HEAD' failed: 160 | error: unknown option `points-at' 161 | ``` 162 | 163 | or 164 | 165 | ``` 166 | Git command 'stash save --include-untracked' failed: 167 | error: unknown option for 'stash save': --include-untracked 168 | ``` 169 | 170 | You need to upgrade `git` to version 1.7.10+. 171 | 172 | ## Contribution 173 | 174 | Please give me your feedback and send Pull Requests. 175 | Unit tests rely on ```require(`child_process`).execSync``` command that works in node 0.11+. 176 | 177 | ## Future plans (up for grabs) 178 | 179 | - Replace .es6 extension with .js 180 | - Switch to [shelljs](https://github.com/shelljs/shelljs) from promises API. Promises are still too heavy for such a file oriented CLI tool 181 | 182 | ## Change Log 183 | 184 | ### [3.6.0](https://github.com/bestander/npm-git-lock/releases/tag/3.6.0) - 2018-02-20 185 | - [Feature](https://github.com/bestander/npm-git-lock/pull/36) fixed scoped packages for --cross-platform flag 186 | 187 | ### [3.5.0](https://github.com/bestander/npm-git-lock/releases/tag/3.5.0) - 2016-06-30 188 | - [Feature](https://github.com/bestander/npm-git-lock/pull/32) support --check-all-json-elements 189 | 190 | ### [3.3.0](https://github.com/bestander/npm-git-lock/releases/tag/3.3.0) - 2016-04-26 191 | - [Feature](https://github.com/bestander/npm-git-lock/pull/25) support --production 192 | 193 | ### [3.2.1](https://github.com/bestander/npm-git-lock/releases/tag/3.2.1) - 2016-04-14 194 | - [Fixed](https://github.com/bestander/npm-git-lock/pull/24) support for Node 0.12 195 | 196 | ### [3.2.0](https://github.com/bestander/npm-git-lock/releases/tag/3.2.0) - 2016-03-24 197 | - [Feature](https://github.com/bestander/npm-git-lock/pull/21) run `preinstall` and `postinstall` scripts even in `--cross-platform` mode 198 | 199 | ### [3.1.1](https://github.com/bestander/npm-git-lock/releases/tag/3.1.1) - 2016-03-17 200 | - [Fixed](https://github.com/bestander/npm-git-lock/pull/19) `loglevel` argument for npm commands 201 | 202 | ### [3.0.0](https://github.com/bestander/npm-git-lock/releases/tag/3.0.0) - 2016-02-18 203 | - The hashing algorithm has [changed](https://github.com/sergiu-paraschiv/npm-git-lock/commit/abad012a6d1465ce79879e95a1af725134193ff5) due to a bug that caused different hashes to be generated on different platforms. This means hashes generated by 3.0.0+ are not compatible with older versions. Make sure you use the same version in all your environments! (`git install -g npm-git-lock@x.y.z` is your friend) 204 | 205 | 206 | ## License MIT 207 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4 4 | test: 5 | override: 6 | - npm install -g npm@2 7 | - git config --global user.email "<>" 8 | - git config --global user.name "Test User" 9 | - npm test 10 | - npm install -g npm@3 11 | - npm test -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var argv = require('optimist') 5 | .usage('Usage: $0 --repo [git@bitbucket.org:your/dedicated/node_modules/git/repository.git] --verbose --cross-platform') 6 | .describe('verbose', '[-v] Print progress log messages') 7 | .describe('repo', 'git url to repository with node_modules content') 8 | .describe('cross-platform', 'do not archive platform-specific files in node_modules') 9 | .describe('incremental-install', 'start npm install with last node_modules instead of clearing them') 10 | .describe('production', 'start npm install with production flag') 11 | .describe('check-all-json-elements', 'Sha-1 calculated from all elements in package.json instead of only dependencies and devDependencies') 12 | .alias('v', 'verbose') 13 | .demand(['repo']).argv; 14 | 15 | var checkoutNodeModules = require('./lib/checkout-node-modules'); 16 | 17 | checkoutNodeModules(process.cwd(), { 18 | verbose: argv.verbose, 19 | repo: argv.repo, 20 | crossPlatform: argv['cross-platform'], 21 | incrementalInstall: argv['incremental-install'], 22 | production: argv['production'], 23 | checkAllJsonElements: argv['check-all-json-elements'] 24 | }) 25 | .then(function () { 26 | process.exit(0); 27 | }) 28 | .catch(function (error) { 29 | process.exit(1); 30 | }); 31 | -------------------------------------------------------------------------------- /npm-git-lock-latest.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestander/npm-git-lock/3e7849cc9c1a960043b5ff0e0bd82a0fdc51a3e6/npm-git-lock-latest.tgz -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-git-lock", 3 | "version": "3.6.0", 4 | "description": "A CLI tool to lock all node_modules dependencies to a separate git repository.", 5 | "main": "cli.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/bestander/npm-git-lock.git" 9 | }, 10 | "dependencies": { 11 | "del": "^1.1.1", 12 | "es6-promise": "^3.0.2", 13 | "es6-promisify": "^1.1.1", 14 | "git-promise": "^0.2.0", 15 | "json-stable-stringify": "^1.0.0", 16 | "lodash": "^3.10.1", 17 | "loglevel": "^1.2.0", 18 | "optimist": "^0.6.1", 19 | "shelljs": "^0.5.3", 20 | "string.prototype.startswith": "^0.2.0" 21 | }, 22 | "devDependencies": { 23 | "babel": "^5.0.0", 24 | "chai": "^2.1.2", 25 | "mocha": "^2.2.1", 26 | "rewire": "^2.3.4", 27 | "semver": "^5.1.0" 28 | }, 29 | "bundledDependencies": [ 30 | "del", 31 | "es6-promise", 32 | "es6-promisify", 33 | "git-promise", 34 | "json-stable-stringify", 35 | "lodash", 36 | "loglevel", 37 | "optimist", 38 | "shelljs" 39 | ], 40 | "scripts": { 41 | "test": "mocha --compilers es6:babel/register", 42 | "compile": "babel -d lib/ src/", 43 | "prepublish": "npm run compile", 44 | "pack": "npm pack" 45 | }, 46 | "bin": { 47 | "npm-git-lock": "cli.js" 48 | }, 49 | "author": "Konstantin Raev", 50 | "license": "MIT" 51 | } 52 | -------------------------------------------------------------------------------- /src/checkout-node-modules.es6: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let gitPromise = require(`git-promise`); 4 | let gitUtil = require(`git-promise/util`); 5 | let del = require(`del`); 6 | let fs = require(`fs`); 7 | let os = require(`os`); 8 | let promisify = require(`es6-promisify`); 9 | let log = require(`loglevel`); 10 | let crypto = require(`crypto`); 11 | let shell = require(`shelljs`); 12 | let stringify = require(`json-stable-stringify`); 13 | let uniq = require(`lodash/array/uniq`); 14 | 15 | require('es6-promise').polyfill(); 16 | require('string.prototype.startswith'); 17 | 18 | let readFilePromise = promisify(fs.readFile); 19 | let delPromise = promisify(del); 20 | let statPromise = promisify(fs.stat); 21 | 22 | /** 23 | * List of platforms that we know only work on a certain platform. 24 | * Unfortunately, this is necessary since `npm rebuild` doesn't ignore build failures in optional dependencies 25 | * like it should (cf. https://github.com/npm/npm/issues/10335). 26 | * Obviously, this list is not complete, but contains just what was necessary to make this work for "us". 27 | */ 28 | const PLATFORM_SPECIFIC_MODULES = { 29 | 'fsevents': 'darwin' 30 | }; 31 | 32 | /** 33 | * Conservative estimate of the maximum number of characters for a single shell line. 34 | * According to https://support.microsoft.com/en-us/kb/830473 this is no less than 2047. 35 | * Platforms other than Windows shouldn't pose a problem. 36 | * @type {number} 37 | */ 38 | const MAX_SHELL_LENGTH = 2000; 39 | 40 | module.exports = (cwd, {repo, verbose, crossPlatform, incrementalInstall, production, checkAllJsonElements}) => { 41 | 42 | let packageJsonSha1; 43 | let packageJsonVersion; 44 | let leaveAsIs = false; 45 | log.setLevel(verbose ? `debug`: `info`); 46 | log.debug(`Updating ${cwd}/node_modules using repo ${repo}`); 47 | return readFilePromise(`${cwd}/package.json`, `utf-8`) 48 | .then((packageJsonContent) => { 49 | let packageJson = JSON.parse(packageJsonContent); 50 | let stableContent; 51 | // compute a hash based on the stable-stringified contents of package.json 52 | // (`packageJsonContent` might differ on different platforms, depending on line endings etc.) 53 | if (checkAllJsonElements) { 54 | log.debug(`Sha-1 calculated from all elements in package.json.`); 55 | stableContent = stringify(packageJson); 56 | } else { 57 | log.debug(`Sha-1 calculated from dependencies and devDependencies in package.json.`); 58 | stableContent = stringify([packageJson.dependencies, packageJson.devDependencies]); 59 | } 60 | 61 | // replace / in hash with _ because git does not allow leading / in tags 62 | packageJsonSha1 = crypto.createHash(`sha1`).update(stableContent).digest(`base64`).replace(/\//g, "_"); 63 | packageJsonVersion = packageJson.version; 64 | log.debug(`SHA-1 of package.json (version ${packageJsonVersion}) is ${packageJsonSha1}`); 65 | return packageJsonSha1; 66 | }) 67 | .then(() => { 68 | return statPromise(`${cwd}/node_modules`) 69 | .then(() => { 70 | log.debug(`Checking if remote ${repo} exists`); 71 | process.chdir(`${cwd}/node_modules`); 72 | return git(`git remote -v`) 73 | .then((remoteCommandOutput) => { 74 | if (remoteCommandOutput.indexOf(repo) !== -1) { 75 | // repo is in remotes 76 | return git(`tag -l --points-at HEAD`) 77 | .then((tags) => { 78 | if (tags.split('\n').indexOf(packageJsonSha1) >= 0) { 79 | // if the current HEAD is at the right commit, don't change anything 80 | log.debug(`${repo} is already at tag ${packageJsonSha1}, leaving as is`); 81 | leaveAsIs = true; 82 | } else { 83 | log.debug(`Remote exists, fetching from it`); 84 | return git(`git fetch -t ${repo}`); 85 | } 86 | }); 87 | } 88 | return cloneRepo(); 89 | }); 90 | }) 91 | .catch(cloneRepo) 92 | }) 93 | .then((tags) => { 94 | if (leaveAsIs) { 95 | return; 96 | } 97 | log.debug(`Remote ${repo} is in node_modules, checking out ${packageJsonSha1} tag`); 98 | process.chdir(`${cwd}/node_modules`); 99 | return git(`rev-list ${packageJsonSha1}`, { silent: true }) 100 | .then(() => runNpmScript('preinstall')) 101 | .then(() => git(`checkout tags/${packageJsonSha1}`, {silent: true})) 102 | .then(() => { 103 | log.debug(`Cleanup checked out commit`); 104 | return git(`clean -df`); 105 | }) 106 | .then(() => { 107 | if (crossPlatform) { 108 | return rebuildAndIgnorePlatformSpecific(); 109 | } 110 | }) 111 | .then(() => runNpmScript('postinstall')) 112 | .catch(installPackagesTagAndPushToRemote); 113 | }) 114 | .then(() => { 115 | process.chdir(`${cwd}`); 116 | log.info(`Node_modules are in sync with ${repo} ${packageJsonSha1}`); 117 | }) 118 | .catch((error) => { 119 | process.chdir(`${cwd}`); 120 | log.info(`Failed to synchronise node_modules with ${repo}: ${error}`); 121 | process.exit(1); 122 | }); 123 | 124 | function cloneRepo() { 125 | log.debug(`Remote ${repo} is not present in ${cwd}/node_modules/.git repo`); 126 | log.debug(`Removing ${cwd}/node_modules`); 127 | process.chdir(`${cwd}`); 128 | return delPromise([`node_modules/`]) 129 | .then(() => { 130 | log.debug(`Cloning ${repo}`); 131 | return git(`clone ${repo} node_modules`); 132 | }); 133 | } 134 | 135 | function git(cmd, {silent}={}) { 136 | return gitPromise(cmd).catch((error) => { 137 | if (!silent) { 138 | // report any Git errors immediately 139 | log.info(`Git command '${cmd}' failed:\n${error.stdout}`); 140 | } 141 | throw error; 142 | }); 143 | } 144 | 145 | function gitGetUntracked() { 146 | return git(`status --porcelain --untracked-files=all`) 147 | .then(result => { 148 | return result.split('\n').filter(line => line && line.startsWith('??')).map(line => line.substr(3)); 149 | }); 150 | } 151 | 152 | function isNonEmpty(value) { 153 | return value && value.length; 154 | } 155 | 156 | /** 157 | * Determines if the working tree of a Git repository in the current directory has any changes. 158 | */ 159 | function gitHasChanges() { 160 | return git(`status --porcelain --untracked-files=all`) 161 | .then(result => { 162 | let {index, workingTree} = gitUtil.extractStatus(result); 163 | if (index) { 164 | if (isNonEmpty(index.modified) || isNonEmpty(index.added) || isNonEmpty(index.deleted) || 165 | isNonEmpty(index.renamed) || isNonEmpty(index.copied)) { 166 | return true; 167 | } 168 | } 169 | if (workingTree) { 170 | if (isNonEmpty(workingTree.modified) || isNonEmpty(workingTree.added) || 171 | isNonEmpty(workingTree.deleted)) { 172 | return true; 173 | } 174 | } 175 | return false; 176 | }); 177 | } 178 | 179 | function npmRunCommands(npmCommand, listOfArgs, {silent}={}) { 180 | let logLevel = [`--loglevel=${verbose ? 'warn' : 'silent'}`]; 181 | return new Promise((resolve, reject) => { 182 | let output = []; 183 | listOfArgs.every((args) => { 184 | let command = ['npm', npmCommand].concat(logLevel).concat(args || []); 185 | let result = shell.exec(command.join(' '), {silent}); 186 | if (result.code !== 0) { 187 | log.info(`npm command '${npmCommand}' failed:\n${result.output}`); 188 | reject(new Error(`Running npm returned error code ${result.code}`)); 189 | return false; 190 | } else { 191 | output.push(result.output); 192 | return true; 193 | } 194 | }); 195 | resolve(output.join('\n')); 196 | }); 197 | } 198 | 199 | function npmRunCommand(npmCommand, args, {silent}={}) { 200 | return npmRunCommands(npmCommand, [args], {silent}); 201 | } 202 | 203 | function runNpmScript(scriptName) { 204 | return readFilePromise(`${cwd}/package.json`, `utf-8`) 205 | .then((packageJsonContent) => { 206 | let packageJson = JSON.parse(packageJsonContent); 207 | if (packageJson.scripts && packageJson.scripts[scriptName]) { 208 | log.debug(`Running ${scriptName} script...`); 209 | return npmRunCommand(`run`, scriptName); 210 | } 211 | }); 212 | } 213 | 214 | function groupPackages(packages) { 215 | var groups = [[]]; 216 | packages.forEach((pkg) => { 217 | let existingGroup = groups[groups.length - 1].concat([pkg]); 218 | if (existingGroup.join(' ').length < MAX_SHELL_LENGTH - 20) { 219 | groups[groups.length - 1] = existingGroup; 220 | } else { 221 | groups.push([pkg]); 222 | } 223 | }); 224 | return groups; 225 | } 226 | 227 | function rebuildAndIgnorePlatformSpecific() { 228 | log.debug(`Rebuilding packages in ${cwd}`); 229 | process.chdir(`${cwd}`); 230 | let packages = fs.readdirSync(`${cwd}/node_modules`); 231 | 232 | // Add scoped packages, see https://github.com/bestander/npm-git-lock/issues/38 233 | const reducer = (accumulator, currentPackage) => { 234 | if (currentPackage[0] === '@') { 235 | const scopedPackages = fs.readdirSync(`${cwd}/node_modules/${currentPackage}`) 236 | .map(p => `${currentPackage}/${p}`); 237 | return accumulator.concat(scopedPackages); 238 | } else { 239 | accumulator.push(currentPackage); 240 | return accumulator; 241 | } 242 | }; 243 | packages = packages.reduce(reducer, []); 244 | 245 | let platform = os.platform(); 246 | let packagesToRebuild = packages.filter(pkg => { 247 | let platformSpecific = PLATFORM_SPECIFIC_MODULES[pkg]; 248 | if (!platformSpecific || platformSpecific === platform) { 249 | return true; 250 | } else { 251 | log.debug(`Skipping platform-specific build of ${pkg} on ${platform}`); 252 | return false; 253 | } 254 | }); 255 | packagesToRebuild.sort(); 256 | let packageGroups = groupPackages(packagesToRebuild); 257 | return npmRunCommands('rebuild', packageGroups) 258 | .then(() => { 259 | process.chdir(`${cwd}/node_modules`); 260 | return gitGetUntracked(); 261 | }) 262 | .then((files) => { 263 | let ignored = []; 264 | try { 265 | ignored = fs.readFileSync('.gitignore', {encoding: 'utf8'}).split('\n'); 266 | } catch (e) { 267 | // ignore errors while reading .gitignore 268 | } 269 | ignored = ignored.concat(files); 270 | ignored.sort(); 271 | ignored = uniq(ignored); 272 | fs.writeFileSync('.gitignore', ignored.join('\n'), {encoding: 'utf8'}); 273 | return git(`add .gitignore`); 274 | }); 275 | } 276 | 277 | function installPackagesTagAndPushToRemote() { 278 | log.debug(`Requested tag does not exist, installing node_modules`); 279 | process.chdir(`${cwd}/node_modules`); 280 | // Stash any local changes before switching to master. 281 | // This doesn't seem very elegant... Maybe we should rather hard-reset master to origin/master. 282 | // This just seems a little "safer". 283 | // We should also think about what happens if origin/master diverges between here and the actual push. 284 | return git(`stash save --include-untracked`) 285 | .then(() => { 286 | return git(`checkout master`); 287 | }) 288 | .then(() => { 289 | // Pull first so that the push later does not (or at least is much less likely to) 290 | // fail due to diverged branches. 291 | return git(`pull`); 292 | }) 293 | .then(() => { 294 | if (!incrementalInstall) { 295 | log.debug(`Removing everything from node_modules`); 296 | return delPromise([`**`, `!.git/`]); 297 | } 298 | }) 299 | .then(() => { 300 | process.chdir(`${cwd}`); 301 | return Promise.resolve(); 302 | }) 303 | .then(() => { 304 | if (crossPlatform) { 305 | return runNpmScript('preinstall'); 306 | } 307 | return Promise.resolve(); 308 | }) 309 | .then(() => { 310 | var options = []; 311 | if (crossPlatform) { 312 | log.debug(`Running 'npm install'`); 313 | options.push('--ignore-scripts'); 314 | } 315 | if (production) { 316 | log.debug(`Running 'npm install --production'`); 317 | options.push('--production'); 318 | } 319 | log.debug(`This might take a few minutes -- please be patient`); 320 | return npmRunCommand(`install`, options); 321 | }) 322 | .then(() => { 323 | if (crossPlatform) { 324 | return runNpmScript('postinstall'); 325 | } 326 | return Promise.resolve(); 327 | }) 328 | .then(() => { 329 | log.debug(`All packages installed, adding files to repo`); 330 | process.chdir(`${cwd}/node_modules`); 331 | return git(`add .`); 332 | }) 333 | .then(() => { 334 | if (crossPlatform) { 335 | return rebuildAndIgnorePlatformSpecific(); 336 | } 337 | }) 338 | .then(() => { 339 | return npmRunCommand(`--version`, [], {silent: true}); 340 | }) 341 | .then((versionOutput) => { 342 | let npmVersion = versionOutput.trim(); 343 | log.debug(`Ran npm ${npmVersion}`); 344 | process.chdir(`${cwd}/node_modules`); 345 | return gitHasChanges() 346 | .then((hasChanges) => { 347 | if (hasChanges) { 348 | // Only make another commit if there are actual changes (avoiding an "empty" commit). 349 | // Changes in the project's package.json might not lead to changes in installed dependencies 350 | // (e.g. because only other metadata was changed). 351 | // Then running npm-git-lock will not install new dependencies, if --incremental-install is set. 352 | return git(`commit -a -m "sealing package.json dependencies of version ${packageJsonVersion}, using npm ${npmVersion}"`) 353 | .then(() => { 354 | log.debug(`Committed`); 355 | }); 356 | } 357 | }); 358 | }) 359 | .then(() => { 360 | log.debug(`Adding tag`); 361 | return git(`tag ${packageJsonSha1}`) 362 | .catch(() => { 363 | // Ignore errors while tagging (it's not a problem if the tag already exists) 364 | }) 365 | .then(() => { 366 | log.debug(`Pushing tag ${packageJsonSha1} to ${repo}`); 367 | return git(`push ${repo} master --tags`); 368 | }); 369 | }); 370 | } 371 | }; 372 | -------------------------------------------------------------------------------- /test/checkout-node-modules.spec.es6: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let fs = require(`fs`); 3 | var execSync = require(`child_process`).execSync; 4 | let git = require(`git-promise`); 5 | var rewire = require("rewire"); 6 | let expect = require(`chai`).expect; 7 | let stringify = require(`json-stable-stringify`); 8 | let shell = require(`shelljs`); 9 | let semver = require(`semver`); 10 | 11 | /** 12 | * Those are integration tests that depend on Git and npm being available in CLI. 13 | * Every test uses a local git repo and local npm repo to let the tests run independent of internet connection 14 | * 15 | * I was tempted to use same async-await or promise API for tests code 16 | * but it was a bit more hassle than just doing a sequence of execSync commands 17 | * Maybe I should have done the same for the source code but then it would not be as fun. 18 | * 19 | */ 20 | describe(`npm-git-lock`, function() { 21 | this.timeout(20000); 22 | 23 | let cwd = process.cwd(); 24 | let nodeModulesRemoteRepo = `remote-repo`; 25 | let testProjectFolder = `test-project-folder`; 26 | 27 | const npmVersion = shell.exec(`npm --version`, {silent: true}).output; 28 | const npm3 = semver.gte(npmVersion, '3.0.0'); 29 | 30 | beforeEach(() => { 31 | process.chdir(`${cwd}/test`); 32 | 33 | // set up clean folder for testing project 34 | execSync(`rm -rf ${cwd}/test/${testProjectFolder}`); 35 | execSync(`mkdir ${testProjectFolder}`); 36 | 37 | // set up clean folder for remote repo for npm modules 38 | execSync(`rm -rf ${nodeModulesRemoteRepo}`); 39 | execSync(`mkdir ${nodeModulesRemoteRepo}`); 40 | 41 | // set up git in remote repo for npm moduels 42 | process.chdir(`${cwd}/test/${nodeModulesRemoteRepo}`); 43 | execSync(`git init`); 44 | execSync(`touch file1`); 45 | execSync(`git add .`); 46 | execSync(`git commit -a -m "first commit" `); 47 | execSync(`git config --bool core.bare true`); 48 | }); 49 | 50 | afterEach(function () { 51 | process.chdir(`${cwd}/test`); 52 | execSync(`rm -rf ${nodeModulesRemoteRepo}`); 53 | execSync(`rm -rf ${testProjectFolder}`); 54 | }); 55 | 56 | it(`should do a fresh npm install and push results to remote repo master branch when get repo in node_modules is not present`, function(done) { 57 | 58 | process.chdir(`${cwd}/test/${testProjectFolder}`); 59 | const packageJson = { 60 | name: 'my-project', 61 | version: '1.0.0', 62 | dependencies: { 63 | 'fake-module': 'file:../fixtures/fake-module', 64 | }, 65 | devDependencies: { 66 | }, 67 | author: 'Konstantin Raev', 68 | license: 'MIT', 69 | }; 70 | fs.writeFileSync(`package.json`, JSON.stringify(packageJson)); 71 | 72 | const packageJsonSha1 = require(`crypto`).createHash(`sha1`).update(stringify([ 73 | packageJson.dependencies, 74 | packageJson.devDependencies, 75 | ])).digest(`base64`); 76 | 77 | require(`../src/checkout-node-modules`)(`${cwd}/test/${testProjectFolder}`, { 78 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 79 | verbose: true} 80 | ) 81 | .then(() => { 82 | process.chdir(`${cwd}/test/${nodeModulesRemoteRepo}`); 83 | return git(`show-ref --tags`, (output) => { 84 | return output.trim().split("\n"); 85 | }); 86 | }) 87 | .then((refTags) => { 88 | // there is a tag in nodeModulesRemoteRepo with tagged with package.json hash 89 | expect(refTags.filter((refTag) => refTag.indexOf(`refs/tags/${packageJsonSha1}`) !== -1).length).to.equal(1); 90 | }) 91 | .then(() => { 92 | // there is the same tag in project`s node_modules 93 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 94 | return git(`git describe --tags`); 95 | }) 96 | .then((tag) => { 97 | // current tag in node_modules repo is package.json hash 98 | expect(packageJsonSha1).to.equal(tag.trim()); 99 | }) 100 | .then(() => { 101 | // module has been installed in node_modules 102 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).to.contain(`fake-module`); 103 | let packageInstalled = JSON.parse(fs.readFileSync(`${cwd}/test/${testProjectFolder}/node_modules/fake-module/package.json`, `utf-8`)); 104 | let packageInRepo = JSON.parse(fs.readFileSync(`${cwd}/test/fixtures/fake-module/package.json`, `utf-8`)); 105 | expect(packageInstalled.name).to.equal(packageInRepo.name); 106 | }) 107 | .then(() => done(), done); 108 | }); 109 | 110 | // Apparently, only npm@3 runs the custom "install" script of fake-platform-specific-module. 111 | // So we only support --cross-platform on npm>=3 and disable the related tests on npm<3. 112 | npm3 && it(`should build but not commit platform-specific build artifacts to version control when run in cross-platform mode`, function(done) { 113 | 114 | process.chdir(`${cwd}/test/${testProjectFolder}`); 115 | let packageJson = stringify({ 116 | "name": "my-project", 117 | "version": "1.0.0", 118 | "dependencies": { 119 | "fake-platform-specific-module": "file:../fixtures/fake-platform-specific-module" 120 | }, 121 | "devDependencies": { 122 | }, 123 | "author": "Jan Poeschko", 124 | "license": "MIT" 125 | }); 126 | fs.writeFileSync(`package.json`, packageJson); 127 | 128 | require(`../src/checkout-node-modules`)(`${cwd}/test/${testProjectFolder}`, { 129 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 130 | verbose: true, 131 | crossPlatform: true 132 | }) 133 | .then(() => { 134 | // the platform-specific file should be there after building the project 135 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules/fake-platform-specific-module`)).to.contain(`some-platform-specific-file`); 136 | }) 137 | .then(() => { 138 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules/fake-platform-specific-module`); 139 | return git(`ls-tree --name-only -r HEAD`, (output) => { 140 | return output.trim().split("\n"); 141 | }); 142 | }) 143 | .then((files) => { 144 | // the platform-specific file should not be in version control 145 | expect(files).to.not.contain('some-platform-specific-file'); 146 | }) 147 | .then(() => { 148 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 149 | return git(`check-ignore fake-platform-specific-module/some-platform-specific-file`) 150 | .catch(() => { 151 | // When `git check-ignore` exits with an error, that means the file is not ignored. 152 | expect.fail(`Platform-specific file should be ignored by Git.`); 153 | }); 154 | }) 155 | .then(() => done(), done); 156 | }); 157 | 158 | it(`should checkout node_modules from remote repo resetting all local changes`, function(done) { 159 | 160 | process.chdir(`${cwd}/test/${testProjectFolder}`); 161 | const packageJson = { 162 | name: 'my-project', 163 | version: '1.0.0', 164 | dependencies: { 165 | 'fake-module': 'file:../fixtures/fake-module', 166 | }, 167 | devDependencies: { 168 | }, 169 | author: 'Konstantin Raev', 170 | license: 'MIT', 171 | }; 172 | fs.writeFileSync(`package.json`, JSON.stringify(packageJson)); 173 | 174 | // set up git in node_modules folder 175 | execSync(`git clone ${cwd}/test/${nodeModulesRemoteRepo} node_modules`); 176 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 177 | execSync(`touch file2`); 178 | execSync(`git add .`); 179 | execSync(`git commit -a -m "node_modules is cached"`); 180 | 181 | const packageJsonSha1 = require(`crypto`).createHash(`sha1`).update(stringify([ 182 | packageJson.dependencies, 183 | packageJson.devDependencies, 184 | ])).digest(`base64`); 185 | 186 | execSync(`git tag ${packageJsonSha1}`); 187 | execSync(`git push origin master --tags`); 188 | 189 | // add some change new to local node_modules repo 190 | execSync(`touch file3`); 191 | execSync(`git add .`); 192 | execSync(`git commit -a -m "another commit that should be ignored" `); 193 | execSync(`git tag SOMERANDOMTAG`); 194 | 195 | require(`../src/checkout-node-modules`)(`${cwd}/test/${testProjectFolder}`, { 196 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 197 | verbose: true 198 | }) 199 | .then(() => { 200 | // there is the same tag in project`s node_modules 201 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 202 | return git(`git describe --tags`); 203 | }) 204 | .then((tag) => { 205 | // current tag in node_modules repo is package.json hash 206 | expect(packageJsonSha1).to.equal(tag.trim()); 207 | }) 208 | .then(() => { 209 | // we don`t expect npm install was called 210 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).not.to.contain(`fake-module`); 211 | // commit with file 3 is to be reverted 212 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).not.to.contain(`file3`); 213 | // commit with file 2 should be present 214 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).to.contain(`file2`); 215 | }) 216 | .then(() => done(), done); 217 | }); 218 | 219 | it(`should not do an npm install if remote repo master branch already has a tag with package.json hash`, function(done) { 220 | 221 | process.chdir(`${cwd}/test/${testProjectFolder}`); 222 | const packageJson = { 223 | name: 'my-project', 224 | version: '2.0.0', 225 | dependencies: { 226 | 'fake-module': 'file:../fixtures/fake-module', 227 | }, 228 | devDependencies: { 229 | }, 230 | author: 'Konstantin Raev', 231 | license: 'MIT', 232 | }; 233 | fs.writeFileSync(`package.json`, JSON.stringify(packageJson)); 234 | // just add a tag to master branch then no npm innstallation is necessary 235 | process.chdir(`${cwd}/test/${nodeModulesRemoteRepo}`); 236 | 237 | const packageJsonSha1 = require(`crypto`).createHash(`sha1`).update(stringify([ 238 | packageJson.dependencies, 239 | packageJson.devDependencies, 240 | ])).digest(`base64`); 241 | 242 | execSync(`git tag ${packageJsonSha1}`); 243 | 244 | require(`../src/checkout-node-modules`)(`${cwd}/test/${testProjectFolder}`, { 245 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 246 | verbose: true 247 | }) 248 | .then(() => { 249 | // there is the same tag in project`s node_modules 250 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 251 | return git(`git describe --tags`); 252 | }) 253 | .then((tag) => { 254 | // current tag in node_modules repo is package.json hash 255 | expect(packageJsonSha1).to.equal(tag.trim()); 256 | }) 257 | .then(() => { 258 | // we don`t expect npm install was called 259 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).not.to.contain(`fake-module`); 260 | }) 261 | .then(() => done(), done); 262 | }); 263 | 264 | npm3 && it(`should not rebuild platform-specific modules if node_modules is already at the right commit`, function(done) { 265 | 266 | process.chdir(`${cwd}/test/${testProjectFolder}`); 267 | let packageJson = stringify({ 268 | "name": "my-project", 269 | "version": "2.0.0", 270 | "dependencies": { 271 | "fake-platform-specific-module": "file:../fixtures/fake-platform-specific-module" 272 | }, 273 | "devDependencies": { 274 | }, 275 | "author": "Jan Poeschko", 276 | "license": "MIT" 277 | }); 278 | fs.writeFileSync(`package.json`, packageJson); 279 | let checkout = require(`../src/checkout-node-modules`); 280 | return checkout(`${cwd}/test/${testProjectFolder}`, { 281 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, verbose: true, crossPlatform: true 282 | }) 283 | .then(() => { 284 | // delete the platform specific file 285 | execSync(`rm ${cwd}/test/${testProjectFolder}/node_modules/fake-platform-specific-module/some-platform-specific-file`); 286 | // do the same install another time 287 | return checkout(`${cwd}/test/${testProjectFolder}`, { 288 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, verbose: true, crossPlatform: true 289 | }) 290 | }) 291 | .then(() => { 292 | // we don't expect a rebuild, i.e. the platform-specific file is still not there 293 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules/fake-platform-specific-module`)).not.to.contain(`some-platform-specific-file`); 294 | }) 295 | .then(() => done(), done); 296 | }); 297 | 298 | it(`(back compatible) should not do an npm install if remote repo master branch already has a tag with package.json hash`, function(done) { 299 | 300 | process.chdir(`${cwd}/test/${testProjectFolder}`); 301 | const packageJson = { 302 | name: 'my-project', 303 | version: '2.0.0', 304 | dependencies: { 305 | 'fake-module': 'file:../fixtures/fake-module', 306 | }, 307 | devDependencies: { 308 | }, 309 | author: 'Konstantin Raev', 310 | license: 'MIT', 311 | }; 312 | fs.writeFileSync(`package.json`, JSON.stringify(packageJson)); 313 | // just add a tag to master branch then no npm innstallation is necessary 314 | process.chdir(`${cwd}/test/${nodeModulesRemoteRepo}`); 315 | 316 | const packageJsonSha1 = require(`crypto`).createHash(`sha1`).update(stringify(packageJson)).digest(`base64`); 317 | 318 | execSync(`git tag ${packageJsonSha1}`); 319 | 320 | require(`../src/checkout-node-modules`)(`${cwd}/test/${testProjectFolder}`, { 321 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 322 | verbose: true, 323 | checkAllJsonElements: true 324 | }) 325 | .then(() => { 326 | // there is the same tag in project`s node_modules 327 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 328 | return git(`git describe --tags`); 329 | }) 330 | .then((tag) => { 331 | // current tag in node_modules repo is package.json hash 332 | expect(packageJsonSha1).to.equal(tag.trim()); 333 | }) 334 | .then(() => { 335 | // we don`t expect npm install was called 336 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).not.to.contain(`fake-module`); 337 | }) 338 | .then(() => done(), done); 339 | }); 340 | 341 | npm3 && it(`(back compatible) should not rebuild platform-specific modules if node_modules is already at the right commit`, function(done) { 342 | 343 | process.chdir(`${cwd}/test/${testProjectFolder}`); 344 | let packageJson = stringify({ 345 | "name": "my-project", 346 | "version": "2.0.0", 347 | "dependencies": { 348 | "fake-platform-specific-module": "file:../fixtures/fake-platform-specific-module" 349 | }, 350 | "devDependencies": { 351 | }, 352 | "author": "Jan Poeschko", 353 | "license": "MIT" 354 | }); 355 | fs.writeFileSync(`package.json`, packageJson); 356 | let checkout = require(`../src/checkout-node-modules`); 357 | return checkout(`${cwd}/test/${testProjectFolder}`, { 358 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, verbose: true, crossPlatform: true, checkAllJsonElements: true 359 | }) 360 | .then(() => { 361 | // delete the platform specific file 362 | execSync(`rm ${cwd}/test/${testProjectFolder}/node_modules/fake-platform-specific-module/some-platform-specific-file`); 363 | // do the same install another time 364 | return checkout(`${cwd}/test/${testProjectFolder}`, { 365 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, verbose: true, crossPlatform: true, checkAllJsonElements: true 366 | }) 367 | }) 368 | .then(() => { 369 | // we don't expect a rebuild, i.e. the platform-specific file is still not there 370 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules/fake-platform-specific-module`)).not.to.contain(`some-platform-specific-file`); 371 | }) 372 | .then(() => done(), done); 373 | }); 374 | 375 | it(`should replace / in package json hash with _`, function(done) { 376 | 377 | let fakeHash = "/1g8hUui8sC2JtwIkvw/GmyQYsA="; 378 | let checkoutNodeModules = rewire("../src/checkout-node-modules"); 379 | checkoutNodeModules.__set__('crypto', { 380 | createHash: () => { 381 | console.log("CREATE HASH"); 382 | return { 383 | update: () => { 384 | console.log("CALLED UPDATE"); 385 | return { 386 | digest: () => fakeHash 387 | }; 388 | } 389 | }; 390 | } 391 | }); 392 | 393 | process.chdir(`${cwd}/test/${testProjectFolder}`); 394 | let packageJson = stringify({ 395 | "name": "my-project", 396 | "version": "2.0.0", 397 | "dependencies": { 398 | "fake-module": "file:../fixtures/fake-module" 399 | }, 400 | "devDependencies": { 401 | }, 402 | "author": "Konstantin Raev", 403 | "license": "MIT" 404 | }); 405 | fs.writeFileSync(`package.json`, packageJson); 406 | // just add a tag to master branch then no npm innstallation is necessary 407 | process.chdir(`${cwd}/test/${nodeModulesRemoteRepo}`); 408 | let packageJsonSha1 = fakeHash.replace(/\//g, "_"); 409 | execSync(`git tag ${packageJsonSha1}`); 410 | 411 | checkoutNodeModules(`${cwd}/test/${testProjectFolder}`, { 412 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 413 | verbose: true} 414 | ) 415 | .then(() => { 416 | // there is the same tag in project`s node_modules 417 | process.chdir(`${cwd}/test/${testProjectFolder}/node_modules`); 418 | return git(`git describe --tags`); 419 | }) 420 | .then((tag) => { 421 | // current tag in node_modules repo is package.json hash 422 | expect(packageJsonSha1).to.equal(tag.trim()); 423 | }) 424 | .then(() => { 425 | // we don`t expect npm install was called 426 | expect(fs.readdirSync(`${cwd}/test/${testProjectFolder}/node_modules`)).not.to.contain(`fake-module`); 427 | }) 428 | .then(done, done); 429 | }); 430 | 431 | npm3 && it(`should not perform a fresh install when --incremental-install is set`, (done) => { 432 | 433 | function writeFakeModule(version) { 434 | let moduleJson = stringify({ 435 | "name": "fake-changing-module", 436 | "version": version, 437 | "author": "Jan Poeschko", 438 | "license": "MIT" 439 | }); 440 | fs.writeFileSync(`${cwd}/test/${testProjectFolder}/fake-changing-module/package.json`, moduleJson); 441 | } 442 | 443 | function writePackage(version) { 444 | let packageJson = stringify({ 445 | "name": "my-project", 446 | "version": version, 447 | "dependencies": { 448 | "fake-changing-module": `file:./fake-changing-module` 449 | }, 450 | "author": "Jan Poeschko", 451 | "license": "MIT" 452 | }); 453 | fs.writeFileSync(`${cwd}/test/${testProjectFolder}/package.json`, packageJson); 454 | } 455 | 456 | process.chdir(`${cwd}/test/${testProjectFolder}`); 457 | execSync(`mkdir fake-changing-module`); 458 | writePackage('1.0.0'); 459 | writeFakeModule('1.0.0'); 460 | 461 | const npmGitLock = require(`../src/checkout-node-modules`); 462 | 463 | npmGitLock(`${cwd}/test/${testProjectFolder}`, { 464 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 465 | verbose: true 466 | }) 467 | .then(() => { 468 | const packageInstalled = JSON.parse(fs.readFileSync(`${cwd}/test/${testProjectFolder}/node_modules/fake-changing-module/package.json`, `utf-8`)); 469 | expect(packageInstalled.version).to.equal('1.0.0'); 470 | }) 471 | .then(() => { 472 | writePackage('1.1.0'); 473 | writeFakeModule('1.1.0'); 474 | }) 475 | .then(() => { 476 | return npmGitLock(`${cwd}/test/${testProjectFolder}`, { 477 | repo: `${cwd}/test/${nodeModulesRemoteRepo}`, 478 | verbose: true, 479 | incrementalInstall: true 480 | }); 481 | }) 482 | .then(() => { 483 | const packageInstalled = JSON.parse(fs.readFileSync(`${cwd}/test/${testProjectFolder}/node_modules/fake-changing-module/package.json`, `utf-8`)); 484 | expect(packageInstalled.version).to.equal('1.0.0'); 485 | }) 486 | .then(() => done(), done); 487 | }); 488 | }); 489 | -------------------------------------------------------------------------------- /test/fixtures/fake-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-module", 3 | "version": "2.0.0", 4 | "dependencies": { 5 | }, 6 | "devDependencies": { 7 | }, 8 | "author": "Konstantin Raev", 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/fake-platform-specific-module/install.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var os = require('os'); 3 | var path = require('path'); 4 | 5 | console.log('Running platform-specific build'); 6 | var fileName = path.resolve(__dirname, 'some-platform-specific-file'); 7 | 8 | fs.writeFile(fileName, os.platform, function(err) { 9 | if(err) { 10 | return console.log(err); 11 | } 12 | 13 | console.log('Wrote platform-specific file: ' + fileName); 14 | }); 15 | -------------------------------------------------------------------------------- /test/fixtures/fake-platform-specific-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-platform-specific-module", 3 | "version": "1.0.0", 4 | "description": "A fake module with a custom install script that creates an extra file.", 5 | "dependencies": { 6 | }, 7 | "devDependencies": { 8 | }, 9 | "scripts": { 10 | "install": "echo \"Running custom install script\" && node install.js" 11 | }, 12 | "author": "Jan Poeschko", 13 | "license": "MIT" 14 | } 15 | --------------------------------------------------------------------------------