├── .gitignore ├── .gitattributes ├── CHANGES.md ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | index.js text eol=lf 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 1.0.7 2 | 3 | * Fixed a line ending problem causing the tool not to work on non-Windows systems. 4 | 5 | ### 1.0.6 6 | 7 | * Updated nodegit to v0.18.0+, to allow npmgitdev to work with Node.js 7. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npmgitdev", 3 | "version": "1.0.7", 4 | "description": "A wrapper around npm that allows real live git repos to be cloned into node_modules.", 5 | "main": "index.js", 6 | "bin": { 7 | "npmgitdev": "./index.js" 8 | }, 9 | "scripts": { 10 | "postpublish": "bash -c \"git tag -a ${npm_package_version} -m \"${npm_package_version}\" && git push origin ${npm_package_version}\"" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/TerriaJS/npmgitdev.git" 15 | }, 16 | "author": "", 17 | "license": "Apache-2.0", 18 | "bugs": { 19 | "url": "https://github.com/TerriaJS/npmgitdev/issues" 20 | }, 21 | "homepage": "https://github.com/TerriaJS/npmgitdev#readme", 22 | "dependencies": { 23 | "nodegit": "^0.18.0" 24 | }, 25 | "engineStrict": true, 26 | "engines": { 27 | "node": ">=5.10.0", 28 | "npm": ">=3.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npmgitdev 2 | 3 | npmgitdev a wrapper around `npm` version 3+ that allows you to work with git repos cloned directly into node_modules. 4 | 5 | When we're developing modular software, we often need to edit multiple separate npm packages simultaneously. The "official" way to do this is with `npm link`. We clone a separate repo for each package, and then link it into the appropriate places. The problem is, `npm link` creates all sorts of complexities and bugs. 6 | 7 | It would be nice if we could avoid all this complexity by simply cloning a repo into `node_modules`. Unfortunately, `npm install` will bail (refuse to do anything) when it detects a `.git` directory inside any package in node_modules. 8 | 9 | `npmgitdev` avoids this problem by: 10 | 11 | * Ensuring that all git repos are clean (have no changes in the working directory or index), so that if npm decides to replace the package you won't lose any work. 12 | * Temporarily changing the required version to match the one specified in the git repo's package.json, so npm is not inclined to mess with it. 13 | * Temporarily copying all `devDependencies` of git packages to `dependencies`, because you'll probably need them while you're developing your git package. 14 | * Hiding the `.git` directory temporarily while invoking an npm command. 15 | * Cleaning up all the temporary changes after the git command completes. 16 | 17 | Installation: 18 | 19 | ``` 20 | npm install -g npmgitdev 21 | ``` 22 | 23 | Usage: 24 | 25 | ``` 26 | # in your project's directory 27 | cd node_modules 28 | git clone https://github.com/TerriaJS/terriajs # or whatever repo you want to work with inside your project 29 | cd .. 30 | 31 | # later, or whenever you want: 32 | npmgitdev install 33 | ``` 34 | 35 | The end result is that npm installs packages exactly as it would if you copied all your `devDependencies` to `dependencies` and then published the package to npm. npm's package deduplication actually works, unlike with `npm link`! 36 | 37 | If you accidentally run `npm install` instead, it should be harmless because `npm` will bail when it sees your `.git` directory. 38 | 39 | ## Questions and Answers 40 | 41 | > What happens if my git package is referenced from multiple other packages in my dependency tree? 42 | 43 | Generally, you should clone your git package into the top-level `node_modules` directory of your application. Then, `npmgitdev` will ensure that npm keeps it there by adding a dependency in the top-level `package.json` to that exact version of the package. If other packages elsewhere in the dependency tree depend on a semver-compatible version of that package, npm 3's deduplication wil avoid installing any other copies of that package elsewhere in the tree. 44 | 45 | However, if other packages depend on an _incompatible_ version of that package, or if their dependency is to a Git URL or something else other than a version, npm _will_ install additional copies. If you instead intended for all packages to share the Git repo version of the package, you simply need to delete the extra copies that npm installed. Use `npmgitdev list ` to see what versions exist in your dependency tree. 46 | 47 | ## Dependencies 48 | NPM >= 3 and Node >= 5.10.0 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs'); 3 | var Git = require('nodegit'); 4 | var path = require('path'); 5 | var spawnSync = require('child_process').spawnSync; 6 | 7 | function forEachPackage(directory, callback) { 8 | var nodeModules = path.join(directory, 'node_modules'); 9 | 10 | if (!fs.existsSync(nodeModules)) { 11 | return; 12 | } 13 | 14 | var packages = fs.readdirSync(nodeModules); 15 | packages.forEach(function(packageName) { 16 | var packagePath = path.join(nodeModules, packageName); 17 | callback(packageName, packagePath); 18 | forEachPackage(packagePath, callback); 19 | }); 20 | } 21 | 22 | var rootDir = path.resolve('.'); 23 | var tempDir = fs.mkdtempSync(path.join(rootDir, 'npmgitdev-')); 24 | 25 | var promises = []; 26 | 27 | if (process.argv.length === 2) { 28 | console.log('npmgitdev is a wrapper around npm, so it uses exactly the same arguments. See the README. Try `npmgitdev install`'); 29 | process.exit(1); 30 | } 31 | 32 | if (path.parse(path.resolve('..')).name === 'node_modules') { 33 | output ('** Warning: You are in a directory inside a node_modules directory. Most npmgitdev commands should be run in the enclosing project directory (cd ../..).'); 34 | } 35 | 36 | function output(s) { 37 | console.log('[npmgitdev] ' + s); 38 | } 39 | 40 | 41 | forEachPackage(rootDir, function(packageName, packagePath) { 42 | var gitDir = path.join(packagePath, '.git'); 43 | if (!fs.existsSync(gitDir)) { 44 | return; 45 | } 46 | 47 | var stat = fs.lstatSync(packagePath); 48 | if (stat.isSymbolicLink()) { 49 | output('Skipping symlinked package: ' + packagePath); 50 | return; 51 | } 52 | 53 | var targetDir = path.join(tempDir, packageName); 54 | var createdTargetPath = false; 55 | if (fs.existsSync(targetDir)) { 56 | targetDir = fs.mkdtempSync(tempDir, packageName + '-'); 57 | createdTargetPath = true; 58 | } 59 | 60 | var promise = Git.Repository.open(packagePath).then(function(repo) { 61 | var hasUncommittedChanges = false; 62 | var messages = []; 63 | return Git.Status.foreach(repo, function(file, status) { 64 | if (status !== Git.Status.STATUS.IGNORED) { 65 | hasUncommittedChanges = true; 66 | var statusTexts = Object.keys(Git.Status.STATUS).filter(function(key) {return Git.Status.STATUS[key] & status;}); 67 | messages.push(' * ' + file + ': ' + statusTexts.join(', ')); 68 | } 69 | }).then(function(config) { 70 | repo.free(); 71 | 72 | var packageJson = readPackageJson(packagePath); 73 | 74 | return { 75 | hasUncommittedChanges: hasUncommittedChanges, 76 | messages: messages, 77 | packageName: packageName, 78 | packagePath: packagePath, 79 | original: gitDir, 80 | renamed: targetDir, 81 | createdRenamedPath: createdTargetPath, 82 | version: packageJson.version 83 | }; 84 | }); 85 | }); 86 | 87 | promises.push(promise); 88 | }); 89 | 90 | Promise.all(promises).then(function(mappings) { 91 | var mappingsPath = path.join(tempDir, 'mappings.json'); 92 | fs.writeFileSync(mappingsPath, JSON.stringify(mappings, undefined, ' ')); 93 | 94 | var uncommittedPackages = mappings.filter(function(mapping) { return mapping.hasUncommittedChanges; }); 95 | if (uncommittedPackages.length > 0) { 96 | output('The following packages have uncommitted changes:'); 97 | uncommittedPackages.forEach(function(mapping) { 98 | output(' ' + mapping.packagePath); 99 | output(mapping.messages.join('\n')); 100 | }); 101 | output('Please ensure all packages with a git repository have a clean working directory.') 102 | } 103 | 104 | var i = 0; 105 | var mapping; 106 | var rootPackageJsonText; 107 | 108 | if (uncommittedPackages.length === 0) { 109 | // Temporarily each git directory's version to the root package.json. 110 | // That way npm won't get clever and "upgrade" it. 111 | rootPackageJsonText = readPackageJsonText(rootDir); 112 | var rootPackageJson = JSON.parse(rootPackageJsonText); 113 | 114 | rootPackageJson.dependencies = rootPackageJson.dependencies || {}; 115 | mappings.forEach(function(mapping) { 116 | rootPackageJson.dependencies[mapping.packageName] = mapping.version; 117 | if (rootPackageJson.devDependencies) { 118 | delete rootPackageJson.devDependencies[mapping.packageName]; 119 | } 120 | }); 121 | 122 | writePackageJson(rootDir, rootPackageJson); 123 | 124 | try { 125 | for (i = 0; i < mappings.length; ++i) { 126 | mapping = mappings[i]; 127 | output('Moving ' + mapping.original + ' to ' + mapping.renamed); 128 | fs.renameSync(mapping.original, mapping.renamed); 129 | 130 | var packageJsonPath = path.join(mapping.packagePath, 'package.json'); 131 | if (fs.existsSync(packageJsonPath)) { 132 | var originalPackageJsonText = fs.readFileSync(packageJsonPath, 'utf8'); 133 | var originalPackageJson = JSON.parse(originalPackageJsonText); 134 | if (originalPackageJson.devDependencies) { 135 | output('Temporarily adding devDependencies to dependencies in ' + packageJsonPath); 136 | mapping.originalPackageJsonText = originalPackageJsonText; 137 | mapping.packageJsonPath = packageJsonPath; 138 | var newPackageJson = Object.assign({}, originalPackageJson); 139 | newPackageJson.dependencies = Object.assign({}, newPackageJson.dependencies || {}, newPackageJson.devDependencies); 140 | fs.writeFileSync(packageJsonPath, JSON.stringify(newPackageJson, undefined, ' ')); 141 | } 142 | } 143 | } 144 | } catch(e) { 145 | output(e); 146 | } 147 | } 148 | 149 | if (i === mappings.length) { 150 | try { 151 | var passargs = process.argv.slice(2); 152 | output('Running `npm ' + passargs.join(' ') + '`)'); 153 | var result = spawnSync('npm', passargs, { 154 | stdio: 'inherit', 155 | shell: true 156 | }); 157 | output('npm finished'); 158 | } catch (e) { 159 | output(e); 160 | } 161 | } 162 | 163 | // Restore original root package.json. 164 | if (rootPackageJsonText) { 165 | writePackageJsonText(rootDir, rootPackageJsonText); 166 | } 167 | 168 | var errors = false; 169 | var j; 170 | for (j = 0; j < i; ++j) { 171 | mapping = mappings[j]; 172 | if (mapping.originalPackageJsonText) { 173 | output('Restoring original ' + mapping.packageJsonPath); 174 | fs.writeFileSync(mapping.packageJsonPath, mapping.originalPackageJsonText); 175 | } 176 | output('Returning ' + mapping.renamed + ' to ' + mapping.original); 177 | try { 178 | fs.renameSync(mapping.renamed, mapping.original); 179 | } catch(e) { 180 | output('** Error while renaming ' + mapping.renamed + ' back to ' + mapping.original); 181 | errors = true; 182 | } 183 | } 184 | 185 | for (j = i; j < mappings.length; ++j) { 186 | mapping = mappings[j]; 187 | if (mapping.createdRenamedPath) { 188 | try { 189 | fs.rmdirSync(mapping.renamed); 190 | } catch(e) { 191 | output('** Error while removing ' + mapping.renamed); 192 | errors = true; 193 | } 194 | } 195 | } 196 | 197 | if (!errors) { 198 | fs.unlinkSync(mappingsPath); 199 | fs.rmdirSync(tempDir); 200 | } 201 | }).catch(function(e) { 202 | output(e); 203 | output(e.stack); 204 | }); 205 | 206 | function readPackageJsonText(packagePath) { 207 | var packageJsonPath = path.join(packagePath, 'package.json'); 208 | return fs.readFileSync(packageJsonPath, 'utf8'); 209 | } 210 | 211 | function readPackageJson(packagePath) { 212 | return JSON.parse(readPackageJsonText(packagePath)); 213 | } 214 | 215 | function writePackageJsonText(packagePath, text) { 216 | var packageJsonPath = path.join(packagePath, 'package.json'); 217 | fs.writeFileSync(packageJsonPath, text); 218 | } 219 | 220 | function writePackageJson(packagePath, json) { 221 | writePackageJsonText(packagePath, JSON.stringify(json, undefined, ' ')); 222 | } --------------------------------------------------------------------------------