├── .npmrc ├── index.js ├── bin └── ducttape.js ├── spec ├── support │ └── jasmine.json └── npm.spec.js ├── .gitignore ├── package.json ├── LICENSE ├── lib ├── ducttape.js ├── utils.js └── npm.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | sign-git-tag=true -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.apply = require('./lib/ducttape').apply; -------------------------------------------------------------------------------- /bin/ducttape.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var ducttape = require('../lib/ducttape'); 5 | var utils = require('../lib/utils'); 6 | 7 | var opts = utils.getOpts(); 8 | 9 | ducttape.apply(path.resolve(), opts); -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | .idea 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-ducttape", 3 | "version": "1.2.1", 4 | "description": "Utility for packaging your dependencies along with the code", 5 | "main": "index.js", 6 | "bin": { 7 | "npm-ducttape": "./bin/ducttape.js" 8 | }, 9 | "scripts": { 10 | "test": "jasmine" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/DeadAlready/npm-ducttape.git" 15 | }, 16 | "keywords": [ 17 | "shrinkwrap", 18 | "dependencies", 19 | "npm", 20 | "deploy" 21 | ], 22 | "author": "Karl Düüna", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/DeadAlready/npm-ducttape/issues" 26 | }, 27 | "preferGlobal": true, 28 | "homepage": "https://github.com/DeadAlready/npm-ducttape#readme", 29 | "dependencies": {}, 30 | "devDependencies": { 31 | "jasmine": "2.4.1", 32 | "rimraf": "2.5.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Karl Düüna 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 | -------------------------------------------------------------------------------- /lib/ducttape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.apply = apply; 4 | 5 | /*********************************/ 6 | 7 | var utils = require('./utils'); 8 | var NPM = require('./npm'); 9 | 10 | function apply(root, opts) { 11 | var npm = NPM.create(root, opts.target); 12 | 13 | //Get possible flags for shrinkwrap (--dev) 14 | if(opts.shrinkwrap.length) { 15 | console.log('flags provided for shrinkwrap', opts.shrinkwrap); 16 | } 17 | 18 | //Run shrinkwrap 19 | npm.shrinkwrap(opts.shrinkwrap); 20 | console.log('shrinkwrap successful'); 21 | 22 | //Require shrinkwrap results 23 | var shrinkwrapData = npm.getShrinkwrap(); 24 | if(!shrinkwrapData.dependencies || Object.keys(shrinkwrapData.dependencies).length < 1) { 25 | // No dependencies, nothing to do 26 | console.log('No dependencies'); 27 | return; 28 | } 29 | 30 | //Clear the folder 31 | utils.clearFolderSync(root, opts.target); 32 | console.log('folder cleared'); 33 | 34 | var result = npm.listDependencies(shrinkwrapData); 35 | console.log('A total of', result.list.length, 'dependencies found'); 36 | 37 | return npm.getFiles(result.list) 38 | .then(function () { 39 | console.log('files consolidated'); 40 | 41 | //Write new shrinkwrap file 42 | npm.setShrinkwrap(result.dependencies); 43 | console.log('new npm-shrinkwrap.json written'); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.clearFolderSync = clearFolderSync; 4 | module.exports.promisifyPipe = promisifyPipe; 5 | module.exports.getOpts = getOpts; 6 | module.exports.joinFlags = joinFlags; 7 | 8 | /**********************/ 9 | 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | 13 | function clearFolderSync (root, target) { 14 | var dirname = path.join(root, target); 15 | try { 16 | fs.readdirSync(dirname) 17 | .map(function (file) { 18 | return path.join(dirname, file); 19 | }) 20 | .forEach(fs.unlinkSync); 21 | } catch(error) { 22 | if(error.code === 'ENOENT') { 23 | fs.mkdirSync(dirname); 24 | } else { 25 | throw error; 26 | } 27 | } 28 | } 29 | 30 | function promisifyPipe(instream, outstream) { 31 | return new Promise(function (resolve, reject) { 32 | var sent = false; 33 | function innerReject(err) { 34 | if(!sent) { 35 | sent = true; 36 | reject(err); 37 | } 38 | } 39 | function innerResolve() { 40 | if(!sent) { 41 | sent = true; 42 | resolve(); 43 | } 44 | } 45 | 46 | instream.on('error', innerReject); 47 | outstream.on('error', innerReject); 48 | 49 | outstream.on('close', innerResolve); 50 | outstream.on('finish', innerResolve); 51 | 52 | instream.pipe(outstream); 53 | }); 54 | } 55 | 56 | function getOpts() { 57 | var argv = process.argv.slice(0).splice(2); 58 | var _default = { 59 | target: '.packages', 60 | shrinkwrap: [] 61 | }; 62 | 63 | argv.forEach(function (opt) { 64 | if(opt === '--dev') { 65 | _default.shrinkwrap.push(opt); 66 | } else if(_default.target === '.packages') { // only use the first 67 | _default.target = opt; 68 | } 69 | }); 70 | 71 | return _default; 72 | } 73 | 74 | function joinFlags(cmd, flags) { 75 | if(Array.isArray(flags) && flags.length) { 76 | cmd += ' ' + flags.join(' '); 77 | } 78 | return cmd; 79 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-ducttape 2 | 3 | A simple script to ducttape your dependencies into a subfolder of your package, so they can be installed without NPM registry on deploy. 4 | 5 | **NB! Due to changes in NPM install in version 3.10.8 then shrinkwrap generated with 3.10.8 will not work with earlier versions and vice versa.** 6 | 7 | ## Installation 8 | 9 | Prefer installing globally as installing locally will require you to also add it to package.json as a dependency - otherwise shrinkwrap **WILL** fail. 10 | 11 | npm install -g npm-ducttape 12 | 13 | ## Usage 14 | 15 | ### Global 16 | 17 | npm-ducttape 18 | 19 | Or if you want to specify a folder 20 | 21 | npm-ducttape my-fancy-folder 22 | 23 | ## Options 24 | 25 | ### --dev 26 | 27 | You can provide the *--dev* flag to *npm-ducttape*. This flag will be passed on to shrinkwrap to include devDependencies in the build. 28 | 29 | npm-ducttape --dev 30 | 31 | You can provide both a custom folder and the --dev flag 32 | 33 | npm-ducttape my-fancy-folder --dev 34 | 35 | 36 | ## TODO: 37 | 38 | 1. More tests 39 | 40 | ## License 41 | 42 | The MIT License (MIT) 43 | Copyright (c) 2016 Karl Düüna 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy of 46 | this software and associated documentation files (the "Software"), to deal in 47 | the Software without restriction, including without limitation the rights to 48 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 49 | the Software, and to permit persons to whom the Software is furnished to do so, 50 | subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in all 53 | copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 56 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 57 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 58 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 59 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 60 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 61 | SOFTWARE. -------------------------------------------------------------------------------- /spec/npm.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var childProcess = require('child_process'); 4 | var NPM = require('../lib/npm'); 5 | var path = require('path'); 6 | var rimraf = require('rimraf'); 7 | var fs = require('fs'); 8 | 9 | var root = path.join(__dirname, 'test'); 10 | var target = '.packages'; 11 | var swPath = path.join(root, 'npm-shrinkwrap.json'); 12 | 13 | 14 | describe('NPM', function() { 15 | beforeAll(function () { 16 | try { 17 | fs.mkdirSync(root); 18 | } catch(e) { 19 | if(e.code === 'EEXIST') { 20 | rimraf.sync(root); 21 | fs.mkdirSync(root); 22 | } 23 | } 24 | }); 25 | afterAll(function () { 26 | rimraf.sync(root); 27 | }); 28 | 29 | it('should throw an error if non-absolute root', function() { 30 | expect(function () { 31 | NPM.create('./what', target); 32 | }).toThrowError(); 33 | }); 34 | 35 | it('should throw an error if target is outside root', function() { 36 | expect(function () { 37 | NPM.create(root, '../what'); 38 | }).toThrowError(); 39 | }); 40 | 41 | it('should initialize if all ok', function() { 42 | expect(function () { 43 | NPM.create(root, target); 44 | }).not.toThrowError(); 45 | }); 46 | 47 | it('should have correct internal variables', function() { 48 | spyOn(childProcess, 'execSync').and.callThrough(); 49 | var npm = NPM.create(root, target); 50 | expect(npm._dir).toEqual(root); 51 | expect(npm._target).toEqual(target); 52 | expect(npm._fullTargetPath).toEqual(path.join(root, target)); 53 | expect(npm._shrinkwrapLocation).toEqual(swPath); 54 | expect(typeof npm._cache).toBe('string') 55 | 56 | expect(childProcess.execSync).toHaveBeenCalled(); 57 | expect(childProcess.execSync).toHaveBeenCalledWith('npm config get cache', {cwd: root}); 58 | }); 59 | 60 | it('should be able to write npm-shrinkwrap.json', function() { 61 | var npm = NPM.create(root, target); 62 | npm.setShrinkwrap({test: true}); 63 | expect(require(swPath)).toEqual({test:true}); 64 | }); 65 | 66 | it('should be able to get npm-shrinkwrap.json', function() { 67 | fs.writeFileSync(swPath, JSON.stringify({test2:true})); 68 | var npm = NPM.create(root, target); 69 | expect(npm.getShrinkwrap()).toEqual({test2:true}); 70 | }); 71 | 72 | it('should call npm shrinkwrap', function() { 73 | spyOn(childProcess, 'execSync').and.returnValue('wrote npm-shrinkwrap.json'); 74 | var npm = NPM.create(root, target); 75 | npm.shrinkwrap(); 76 | expect(childProcess.execSync).toHaveBeenCalled(); 77 | expect(childProcess.execSync).toHaveBeenCalledWith('npm shrinkwrap', {cwd: root}); 78 | }); 79 | 80 | 81 | it('should call npm shrinkwrap with flags', function() { 82 | spyOn(childProcess, 'execSync').and.returnValue('wrote npm-shrinkwrap.json'); 83 | var npm = NPM.create(root, target); 84 | npm.shrinkwrap(['--dev']); 85 | expect(childProcess.execSync).toHaveBeenCalled(); 86 | expect(childProcess.execSync).toHaveBeenCalledWith('npm shrinkwrap --dev', {cwd: root}); 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /lib/npm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.create = create; 4 | 5 | /**********************/ 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var childProcess = require('child_process'); 10 | 11 | var utils = require('./utils'); 12 | 13 | function NPM(dirname, folder) { 14 | 15 | if(!path.isAbsolute(dirname)) { 16 | throw new TypeError('root must be an absolute path'); 17 | } 18 | 19 | this._dir = dirname; 20 | this._target = folder; 21 | this._fullTargetPath = path.join(dirname, folder); 22 | this._cache = this.getCache(); 23 | this._shrinkwrapLocation = path.join(this._dir, 'npm-shrinkwrap.json'); 24 | this._isNewer = this._isNewerVersion(); 25 | 26 | if(this._fullTargetPath.indexOf(dirname) !== 0) { 27 | throw new Error('target folder must be a subfolder of the root'); 28 | } 29 | } 30 | 31 | NPM.prototype.run = function run(cmd) { 32 | var finalCmd = 'npm ' + cmd; 33 | console.log('running', finalCmd); 34 | 35 | return childProcess.execSync(finalCmd, {cwd: this._dir}).toString('utf8'); 36 | }; 37 | 38 | NPM.prototype.shrinkwrap = function shrinkwrap(flags) { 39 | try { 40 | var cmd = utils.joinFlags('shrinkwrap', flags); 41 | this.run(cmd); 42 | } catch(e) { 43 | console.error('Shrinkwrap command failed'); 44 | console.error('You most probably need to run npm prune and/or npm install first'); 45 | console.error('Refer to error above'); 46 | process.exit(1); 47 | } 48 | }; 49 | 50 | NPM.prototype.getShrinkwrap = function getShrinkwrap() { 51 | return JSON.parse(fs.readFileSync(this._shrinkwrapLocation, 'utf8')); 52 | }; 53 | 54 | NPM.prototype.setShrinkwrap = function setShrinkwrap(data) { 55 | fs.writeFileSync(this._shrinkwrapLocation, JSON.stringify(data, undefined, 2), 'utf8'); 56 | }; 57 | 58 | NPM.prototype.listDependencies = function listDependencies(obj, name, prefix) { 59 | var $this = this; 60 | name = name || ''; 61 | prefix = prefix || ''; 62 | 63 | var newObj = { 64 | dependencies: {} 65 | }; 66 | var list = []; 67 | Object.keys(obj).forEach(function (key) { 68 | if(key !== 'dependencies') { 69 | newObj[key] = obj[key]; 70 | return; 71 | } 72 | 73 | var newPrefix = !name || $this._isNewer ? '' : prefix ? ['..','..', prefix].join('/') : ['..','..'].join('/'); 74 | Object.keys(obj.dependencies).forEach(function (depKey) { 75 | var result = $this.listDependencies(obj.dependencies[depKey], depKey, newPrefix); 76 | list = list.concat(result.list); 77 | newObj.dependencies[depKey] = result.dependencies; 78 | }); 79 | }); 80 | 81 | if(!name) { 82 | // No name, so don't include itself 83 | return { 84 | dependencies: newObj, 85 | list: list 86 | }; 87 | } 88 | 89 | // npm pack will remove @ symbols from the name. 90 | var santitizedName = name.replace(/[@]/, ''); 91 | // slashes are converted into dashes by npm pack 92 | santitizedName = santitizedName.replace(/[/]/, '-'); 93 | 94 | var fileName = santitizedName + '-' + newObj.version + '.tgz'; 95 | var filePath = path.join($this._fullTargetPath, fileName); 96 | var pathParts = [$this._target, fileName]; 97 | if(prefix) { 98 | pathParts.unshift(prefix); 99 | } 100 | if(obj.resolved) { 101 | list.push({ 102 | packageName: name, 103 | versionName: newObj.version, 104 | filePath: filePath, 105 | url: newObj.resolved 106 | }); 107 | newObj.resolved = 'file:' + pathParts.join('/'); 108 | } 109 | 110 | return { 111 | dependencies: newObj, 112 | list: list 113 | }; 114 | }; 115 | 116 | NPM.prototype.getCache = function getCache() { 117 | return this.run('config get cache').replace(/\s/g, ''); 118 | }; 119 | 120 | NPM.prototype.getVersion = function getCache() { 121 | return this.run('-v').replace(/\s/g, ''); 122 | }; 123 | 124 | NPM.prototype._isNewerVersion = function _isNewerVersion() { 125 | var version = this.getVersion(); 126 | var semverNrs = version.split('.').map(function (nr) { 127 | return parseInt(nr, 10); 128 | }); 129 | if(semverNrs[0] < 3) { 130 | return false; 131 | } 132 | if(semverNrs[1] < 10) { 133 | return false; 134 | } 135 | return semverNrs[2] >= 8; 136 | }; 137 | 138 | NPM.prototype.pack = function pack(url) { 139 | var $this = this; 140 | return new Promise(function (resolve, reject) { 141 | childProcess.exec('npm pack ' + url, {cwd: $this._fullTargetPath}, function (err, stdout, stderr) { 142 | if(err || stderr) { 143 | reject(err || stderr); 144 | return; 145 | } 146 | resolve(stdout); 147 | }); 148 | }); 149 | }; 150 | 151 | NPM.prototype.getFileFromCache = function getFileFromCache(data) { 152 | var cachePath = path.join(this._cache, data.packageName, data.versionName, 'package.tgz'); 153 | return utils.promisifyPipe(fs.createReadStream(cachePath), fs.createWriteStream(data.filePath, 'utf8')); 154 | }; 155 | 156 | NPM.prototype.getFile = function getFile(data) { 157 | var $this = this; 158 | return $this.getFileFromCache(data) 159 | .catch(function () { 160 | return $this.pack(data.url); 161 | }); 162 | }; 163 | 164 | NPM.prototype.getFiles = function getFiles(datas) { 165 | var $this = this; 166 | return Promise.all(datas.map(function (data) { 167 | return $this.getFile(data); 168 | })); 169 | }; 170 | 171 | function create (dirname, target) { 172 | return new NPM(dirname, target); 173 | } 174 | --------------------------------------------------------------------------------