├── .gitignore ├── .jshintrc ├── .npmignore ├── BROKEN.md ├── LICENSE ├── README.md ├── TUTORIAL.md ├── offline-npm.js ├── package.json ├── test ├── cli.mocha.js ├── five │ ├── five-0.1.0.tgz │ ├── index.js │ └── package.json ├── mocha.opts ├── proxy.js ├── scoped │ ├── index.js │ ├── my-scoped-0.1.0.tgz │ └── package.json ├── semver.mocha.js └── test.sh └── try ├── .npmignore ├── five-0.1.0.tgz ├── index.js ├── package.json └── try-offline └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | offline.pid 4 | /*.log 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | tmp/ 4 | try/ 5 | offline-npm-*.tgz 6 | offline.pid 7 | npm-debug.log 8 | .jshintrc 9 | .npmignore 10 | -------------------------------------------------------------------------------- /BROKEN.md: -------------------------------------------------------------------------------- 1 | ## node@v4 2 | 3 | `>> preinstall test-package` comes BEFORE `npm verb get` 4 | 5 | ````sh 6 | npm info using npm@2.15.8 7 | npm info using node@v4.4.7 8 | > test-package@1.0.0 preinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 9 | > node -e "console.log('>> preinstall test-package')" ; npm run clean 10 | >> preinstall test-package 11 | npm info using npm@2.15.8 12 | npm info using node@v4.4.7 13 | > test-package@1.0.0 clean /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 14 | > rm -rf node_modules 15 | > test-preinst@1.0.0 preinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test/node_modules/test-preinst 16 | > node index.js 17 | npm verb addNamed ">=5.3.0 <6.0.0" is a valid semver range for semver 18 | npm verb get saving semver to /home/comm/.npm/registry.npmjs.org/semver/.cache.json 19 | > test-preinst@1.0.0 install /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test/node_modules/test-preinst 20 | > node -e "console.log('install test-preinst')" 21 | > test-preinst@1.0.0 postinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test/node_modules/test-preinst 22 | > node -e "console.log('postinstall test-preinst')" 23 | > test-package@1.0.0 install /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 24 | > node -e "console.log('>> install test-package')" 25 | >> install test-package 26 | > test-package@1.0.0 postinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 27 | > node -e "console.log('>> postinstall test-package')" 28 | >> postinstall test-package 29 | ```` 30 | 31 | ## node@v5 32 | 33 | `>> preinstall test-package` comes AFTER `npm verb get https://` 34 | 35 | --> Broken support in npm@3 36 | 37 | ````sh 38 | comm@cu:test$ npm cache clean && npm i --verbose 2>&1 | egrep ">|get|npm info using" 39 | npm info using npm@3.8.6 40 | npm info using node@v5.12.0 41 | npm verb get saving semver to /home/comm/.npm/registry.npmjs.org/semver/.cache.json 42 | npm verb addNamed ">=5.3.0 <6.0.0" is a valid semver range for semver 43 | npm verb get https://registry.npmjs.org/semver not expired, no request 44 | > test-package@1.0.0 preinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 45 | > node -e "console.log('>> preinstall test-package')" ; npm run clean 46 | >> preinstall test-package 47 | npm info using npm@3.8.6 48 | npm info using node@v5.12.0 49 | > test-package@1.0.0 clean /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 50 | > rm -rf node_modules 51 | > test-package@1.0.0 install /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 52 | > node -e "console.log('>> install test-package')" 53 | >> install test-package 54 | > test-package@1.0.0 postinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 55 | > node -e "console.log('>> postinstall test-package')" 56 | >> postinstall test-package 57 | ```` 58 | 59 | ## node@v6 60 | 61 | `>> preinstall test-package` comes AFTER `npm verb get` 62 | 63 | --> Broken support in npm@3 64 | 65 | ````sh 66 | comm@cu:test$ npm cache clean && npm i --verbose 2>&1 | egrep ">|get|npm info using" 67 | npm info using npm@3.10.3 68 | npm info using node@v6.3.0 69 | npm verb get saving semver to /home/comm/.npm/registry.npmjs.org/semver/.cache.json 70 | npm verb addNamed ">=5.3.0 <6.0.0" is a valid semver range for semver 71 | npm verb get https://registry.npmjs.org/semver not expired, no request 72 | > test-preinst@1.0.0 preinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test/node_modules/.staging/test-preinst-8a4e7582 73 | > node index.js 74 | > test-preinst@1.0.0 install /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test/node_modules/test-preinst 75 | > node -e "console.log('install test-preinst')" 76 | > test-preinst@1.0.0 postinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test/node_modules/test-preinst 77 | > node -e "console.log('postinstall test-preinst')" 78 | > test-package@1.0.0 preinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 79 | > node -e "console.log('>> preinstall test-package')" ; npm run clean 80 | >> preinstall test-package 81 | npm info using npm@3.10.3 82 | npm info using node@v6.3.0 83 | > test-package@1.0.0 clean /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 84 | > rm -rf node_modules 85 | > test-package@1.0.0 install /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 86 | > node -e "console.log('>> install test-package')" 87 | >> install test-package 88 | > test-package@1.0.0 postinstall /home/comm/workspace/commenthol/offline-npm/tmp/preinst/test 89 | > node -e "console.log('>> postinstall test-package')" 90 | >> postinstall test-package 91 | ```` 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 commenthol 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # offline-npm 2 | 3 | > **npm >= v3.x bundled with node >= v5** has **[broken](https://github.com/npm/npm/issues/10379)** this project. 4 | > `preinstall` script is since then called after requests to npm registry are made. 5 | > This makes it impossible for `offline-npm` to start as a registry server. 6 | > See [BROKEN.md](BROKEN.md) for detailed logs. 7 | 8 | Hassle-free `npm pack` including all dependencies for offline installation with `npm install` 9 | 10 | Add `offline-npm` to your project to serve a npm compatible tgz file wich contains all dependencies for offline installation with `npm install`. 11 | 12 | Additionally you can use `offline-npm -n` to install packages from your local npm cache directory (Could be useful e.g. on travelling). 13 | 14 | Even installs using `git:` or `file:` (requires node>=0.11) are considered. 15 | 16 | ## Table of Contents 17 | 18 | 19 | 20 | * [Installation](#installation) 21 | * [Usage](#usage) 22 | * [Tutorial](#tutorial) 23 | * [Install packages from npm cache offline](#install-packages-from-npm-cache-offline) 24 | * [Troubleshooting](#troubleshooting) 25 | * [License](#license) 26 | 27 | 28 | 29 | ## Installation 30 | 31 | npm install -g offline-npm 32 | 33 | 34 | ## Usage 35 | 36 | 1. Open terminal and go to your project you want to prepare for offline use. 37 | This folder needs to contain a `package.json` file. 38 | 39 | 2. Prepare your project for offline use 40 | 41 | offline-npm --add 42 | 43 | This changes the `package.json` file and adds a `offline` folder which will contain all your dependencies. 44 | 45 | 3. Pack your project 46 | 47 | npm pack 48 | 49 | Now the local cache is changed and all your projects dependencies will be downloaded into `offline/cache` and packed into the npm tgz file. 50 | 51 | > __Note__: Take care not to add a global `*.tgz` into your `.npmignore` file! 52 | 53 | > __Note__: An existing `npm-shrinkwrap.json` file will get overwritten in this step to provide install without the `--registry` switch. A backup is stored in the `./offline` folder. 54 | 55 | 4. Transfer the resulting `-.tgz` from the pack command onto a machine with no connectivity to the required registry. Execute this line from a terminal. 56 | 57 | Now install the package with: 58 | 59 | npm install [-g] -.tgz 60 | 61 | 62 | ## Tutorial 63 | 64 | Find [here](TUTORIAL.md) a step-by-step [tutorial](TUTORIAL.md) using a provided sample project. 65 | 66 | 67 | ## Install packages from npm cache offline 68 | 69 | If you want to use your local npm cache to install packages from use the option 70 | 71 | offline-npm -n [-d] 72 | 73 | > `-d` shows you some server logs on the console. 74 | 75 | Then install packages from the local npm cache with: 76 | 77 | npm --registry http://localhost:4873 [-f] install 78 | 79 | > Use the `-f` switch to force installing packages. This might be required if `npm` stops stating "shasum" errors. 80 | 81 | ## Troubleshooting 82 | 83 | 1. Never add `*.tgz` into your `.npmignore` file. Otherwise all `package.tgz` files for the offline installation will be missing. 84 | 85 | If you want to exclude previously packed versions of the package you're working with use `-*.tgz` instead. 86 | 87 | 2. The script needs access to `npm`. It is assumed that `npm` is installed alongside with `node`. If you experience problems with correcty resolving `npm`, add to your `$HOME/.profile` or `$HOME/.bashrc` 88 | 89 | export NODE_PATH=/node_modules:$NODE_PATH 90 | 91 | where `` is the path to the `node_modules` dir which contains npm. 92 | 93 | 3. If installation hangs try installing in verbose mode 94 | 95 | `npm install .tgz --verbose` 96 | 97 | If you see that some `.lock` in your files block you from progress, consider deleting them with `npm cache clean [@]` 98 | 99 | 100 | ## License 101 | 102 | Copyright (c) 2014 commenthol 103 | 104 | Software is released under [MIT][MIT]. 105 | 106 | [MIT]: ./LICENSE 107 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This little guide shall help you to understand using `offline-npm`. 4 | 5 | There is a small project in folder `try` using two dependencies `semver` and `request`. 6 | `request` itself has sub-dependencies which should be included into the final "offline" package as well. 7 | 8 | **Conventions** 9 | 10 | ```bash 11 | ## = this is line of comment 12 | #> = this is a line of output of the executed binary 13 | ``` 14 | 15 | ## Table of Contents 16 | 17 | * [Prepare & Pack](#prepare-pack) 18 | * [Install offline](#install-offline) 19 | * [Restore "On-line" state](#restore-on-line-state) 20 | * [Offline registry server](#offline-registry-server) 21 | 22 | 23 | ## Prepare & Pack 24 | 25 | Open a terminal and lets start... 26 | 27 | ```bash 28 | ## clone this project from github 29 | git clone https://github.com/commenthol/offline-npm.git 30 | cd offline-npm 31 | ## install offline-npm from clone 32 | npm install -g 33 | ## change to folder `try` 34 | cd ./try 35 | ``` 36 | 37 | The `try` project contains a package which installs from npm registry, file and git. 38 | 39 | ```bash 40 | ## first run the project as is and install all dependencies 41 | npm install 42 | ## run index.js 43 | node index.js 44 | #> semver: true 45 | #> five: 5 46 | #> mergee: { a: 1 } 47 | #> request: DuckDuckGo 48 | ## shrinkwrap the used versions 49 | npm shrink 50 | 51 | ## ---- This would be a good point now to check-in your code into your GIT 52 | 53 | ## now prepare for offline packaging 54 | offline-npm -a 55 | #> offline-npm was added to project: try 56 | ## pack all 57 | npm pack 58 | #> > try@0.0.0 prepublish . 59 | #> > ./offline/offline-npm --prepublish ; 60 | #> 61 | #> npm WARN package.json try@0.0.0 No repository field. 62 | #> npm WARN package.json try@0.0.0 No README data 63 | #> five@0.1.0 node_modules/five 64 | #> 65 | #> mergee@0.2.2 node_modules/mergee 66 | #> 67 | #> semver@3.0.1 node_modules/semver 68 | #> 69 | #> request@2.40.0 node_modules/request 70 | #> ├── json-stringify-safe@5.0.0 71 | #> ├── aws-sign2@0.5.0 72 | #> ├── forever-agent@0.5.2 73 | #> ├── oauth-sign@0.3.0 74 | #> ├── stringstream@0.0.4 75 | #> ├── tunnel-agent@0.4.0 76 | #> ├── qs@1.0.2 77 | #> ├── node-uuid@1.4.1 78 | #> ├── mime-types@1.0.2 79 | #> ├── tough-cookie@0.12.1 (punycode@1.3.1) 80 | #> ├── http-signature@0.10.0 (assert-plus@0.1.2, asn1@0.1.11, ctype@0.5.2) 81 | #> ├── hawk@1.1.1 (cryptiles@0.2.2, sntp@0.2.4, boom@0.4.2, hoek@0.9.1) 82 | #> └── form-data@0.1.4 (mime@1.2.11, async@0.9.0, combined-stream@0.0.5) 83 | #> try-0.0.0.tgz 84 | ``` 85 | 86 | ## Install offline 87 | 88 | You got now an archive which contains all dependencies in `offline/cache`. Now you could transfer this file to your target machine. 89 | 90 | For now we change to folder `try-offline`. Disconnect your internet connection (e.g. plug-out Ethernet, or turn-off Wifi). 91 | 92 | ```bash 93 | ## copy the archive 94 | cp try-0.0.0.tgz try-offline 95 | ## change folder 96 | cd try-offline 97 | ## make a node_modules dir such to install the package herein 98 | ## now install 99 | npm install --save try-0.0.0.tgz --verbose 100 | #> npm WARN package.json try@0.0.0 No repository field. 101 | #> 102 | #> > try@0.0.0 preinstall ./test/try/try-offline/node_modules/try 103 | #> > ./offline/offline-npm --preinstall & sleep 2 ; 104 | #> 105 | #> Server running on port:4873 using cache in ./try/try-offline/node_modules/try/offline/cache/ 106 | #> 107 | #> > try@0.0.0 postinstall ./test/try/try-offline/node_modules/try 108 | #> > ./offline/offline-npm --postinstall ; 109 | #> 110 | #> try@0.0.0 node_modules/try 111 | #> ├── five@0.1.0 112 | #> ├── mergee@0.2.2 113 | #> ├── semver@3.0.1 114 | #> └── request@2.40.0 (json-stringify-safe@5.0.0, forever-agent@0.5.2, aws-sign2@0.5.0, oauth-sign@0.3.0, stringstream@0.0.4, tunnel-agent@0.4.0, qs@1.0.2, node-uuid@1.4.1, mime-types@1.0.2, form-data@0.1.4, tough-cookie@0.12.1, http-signature@0.10.0, hawk@1.1.1) 115 | 116 | ## Finally execute the `index.js` 117 | node index.js 118 | #> semver: true 119 | #> five: 5 120 | #> mergee: { a: 1 } 121 | #> { [Error: connect ECONNREFUSED] 122 | #> code: 'ECONNREFUSED', 123 | #> errno: 'ECONNREFUSED', 124 | #> syscall: 'connect' } 125 | ``` 126 | 127 | ## Restore "On-line" state 128 | 129 | If you want to continue your development you'll need to remove the offline scripts to allow a normal "online" install. 130 | 131 | ```bash 132 | ## Back in project `try` 133 | cd .. 134 | ## All files we used previously for offline packaging are still in the folder `offline` 135 | find offline 136 | #> offline/cache/... 137 | ## the `package.json` still has the offline scripts in 138 | grep offline/ package.json 139 | #> "prepublish": "./offline/offline-npm --prepublish ; ", 140 | #> "preinstall": "./offline/offline-npm --preinstall & sleep 2 ; ", 141 | #> "postinstall": "./offline/offline-npm --postinstall ; " 142 | 143 | ## remove all the "offline" stuff 144 | offline-npm -r 145 | #> offline-npm was removed from project: try 146 | ## The folder `offline` is gone 147 | find offline 148 | #> find: `offline': No such file or directory 149 | ## check `package.json` 150 | grep offline/ package.json 151 | #> 152 | ``` 153 | 154 | With this a `npm install` will give you the usual install from your preferred registry. 155 | 156 | ## Offline registry server 157 | 158 | `offline-npm` can also be used as an offline registry server. That is usefull if you want to make use of your normal npm-cache, e.g. while travelling ... 159 | 160 | ```bash 161 | ## Open in a new terminal window 162 | ## start the server using your npm cache - we use the debug mode here to see the requests 163 | offline-npm -n -d 164 | #> Server running on port:4873 using cache in ~/.npm 165 | 166 | ## In this terminal window - change to the `try` folder 167 | ## remove node_modules 168 | rm -rf node_modules 169 | ## make sure that the project is not in "offline" state 170 | offline-npm -r 171 | ## install from the npm cache using offline-npm 172 | npm --registry=http://localhost:4873/ install 173 | 174 | ## In the other terminal you should see the package requests using your offline registry server 175 | #> [2014-08-15T08:56:33.905Z] 200 /semver semver 176 | #> [2014-08-15T08:56:33.919Z] 200 /mocha mocha 177 | ``` 178 | 179 | Enjoy ... 180 | -------------------------------------------------------------------------------- /offline-npm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | /* jshint node:true */ 6 | 7 | var fs = require('fs') 8 | var path = require('path') 9 | var http = require('http') 10 | var spawn = require('child_process').spawn 11 | var npm = requireNpm() 12 | 13 | var VERSION = '0.2.1' 14 | 15 | /* 16 | * configuration settings 17 | */ 18 | var config = { 19 | port: 4873 20 | } 21 | config.npm = { 22 | prepublish: { 23 | 'cache': path.join(__dirname, 'cache') 24 | } 25 | } 26 | 27 | // --------------------------------------------------------------------- 28 | // file operations 29 | // --------------------------------------------------------------------- 30 | /* 31 | * current working directory 32 | */ 33 | function pwd () { 34 | return path.resolve(process.cwd()) 35 | } 36 | 37 | /* 38 | * make directories if they do not yet exists 39 | * credits to https://github.com/substack/node-mkdirp 40 | */ 41 | function mkdir (dir, made) { 42 | var mode = parseInt('0777', 8) & (~process.umask()) 43 | 44 | dir = path.resolve(dir) 45 | 46 | try { 47 | fs.mkdirSync(dir, mode) 48 | made = made || dir 49 | } catch (e) { 50 | switch (e.code) { 51 | case 'ENOENT': { 52 | made = mkdir(path.dirname(dir), made) 53 | mkdir(dir, made) 54 | break 55 | } 56 | default: { 57 | var stat 58 | try { 59 | stat = fs.statSync(dir) 60 | } catch (e1) { 61 | throw e 62 | } 63 | if (!stat.isDirectory()) throw e 64 | break 65 | } 66 | } 67 | } 68 | return made 69 | } 70 | 71 | /* 72 | * remove a directory 73 | * credits go to http://github.com/arturadib/shelljs 74 | */ 75 | function rmdir (dir, force) { 76 | var files 77 | var result 78 | 79 | function isWriteable (file) { 80 | var writePermission = true 81 | try { 82 | var __fd = fs.openSync(file, 'a') 83 | fs.closeSync(__fd) 84 | } catch (e) { 85 | writePermission = false 86 | } 87 | 88 | return writePermission 89 | } 90 | 91 | try { 92 | files = fs.readdirSync(dir) 93 | 94 | // Loop through and delete everything in the sub-tree after checking it 95 | for (var i = 0; i < files.length; i++) { 96 | var file = path.join(dir, files[i]) 97 | var currFile = fs.lstatSync(file) 98 | 99 | if (currFile.isDirectory()) { // Recursive function back to the beginning 100 | rmdir(file, force) 101 | } else if (currFile.isSymbolicLink()) { // Unlink symlinks 102 | if (force || isWriteable(file)) { 103 | try { 104 | fs.unlinkSync(file) 105 | } catch (e) { 106 | log.error('could not remove file (code ' + e.code + '): ' + file, true) 107 | } 108 | } 109 | } else { // Assume it's a file - perhaps a try/catch belongs here? 110 | if (force || isWriteable(file)) { 111 | try { 112 | fs.unlinkSync(file) 113 | } catch (e) { 114 | log.error('could not remove file (code ' + e.code + '): ' + file, true) 115 | } 116 | } 117 | } 118 | } 119 | 120 | // Now that we know everything in the sub-tree has been deleted, we can delete the main directory. 121 | // Huzzah for the shopkeep. 122 | 123 | try { 124 | result = fs.rmdirSync(dir) 125 | } catch (e) { 126 | log.error('could not remove directory (code ' + e.code + '): ' + dir, true) 127 | } 128 | } catch (e) {} 129 | 130 | return result 131 | } // rmdir 132 | 133 | /* 134 | * copy files 135 | * credits go to http://github.com/arturadib/shelljs 136 | */ 137 | function cp (srcFile, destFile) { 138 | if (!fs.existsSync(srcFile)) { 139 | log.error('cp: no such file or directory: ' + srcFile) 140 | } 141 | 142 | var BUF_LENGTH = 64 * 1024 143 | var buf = new Buffer(BUF_LENGTH) 144 | var bytesRead = BUF_LENGTH 145 | var pos = 0 146 | var fdr = null 147 | var fdw = null 148 | 149 | try { 150 | fdr = fs.openSync(srcFile, 'r') 151 | } catch (e) { 152 | log.error('cp: could not read src file (' + srcFile + ')') 153 | } 154 | 155 | try { 156 | fdw = fs.openSync(destFile, 'w') 157 | } catch (e) { 158 | log.error('cp: could not write to dest file (code=' + e.code + '):' + destFile) 159 | } 160 | 161 | while (bytesRead === BUF_LENGTH) { 162 | bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos) 163 | fs.writeSync(fdw, buf, 0, bytesRead) 164 | pos += bytesRead 165 | } 166 | 167 | fs.closeSync(fdr) 168 | fs.closeSync(fdw) 169 | 170 | fs.chmodSync(destFile, fs.statSync(srcFile).mode) 171 | } 172 | 173 | // --------------------------------------------------------------------- 174 | // find modules 175 | // --------------------------------------------------------------------- 176 | /* 177 | * detect and load npm 178 | */ 179 | function requireNpm () { 180 | // it is assumed that npm is always installed alongside with node 181 | var npm 182 | var npmBinPath 183 | var npmPath 184 | var binDir = path.dirname(process.execPath) 185 | var npmBin = path.join(binDir, 'npm') 186 | 187 | try { 188 | npm = require('npm') // maybe the NODE_PATH var is already set correctly 189 | return npm 190 | } catch (e) { 191 | if (fs.existsSync(npmBin) && fs.lstatSync(npmBin).isSymbolicLink()) { 192 | npmBinPath = path.resolve(binDir, fs.readlinkSync(npmBin)) 193 | npmPath = npmBinPath.replace(/^(.*\/node_modules\/npm)(?:(?!\/node_modules\/npm).)*?$/, '$1') 194 | npm = require(npmPath) // if the assumption is wrong, then an assertion is thrown here 195 | return npm 196 | } 197 | } 198 | } 199 | 200 | // --------------------------------------------------------------------- 201 | // program 202 | // --------------------------------------------------------------------- 203 | /* 204 | * a very very basic command line parser 205 | */ 206 | var cli = { 207 | opts: {}, 208 | _space: ' ', 209 | _store: { 210 | version: null, 211 | option: {}, 212 | help: [] 213 | }, 214 | _strip: function (str) { 215 | return str.replace(/-/g, '') 216 | }, 217 | version: function (str) { 218 | this._store.version = str 219 | this.option('-v', '--version', 'show version') 220 | return this 221 | }, 222 | help: function (str) { 223 | this._store.help.push(this._space + str) 224 | return this 225 | }, 226 | option: function (short, long, desc, arg) { 227 | var s = this._strip(long) 228 | var spc = ' '.substr(0, (13 - long.length)) 229 | 230 | this._store.option[s] = { desc: desc, arg: arg } 231 | if (short !== '') { 232 | this._store.option[this._strip(short)] = { long: s } 233 | this._store.help.push(this._space + short + ' , ' + long + spc + ' : ' + desc) 234 | } else { 235 | this._store.help.push(this._space + ' ' + long + spc + ' : ' + desc) 236 | } 237 | return this 238 | }, 239 | parse: function () { 240 | var i 241 | var r 242 | var s 243 | var argv = process.argv 244 | 245 | for (i = 2; i < argv.length; i += 1) { 246 | s = this._strip(argv[i]) 247 | r = this._store.option[s] 248 | // print out help 249 | if (s === 'h' || s === 'help') { 250 | console.log('\n' + this._store.help.join('\n') + '\n') 251 | this.exit = true 252 | return 253 | } 254 | // print out version 255 | if (s === 'v' || s === 'version') { 256 | console.log(this._store.version) 257 | this.exit = true 258 | return 259 | } 260 | if (r) { 261 | if (r.long) { 262 | s = r.long 263 | r = this._store.option[r.long] 264 | } 265 | if (r.arg !== undefined) { 266 | var arg = argv[i + 1] 267 | if (!/^-/.test(arg) && typeof arg === r.arg) { 268 | this.opts[s] = arg 269 | i += 1 270 | } else { 271 | this.opts[s] = true 272 | } 273 | } else { 274 | this.opts[s] = true 275 | } 276 | } 277 | } 278 | } 279 | } 280 | 281 | /* 282 | * a simple logger 283 | */ 284 | var log = { 285 | _debug: true, 286 | error: function (msg, _continue) { 287 | console.error('\n Error: ' + msg + '\n') 288 | if (!_continue) { 289 | console.trace() 290 | process.exit(1) 291 | } 292 | }, 293 | info: function (msg) { 294 | console.log(' ' + msg) 295 | }, 296 | debug: function () { 297 | if (this._debug) { 298 | var args = Array.prototype.slice.call(arguments) 299 | console.log.apply(this, args) 300 | } 301 | } 302 | } 303 | 304 | // --------------------------------------------------------------------- 305 | // npm, package.json, semver 306 | // --------------------------------------------------------------------- 307 | var FsJson = function (filename) { 308 | if (!(this instanceof FsJson)) { 309 | return new FsJson(filename) 310 | } 311 | this.filename = filename 312 | } 313 | FsJson.prototype = { 314 | read: function (cb) { 315 | var _this = this 316 | var filename = path.join(pwd(), _this.filename) 317 | 318 | fs.readFile(filename, 'utf8', function (err, data) { 319 | var obj 320 | if (err) { 321 | cb(err) 322 | return 323 | } 324 | try { 325 | obj = JSON.parse(data) 326 | } catch (e) { 327 | cb(err) 328 | return 329 | } 330 | cb(null, obj) 331 | }) 332 | }, 333 | write: function (data, cb) { 334 | var _this = this 335 | var filename = path.join(pwd(), _this.filename) 336 | 337 | fs.writeFile(filename, JSON.stringify(data, null, ' '), function (err) { 338 | if (cb) { cb(err) } 339 | }) 340 | } 341 | } 342 | 343 | /* 344 | * handle fs operations on package.json 345 | */ 346 | var packageJson = { 347 | _name: 'package.json', 348 | read: function (cb) { 349 | var _this = this 350 | FsJson(_this._name).read(function (err, data) { 351 | if (err) { 352 | log.error(_this._name + ' failed to read or parse: ' + err.message) 353 | return 354 | } else { 355 | cb(null, data) 356 | } 357 | }) 358 | }, 359 | write: function (data, cb) { 360 | var _this = this 361 | FsJson(_this._name).write(data, function (err) { 362 | if (err) { 363 | log.error(_this._name + ' failed to write: ' + err.message) 364 | } 365 | if (cb) cb(err) 366 | }) 367 | } 368 | } 369 | 370 | /** 371 | * handle the shrinkwrap file 372 | */ 373 | var shrinkwrap = { 374 | _name: 'npm-shrinkwrap.json', 375 | /** 376 | * read the file 377 | */ 378 | read: function (cb) { 379 | FsJson(this._name).read(cb) 380 | }, 381 | /** 382 | * write the file 383 | */ 384 | write: function (data, cb) { 385 | FsJson(this._name).write(data, cb) 386 | }, 387 | 388 | /** 389 | * backup npm-shrinkwrap.json to offline dir 390 | */ 391 | backup: function (prepublish) { 392 | var fileOrg = path.join(pwd(), this._name) 393 | var fileBak = path.join(pwd(), offline._dir, this._name) 394 | 395 | if (!fs.existsSync(fileOrg)) { 396 | if (prepublish && fs.existsSync(fileBak)) { 397 | fs.unlinkSync(fileBak) 398 | } 399 | return false 400 | } else if (!fs.existsSync(fileBak)) { 401 | cp(fileOrg, fileBak) 402 | return true 403 | } 404 | }, 405 | /** 406 | * if exists npm-shrinkwrap.json restore to main dir 407 | */ 408 | restore: function () { 409 | var fileGen = path.join(pwd(), this._name) 410 | var fileBak = path.join(pwd(), offline._dir, this._name) 411 | 412 | if (fs.existsSync(fileBak)) { 413 | cp(fileBak, fileGen) 414 | return true 415 | } else if (fs.existsSync(fileGen)) { 416 | fs.unlinkSync(fileGen) 417 | } 418 | }, 419 | /** 420 | * parse npm-shrinkwrap and change resolved property 421 | */ 422 | parse: function (obj) { 423 | var name 424 | if (obj) { 425 | for (name in obj) { 426 | if (obj[name].resolved) { 427 | obj[name].resolved = server.packageUrl(name, obj[name].version) 428 | } 429 | if (obj[name].dependencies) { 430 | obj[name].dependencies = shrinkwrap.parse(obj[name].dependencies) 431 | } 432 | } 433 | } 434 | return obj 435 | }, 436 | /** 437 | * change the shrinkwrap file 438 | */ 439 | change: function (cb) { 440 | var _this = this 441 | 442 | _this.read(function (err, obj) { 443 | if (!err && obj) { 444 | obj.dependencies = _this.parse(obj.dependencies) 445 | _this.write(obj, function () { 446 | if (cb) cb() 447 | }) 448 | } else { 449 | if (cb) cb() 450 | } 451 | }) 452 | } 453 | } 454 | 455 | /* 456 | * handle stuff related to npmrc 457 | */ 458 | var npmrc = function (npm, config) { 459 | var _this = {} 460 | _this._npmBackup = {} 461 | 462 | _this.backup = function () { 463 | for (var key in config) { 464 | _this._npmBackup[key] = npm.config.get(key) 465 | } 466 | } 467 | 468 | _this.restore = function () { 469 | for (var key in _this._npmBackup) { 470 | npm.config.set(key, _this._npmBackup[key]) 471 | } 472 | } 473 | 474 | _this.set = function () { 475 | for (var key in config) { 476 | npm.config.set(key, config[key]) 477 | } 478 | } 479 | 480 | _this.backup() 481 | 482 | return _this 483 | } 484 | 485 | /* 486 | * A semver parser to correctly sort for "latest" version 487 | * Follows spec on 488 | */ 489 | var semver = { 490 | int: function (n) { 491 | return parseInt(n, 10) 492 | }, 493 | preRel: function (p) { 494 | if (p) { 495 | p = p.split('.') 496 | return p.map(function (n) { 497 | if (/^\d+$/.test(n)) { 498 | n = semver.int(n) 499 | } 500 | return n 501 | }) 502 | } 503 | return 504 | }, 505 | version: function (v) { 506 | var o 507 | 508 | v.replace(/^(\d+)\.(\d+)\.(\d+)(?:\-([a-zA-Z0-9\.\-]*)?(?:\+(.*))?)?/, function (m, a0, a1, a2, pre) { 509 | o = { rel: [a0, a1, a2], pre: semver.preRel(pre) } 510 | o.rel = o.rel.map(semver.int) 511 | }) 512 | 513 | return o 514 | }, 515 | sortVersion: function (a, b) { 516 | for (var i = 0; i < a.length; i += 1) { 517 | if (a[i] !== b[i]) { 518 | return (b[i] - a[i]) 519 | } 520 | } 521 | return 0 522 | }, 523 | sortPreRel: function (a, b) { 524 | var min, r 525 | if (!a) { return -1 } 526 | if (!b) { return 1 } 527 | 528 | min = (a.length < b.length ? a.length : b.length) 529 | 530 | for (var i = 0; i < min; i += 1) { 531 | if (a[i] !== b[i]) { 532 | r = b[i] - a[i] 533 | if (isNaN(r)) { 534 | if (b[i] < a[i]) { return -1 } 535 | if (b[i] > a[i]) { return 1 } 536 | } else { 537 | return r 538 | } 539 | } 540 | } 541 | if (a.length > min) { return -1 } 542 | if (b.length > min) { return 1 } 543 | 544 | return 0 545 | }, 546 | sort: function (a, b) { 547 | var r 548 | var _a = semver.version(a) 549 | var _b = semver.version(b) 550 | 551 | if (!_a) { return 1 } 552 | if (!_b) { return -1 } 553 | 554 | r = semver.sortVersion(_a.rel, _b.rel) 555 | if (r !== 0) { return r } 556 | 557 | r = semver.sortPreRel(_a.pre, _b.pre) 558 | return r 559 | } 560 | } 561 | 562 | // --------------------------------------------------------------------- 563 | // npm registry server 564 | // --------------------------------------------------------------------- 565 | /* 566 | * serve the files from the npm cache as npm registry 567 | */ 568 | var server = { 569 | packageUrl: function (name, version) { 570 | return 'http://localhost:' + config.port + '/' + name + '/-/' + name + '-' + version + '.tgz' 571 | }, 572 | pack: function (cache, name, cb) { 573 | name = unescape(name) 574 | 575 | var _this = this 576 | var p = { name: name, versions: {} } 577 | var dir = cache + '/' + name 578 | 579 | fs.readdir(dir, function (err, versions) { 580 | var vv = [] 581 | var cnt = 0 582 | 583 | if (err) { 584 | return cb(err) 585 | } 586 | 587 | function done () { 588 | if (cnt === versions.length) { 589 | p['dist-tags'] = { 590 | latest: vv.sort(semver.sort)[0] 591 | } 592 | 593 | // ~ log.debug(JSON.stringify(p, null, ' ')); 594 | cb(null, p) 595 | } else { 596 | cnt += 1 597 | load(versions[cnt]) 598 | } 599 | } 600 | 601 | function load (version) { 602 | var pj = dir + '/' + version + '/package/package.json' 603 | fs.readFile(pj, {encoding: 'utf8'}, function (err, data) { 604 | var pck 605 | if (err) { 606 | return done() 607 | } 608 | vv.push(version) 609 | pck = JSON.parse(data) 610 | pck.from = pck._from || '.' 611 | pck.dist = {} 612 | if (pck._shasum) { 613 | pck.dist.shasum = pck._shasum 614 | } 615 | pck.dist.tarball = _this.packageUrl(name, version) 616 | delete (pck.readme) 617 | delete (pck._from) 618 | delete (pck._shasum) 619 | delete (pck._resolved) 620 | p.versions[version] = pck 621 | // ~ log.debug(' > ', name, version, pck.dist.tarball); 622 | done() 623 | }) 624 | } 625 | 626 | load(versions[cnt]) 627 | }) 628 | }, 629 | error404: function (req, res) { 630 | res.writeHead(404) 631 | res.end() 632 | log.debug('[' + (new Date()).toISOString() + ']', 404, req.url) 633 | }, 634 | files: function (options) { 635 | var _this = this 636 | var REGEX_TGZ = /^\/([^\/]+)\/-\/(?:(?!\d+\.\d+\.\d+).)*\-(\d+\.\d+\.\d+.*)\.tgz$/ 637 | 638 | options = options || {} 639 | options.path = options.path || '/' 640 | options.base = path.normalize(options.base || '/') 641 | 642 | return function (req, res) { 643 | var file, 644 | stream 645 | 646 | // check routing - options.path needs to be part of req.url 647 | if (req.url.indexOf(options.path) === 0) { 648 | file = options.base + '/' + req.url.substr(options.path.length, req.url.length) 649 | 650 | if (/^\/[^\/]+$/.test(req.url)) { 651 | file = req.url.replace(/\//, '') 652 | _this.pack(options.base, file, function (err, p) { 653 | if (err) { 654 | return _this.error404(req, res) 655 | } 656 | res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'}) 657 | res.end(JSON.stringify(p, null, ' ')) 658 | log.debug('[' + (new Date()).toISOString() + ']', 200, req.url, file) 659 | }) 660 | } else if (REGEX_TGZ.test(req.url)) { 661 | req.url.replace(REGEX_TGZ, function (m, name, version) { 662 | file = path.join(options.base, name, version, 'package.tgz') 663 | }) 664 | 665 | fs.stat(file, function (err, stat) { 666 | if (err || !stat.isFile()) { 667 | return _this.error404(req, res) 668 | } 669 | stream = fs.createReadStream(file) 670 | res.writeHead(200, { 'Content-Type': 'application/octet-stream' }) 671 | stream.on('data', function (chunk) { 672 | res.write(chunk) 673 | }) 674 | stream.on('end', function () { 675 | res.end() 676 | log.debug('[' + (new Date()).toISOString() + ']', 200, req.url, file) 677 | }) 678 | }) 679 | } else { 680 | return _this.error404(req, res) 681 | } 682 | } else { 683 | return _this.error404(req, res) 684 | } 685 | } 686 | }, 687 | start: function (cache, cb) { 688 | var _this = this 689 | 690 | // create and start-up the server with the chained middlewares 691 | var server = http.createServer(_this.files({ path: '/', base: cache })) 692 | 693 | server.on('error', function (err) { 694 | if (err.code === 'EADDRINUSE') { 695 | log.error('Address in use, trying to use the running one ...', true) 696 | } else { 697 | log.error('There is something bad: ' + err.code + ': ' + err.message) 698 | } 699 | }) 700 | 701 | server.listen(config.port, function () { 702 | log.info('Server running on port:' + config.port + ' using cache in ' + cache) 703 | cb && cb() 704 | }) 705 | } 706 | } 707 | 708 | /* 709 | * the offline script 710 | */ 711 | var offline = { 712 | _server: null, 713 | _cmds: ['prepublish', 'preinstall', 'postinstall'], 714 | _dir: 'offline', 715 | _pidfile: path.join(__dirname, 'offline.pid'), 716 | _script: './offline/offline-npm', 717 | _regex: /\s*\.\/offline\/offline-npm (?:(?!;|&&).)*(;|&&\s*|$)\s*/g, 718 | /** 719 | * check if script was called from global install or local 720 | */ 721 | _globalScript: function () { 722 | var pathToJs = __dirname.split(path.sep) 723 | if (pathToJs.length > 1 && (pathToJs[pathToJs.length - 1] === this._dir)) { 724 | return false 725 | } 726 | return true 727 | }, 728 | /** 729 | * call on publish 730 | */ 731 | prepublish: function () { 732 | npm.load(function (err, _npm) { 733 | var n 734 | if (!err) { 735 | n = npmrc(_npm, config.npm.prepublish) 736 | n.set() 737 | rmdir(path.join(__dirname, 'cache')) 738 | mkdir(path.join(__dirname, 'cache')) 739 | rmdir(path.join(__dirname, '..', 'node_modules')) 740 | 741 | shrinkwrap.restore() 742 | 743 | packageJson.read(function (err, data) { 744 | var i 745 | var packages = [] 746 | 747 | if (err) { 748 | throw err 749 | } 750 | 751 | if (data.dependencies) { 752 | for (i in data.dependencies) { 753 | packages.push(i) 754 | } 755 | _npm.commands.install(packages, function () { 756 | n.restore() 757 | 758 | shrinkwrap.backup(true) 759 | _npm.commands.shrinkwrap([], function (err, data) { 760 | if (err) { 761 | log.error('shrinkwrap error: ' + err.message) 762 | return 763 | } 764 | shrinkwrap.change(function (/* err */) { 765 | }) 766 | }) 767 | }) 768 | } 769 | }) 770 | } 771 | }) 772 | }, 773 | /** 774 | * to be called on install 775 | * Change the npm registry to localhost:port 776 | * Start a fake registry server 777 | */ 778 | preinstall: function () { 779 | // start the npm registry using the cache 780 | var child = spawn('./offline/offline-npm', ['-s'], { 781 | cwd: pwd(), 782 | detached: true, 783 | stdio: 'inherit' 784 | }) 785 | child.unref() 786 | setTimeout(function () { 787 | process.exit(0) 788 | }, 2000) 789 | }, 790 | /** 791 | * to be called on postinstall 792 | * deletes the offline folder 793 | */ 794 | postinstall: function () { 795 | var pid 796 | 797 | if (fs.existsSync(this._pidfile)) { 798 | pid = fs.readFileSync(this._pidfile, 'utf8') 799 | try { 800 | process.kill(pid) 801 | } catch (e) {} 802 | } 803 | if (!this._globalScript()) { 804 | rmdir(__dirname) 805 | } 806 | }, 807 | /** 808 | * add scripts to `package.json` 809 | */ 810 | add: function () { 811 | var _this = this 812 | 813 | packageJson.read(function (err, data) { 814 | if (err) { 815 | log.error('no package.json file found') 816 | return 817 | } 818 | if (!data.scripts) { data.scripts = {} } 819 | 820 | _this._cmds.forEach(function (s) { 821 | var sep = ' ; ' 822 | var tmp = data.scripts[s] 823 | 824 | if (tmp) { 825 | data.scripts[s] = _this._script + ' --' + s + sep + tmp.replace(_this._regex, '') 826 | } else { 827 | data.scripts[s] = _this._script + ' --' + s + sep 828 | } 829 | }) 830 | packageJson.write(data, function (err) { 831 | if (!err) { 832 | if (_this._globalScript()) { 833 | mkdir(path.join(pwd(), 'offline', 'cache')) 834 | cp(process.mainModule.filename, path.join(pwd(), 'offline', 'offline-npm')) 835 | } else { 836 | mkdir(path.join(__dirname, 'cache')) 837 | } 838 | shrinkwrap.backup() 839 | log.info('offline-npm was added to project: ' + data.name) 840 | } else { 841 | log.error('offline-npm COULD NOT be added to project: ' + data.name) 842 | } 843 | }) 844 | }) 845 | }, 846 | /** 847 | * remove offline scripts from `package.json` 848 | */ 849 | remove: function () { 850 | var _this = this 851 | 852 | packageJson.read(function (err, data) { 853 | var tmp 854 | 855 | if (err) { 856 | log.error('no package.json file found') 857 | return 858 | } 859 | 860 | shrinkwrap.restore() 861 | // delete the offline directory 862 | tmp = path.resolve(pwd(), _this._dir) 863 | if (fs.existsSync(tmp)) { 864 | rmdir(tmp) 865 | } 866 | 867 | if (!data.scripts) { data.scripts = {} } 868 | 869 | _this._cmds.forEach(function (s) { 870 | tmp = data.scripts[s] 871 | if (typeof tmp === 'string') { 872 | data.scripts[s] = tmp.replace(_this._regex, '') 873 | if (data.scripts[s] === '') { 874 | delete (data.scripts[s]) 875 | } 876 | } 877 | }) 878 | 879 | packageJson.write(data, function (err) { 880 | if (!err) { 881 | if (!_this._globalScript()) { 882 | rmdir(__dirname) 883 | } 884 | log.info('offline-npm was removed from project: ' + data.name) 885 | } else { 886 | log.error('offline-npm COULD NOT be removed from project: ' + data.name) 887 | } 888 | }) 889 | }) 890 | }, 891 | /** 892 | * starts the local npm registry server 893 | */ 894 | server: function () { 895 | var _this = this 896 | var dir = typeof cli.opts.server === 'string' ? path.resolve(__dirname, cli.opts.server) : path.resolve(__dirname, 'cache') 897 | try { 898 | if (fs.existsSync(dir) && 899 | fs.lstatSync(dir).isDirectory()) { 900 | server.start(dir) 901 | // write a pid file to kill the server on postinstall 902 | fs.writeFileSync(_this._pidfile, process.pid, 'utf8') 903 | } else { 904 | log.error(dir + ' is not a directory - server did not start') 905 | } 906 | } catch (e) { 907 | log.error(e) 908 | } 909 | }, 910 | /** 911 | * starts the local npm registry server using the npm cache 912 | */ 913 | npmcache: function () { 914 | npm.load(function (err, npm) { 915 | if (err) console.error('npm error', err) 916 | var cache = npm.config.get('cache') 917 | server.start(cache, function () { 918 | log.info('export NPM_CONFIG_REGISTRY="http://localhost:' + config.port + '"') 919 | }) 920 | }) 921 | } 922 | } 923 | 924 | // --------------------------------------------------------------------- 925 | if (module === require.main) { 926 | (function () { 927 | cli 928 | .option('', '--prepublish', 'call on prepublish') 929 | .option('', '--preinstall', 'call on preinstall') 930 | .option('', '--postinstall', 'call on postinstall') 931 | .option('-a', '--add', 'add offline-npm to project') 932 | .option('-r', '--remove', 'remove from project') 933 | .option('-n', '--npmcache', 'start npm registry using current npm cache') 934 | .option('-d', '--debug', 'show debug info') 935 | .option('-s', '--server', 'start npm registry server using [path]', 'string') 936 | .version(VERSION) 937 | .option('-h', '--help', 'print this help') 938 | .parse() 939 | 940 | // process switches 941 | log._debug = cli.opts.debug || false 942 | 943 | for (var i in cli.opts) { 944 | if (offline[i]) { 945 | offline[i]() 946 | return // only one option at a time allowed 947 | } 948 | } 949 | })() 950 | } 951 | 952 | module.exports = { 953 | cli: cli, 954 | log: log, 955 | semver: semver, 956 | server: server, 957 | offline: offline, 958 | npmrc: npmrc, 959 | requireNpm: requireNpm, 960 | packageJson: packageJson, 961 | shrinkwrap: shrinkwrap 962 | } 963 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "offline-npm", 3 | "version": "0.2.2", 4 | "description": "npm pack for offline installation", 5 | "bin": "offline-npm.js", 6 | "scripts": { 7 | "test": "mocha && test/test.sh", 8 | "lint": "jshint *.js */*.js", 9 | "readme": "markedpp --githubid README.md > tmp.md && mv tmp.md README.md" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/commenthol/offline-npm.git" 14 | }, 15 | "homepage": "https://github.com/commenthol/offline-npm", 16 | "bugs": { 17 | "url": "https://github.com/commenthol/offline-npm/issues" 18 | }, 19 | "keywords": [ 20 | "npm", 21 | "pack", 22 | "offline", 23 | "install" 24 | ], 25 | "author": "commenthol", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "lodash": "latest", 29 | "mocha": "latest" 30 | }, 31 | "eslintConfig": { 32 | "extends": "standard", 33 | "rules": { 34 | "one-var": 0 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/cli.mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it */ 4 | 5 | var 6 | assert = require('assert'), 7 | _ = require('lodash'), 8 | M = require('../offline-npm').cli 9 | 10 | describe('#cli', function () { 11 | it('show help', function () { 12 | var cli = _.merge({}, M) 13 | process.argv[2] = '--help' 14 | cli 15 | .help('this is a help text') 16 | .parse() 17 | 18 | assert.deepEqual(cli._store.help, [ ' this is a help text' ]) 19 | }) 20 | 21 | it('show version', function () { 22 | var cli = _.merge({}, M) 23 | process.argv[2] = '-v' 24 | cli 25 | .version('0.0.1-a') 26 | .help('this is a help text') 27 | .parse() 28 | 29 | assert.deepEqual(cli._store.version, '0.0.1-a') 30 | }) 31 | 32 | it('parse one option', function () { 33 | var cli = _.merge({}, M) 34 | process.argv[2] = '-t' 35 | cli 36 | .option('-t', '--test', 'this is a test') 37 | .parse() 38 | 39 | assert.deepEqual(cli.opts, { test: true }) 40 | assert.deepEqual(cli._store.help, [' -t , --test : this is a test']) 41 | }) 42 | 43 | it('parse one option in long format', function () { 44 | var cli = _.merge({}, M) 45 | process.argv[2] = '--test' 46 | cli 47 | .option('-t', '--test', 'this is a test') 48 | .parse() 49 | 50 | assert.deepEqual(cli.opts, { test: true }) 51 | }) 52 | 53 | it('parse one option with optional [path]', function () { 54 | var cli = _.merge({}, M) 55 | process.argv[2] = '-t' 56 | process.argv[3] = 'this/path' 57 | cli 58 | .option('-t', '--test', 'requires a [path]', 'string') 59 | .parse() 60 | assert.deepEqual(cli.opts, { test: 'this/path' }) 61 | }) 62 | 63 | it('parse one option without optional [path]', function () { 64 | var cli = _.merge({}, M) 65 | process.argv[2] = '-t' 66 | process.argv[3] = '--another-opt' 67 | cli 68 | .option('-t', '--test', 'requires a [path]', 'string') 69 | .parse() 70 | assert.deepEqual(cli.opts, { test: true }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/five/five-0.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/commenthol/offline-npm/9727dc2ea7f658a7fe22b118dec681a930134f7f/test/five/five-0.1.0.tgz -------------------------------------------------------------------------------- /test/five/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function five () { 4 | return 5 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/five/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "five", 4 | "version": "0.1.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R dot 2 | -------------------------------------------------------------------------------- /test/proxy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | ;(function () { 6 | // detect mocha 7 | if (typeof describe === 'function') { 8 | return 9 | } 10 | 11 | // dependencies 12 | var http = require('http'), 13 | url = require('url') 14 | 15 | // variables 16 | var host = '127.0.0.1', 17 | port = 3131, 18 | server 19 | 20 | // create the proxy server 21 | server = http.createServer(function (req, res) { 22 | var _url, 23 | proxyReq, 24 | options 25 | 26 | _url = url.parse(req.url) 27 | 28 | if (_url.port !== '4873') { 29 | res.writeHead(408) 30 | res.write('timeout', 'utf-8') 31 | res.end() 32 | return 33 | } 34 | 35 | // options required to set-up the proxy request 36 | options = { 37 | hostname: _url.hostname || 'localhost', 38 | port: _url.port || 80, 39 | method: req.method || 'GET', 40 | path: _url.path || '/', 41 | headers: req.headers || {} 42 | } 43 | 44 | // add x-forwarded-for header 45 | options.headers['x-forwarded-for'] = req.connection.remoteAddress 46 | 47 | // console.log([ Date(), 'req', req.url, options.headers ]); 48 | 49 | // create a proxy request which shall handle the proxy response 50 | proxyReq = http.request(options, function (proxyRes) { 51 | // console.log([ Date(), 'proxyRes', proxyRes.headers ]); 52 | res.writeHead(proxyRes.statusCode, proxyRes.headers) 53 | 54 | proxyRes.on('error', function (err) { 55 | console.error(err) 56 | }) 57 | // proxy the proxy response back 58 | proxyRes.on('data', function (chunk) { 59 | !res.finished && res.write(chunk, 'binary') 60 | }) 61 | // pass-on end event 62 | proxyRes.on('end', function () { 63 | !res.finished && res.end() 64 | }) 65 | }) 66 | 67 | proxyReq.setTimeout(1000) 68 | 69 | // exception handling for errors and timeouts 70 | proxyReq.on('error', function (err) { 71 | // console.log([ Date(), 'error', err.message ]); 72 | res.writeHead(500) 73 | res.write(err.message, 'utf-8') 74 | res.end() 75 | }) 76 | proxyReq.on('timeout', function () { 77 | console.log([ Date(), 'timeout' ]) 78 | res.writeHead(408) 79 | res.write('timeout', 'utf-8') 80 | res.end() 81 | }) 82 | 83 | // proxy the req 84 | req.on('data', function (chunk) { 85 | proxyReq.write(chunk, 'binary') 86 | }) 87 | // pass-on end event 88 | req.on('end', function () { 89 | proxyReq.end() 90 | }) 91 | }) 92 | 93 | // start the server 94 | server.listen(port, host) 95 | 96 | console.log('Proxy running at http://' + host + ':' + port + '/') 97 | })() 98 | -------------------------------------------------------------------------------- /test/scoped/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function () { 4 | return 'scoped' 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/scoped/my-scoped-0.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/commenthol/offline-npm/9727dc2ea7f658a7fe22b118dec681a930134f7f/test/scoped/my-scoped-0.1.0.tgz -------------------------------------------------------------------------------- /test/scoped/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@my/scoped", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /test/semver.mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it */ 4 | 5 | var 6 | assert = require('assert'), 7 | semver = require('../offline-npm').semver 8 | 9 | describe('#semver', function () { 10 | it('sorts an array of semver version strings in the right order', function () { 11 | var 12 | arr = [ 13 | '0.0.1-alpha1', 14 | '1.15.1', 15 | '1.16.2', 16 | '1.17.0', 17 | '1.18.2', 18 | '1.17.1', 19 | '1.20.1', 20 | '1.20.1-300', 21 | '1.20.1-299', 22 | '1.20.1-305', 23 | '1.20.1-10', 24 | '20.10', 25 | '1.20.1-100', 26 | '1.20.1-1', 27 | '1.20.1-20', 28 | '1.20.1-200', 29 | '1.20.1-2', 30 | '1.21.0', 31 | '0.0.1-a.a.a', 32 | '0.0.1-a.a', 33 | '1.21.3', 34 | '1.21.3-a1', 35 | '1.21.3-r1', 36 | '1.21.4', 37 | '1.21.4+20001', 38 | '1.21.4-12', 39 | '1.21.4-12+2345', 40 | '10.21.4-alpha.1', 41 | '10.21.4-alpha', 42 | '10.21.4-beta', 43 | '10.21.4', 44 | '10.21.5-rc.1' 45 | ], 46 | exp = [ 47 | '10.21.5-rc.1', 48 | '10.21.4', 49 | '10.21.4-beta', 50 | '10.21.4-alpha.1', 51 | '10.21.4-alpha', 52 | '1.21.4', 53 | '1.21.4+20001', 54 | '1.21.4-12', 55 | '1.21.4-12+2345', 56 | '1.21.3', 57 | '1.21.3-r1', 58 | '1.21.3-a1', 59 | '1.21.0', 60 | '1.20.1', 61 | '1.20.1-305', 62 | '1.20.1-300', 63 | '1.20.1-299', 64 | '1.20.1-200', 65 | '1.20.1-100', 66 | '1.20.1-20', 67 | '1.20.1-10', 68 | '1.20.1-2', 69 | '1.20.1-1', 70 | '1.18.2', 71 | '1.17.1', 72 | '1.17.0', 73 | '1.16.2', 74 | '1.15.1', 75 | '0.0.1-alpha1', 76 | '0.0.1-a.a.a', 77 | '0.0.1-a.a', 78 | '20.10' 79 | ] 80 | 81 | var res = arr.sort(semver.sort) 82 | // ~ console.log(res); 83 | assert.deepEqual(res, exp) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pwd=$(pwd) 4 | cwd=$(dirname $0) 5 | npmOpts="" 6 | #npmOpts="--verbose" 7 | 8 | cleanup () { 9 | cd $pwd/try 10 | test -f try-*.tgz && rm try-*.tgz 11 | test -f npm-shrinkwrap.json && rm npm-shrinkwrap.json 12 | test -d offline && rm -rf offline 13 | test -d node_modules && rm -rf node_modules 14 | cd $pwd/try/try-offline 15 | test -d node_modules && rm -rf node_modules 16 | } 17 | 18 | log () { 19 | printf "\n \033[36m%s\033[0m : \033[90m%s\033[0m\n\n" $1 $2 20 | } 21 | 22 | assert () { 23 | printf "\n \033[31mAssert: $@\033[0m\n\n" 24 | cleanup 25 | exit 1 26 | } 27 | 28 | ok () { 29 | printf "\n \033[32mOk: $1\033[0m\n\n" 30 | } 31 | 32 | describe () { 33 | log 'Test' $1 34 | 35 | # test packaging 36 | it_pack () { 37 | # cleanup & reset 38 | cleanup 39 | cd $pwd/try 40 | export HTTP_PROXY="" 41 | ../offline-npm.js -r 42 | 43 | # install packages 44 | npm $npmOpts install 45 | if [ ! -d node_modules ]; then 46 | assert "npm install failed" 47 | fi 48 | 49 | node index.js | grep DuckDuck 50 | if [ $? != 0 ]; then 51 | assert "index.js failed" 52 | fi 53 | ../offline-npm.js -a 54 | 55 | if [ "x$1" = "xshrink" ]; then 56 | npm $npmOpts shrink 57 | if [ ! -f npm-shrinkwrap.json ]; then 58 | assert "npm shrink failed" 59 | fi 60 | fi 61 | 62 | npm $npmOpts pack 63 | if [ ! -f try-0.0.0.tgz ]; then 64 | assert "npm pack failed" 65 | fi 66 | ../offline-npm.js -r 67 | 68 | ok 'Pack test passed: ' $1 69 | } 70 | 71 | # test offline install 72 | it_offline () { 73 | cd $pwd/try/try-offline 74 | # start proxy to simulate offline use 75 | node $pwd/test/proxy.js & 76 | proxyPid=$! 77 | echo $proxyPid 78 | export HTTP_PROXY="http://127.0.0.1:3131" 79 | test -d node_modules && rm -r node_modules 80 | test ! -d node_modules && mkdir node_modules 81 | npm install $npmOpts ../try-0.0.0.tgz 82 | if [ ! -d node_modules/try ]; then 83 | assert "offline npm install failed" 84 | fi 85 | node node_modules/try/index.js | grep 408 86 | if [ $? != 0 ]; then 87 | kill $proxyPid 88 | assert "offline index.js failed" 89 | fi 90 | kill $proxyPid 91 | 92 | ok 'Offline test passed' 93 | } 94 | 95 | it_pack $1 96 | it_offline 97 | } 98 | 99 | describe "normal" 100 | describe "shrink" 101 | cleanup 102 | -------------------------------------------------------------------------------- /try/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | try-offline/ 3 | try-*.tgz 4 | .* 5 | -------------------------------------------------------------------------------- /try/five-0.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/commenthol/offline-npm/9727dc2ea7f658a7fe22b118dec681a930134f7f/try/five-0.1.0.tgz -------------------------------------------------------------------------------- /try/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var request = require('request'), 4 | semver = require('semver'), 5 | five = require('five'), 6 | _m = require('mergee') 7 | 8 | console.log('semver:', semver.gt('0.0.1', '0.0.0')) 9 | 10 | console.log('five:', five()) 11 | 12 | console.log('mergee:', _m.pick({a: 1, b: 2}, 'a')) 13 | 14 | request('http://www.duckduckgo.com', function (err, res, body) { 15 | if (!err && res.statusCode === 200) { 16 | console.log('request:', body.replace(/^[^]*()[^]*$/m, '$1')) // Print the web page. 17 | } else { 18 | if (res && res.statusCode) { 19 | console.log('request:', res.statusCode) 20 | } 21 | if (err) { 22 | console.log(err) 23 | } 24 | } 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /try/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try", 3 | "version": "0.0.0", 4 | "description": "try offline-npm", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "mocha": "latest" 13 | }, 14 | "dependencies": { 15 | "five": "file:five-0.1.0.tgz", 16 | "mergee": "git://github.com/commenthol/mergee.git#master", 17 | "request": "latest", 18 | "semver": "*" 19 | } 20 | } -------------------------------------------------------------------------------- /try/try-offline/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | --------------------------------------------------------------------------------