├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── bin └── copi.js ├── package.json └── src ├── cache-spec.js ├── cache.js ├── copi.js ├── db-spec.js ├── db.js ├── fake-registry.js ├── file-shasum.js ├── load-db.js └── start-registry.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .found-packages.json 3 | .package-filenames.json 4 | .packages.json 5 | npm-debug.log 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | script: 15 | - npm run lint 16 | - npm test 17 | after_success: 18 | - npm run semantic-release 19 | branches: 20 | except: 21 | - "/^v\\d+\\.\\d+\\.\\d+$/" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # copi 2 | > Installs NPM package by finding it and copying from another local folder. Completely offline ;) 3 | 4 | [![NPM][copi-icon] ][copi-url] 5 | 6 | [![Build status][copi-ci-image] ][copi-ci-url] 7 | [![semantic-release][semantic-image] ][semantic-url] 8 | 9 | ## Install 10 | 11 | npm install -g copi 12 | 13 | ## Use 14 | 15 | copi 16 | copi --save | --save-dev | -S | -D 17 | 18 | If an existing local copy of `` is not found, `copi` calls standard 19 | `npm install ` automatically. 20 | 21 | See `copi` in action below: installing a local project, then handling new project and 22 | installing it from the registry 23 | 24 | [![asciicast](https://asciinema.org/a/33013.png)](https://asciinema.org/a/33013) 25 | 26 | ## Limitation 27 | 28 | If the package to be installed is not found locally, `copi` starts the regular 29 | `npm install` command; there is nothing we can do offline if we don't have the data. 30 | 31 | Some packages run `prepublish` step which might fail in the already installed folder, 32 | when `copi` tries to pack them for install. These packages cannot be packed correctly 33 | and cannot be installed, sorry. 34 | 35 | ## What?! 36 | 37 | After surveying developers, I found that most have a single folder with bunch of projects, 38 | each using NPM packages. Thus we have the directory structure looking like this 39 | 40 | /dev 41 | /projectA 42 | package.json 43 | /node_modules 44 | /lodash 45 | /async 46 | /projectB 47 | package.json 48 | /node_modules 49 | /lazy-ass 50 | /check-more-types 51 | 52 | Imagine we start a new project "projectC", and it needs module "async". We can quickly 53 | install it using `npm install /dev/projectA/node_modules/async` **if we knew where it was!** 54 | 55 | `copi` finds all the packages already installed, and finds the latest version of the one 56 | needed (lazily). Thus the installation is offline. 57 | 58 | copi -S lodash 59 | found lodash@3.0.6 among 1 candidate(s) 60 | installing /dev/projectA/node_modules/lodash 61 | projectC@1.0.0 /dev/projectC 62 | └── lodash@3.0.6 63 | 64 | ## Details 65 | 66 | The found packages are stored in a temp file, which will be updated if it is older than N hours, 67 | ensuring newly installed packages are discovered eventually. 68 | 69 | The wildcard that searches for all installed packages looks at the working folder's parent, 70 | and then down two levels. Should discover most of the packages without spending more than a 71 | couple of seconds (if the cache of filenames is old or non-existent). 72 | 73 | To avoid going to NPM for nested dependencies, `copi` spins a simple read-only NPM 74 | registry server *while copi is running*. 75 | Thus `npm install` command goes back to `copi` for additional packages, 76 | making sure we find those locally. 77 | 78 | ### Debugging 79 | 80 | Run the `DEBUG=copi copi ` command, 81 | this package uses [debug](https://www.npmjs.com/package/debug) 82 | 83 | ### Small print 84 | 85 | Author: Gleb Bahmutov © 2016 86 | 87 | * [@bahmutov](https://twitter.com/bahmutov) 88 | * [glebbahmutov.com](http://glebbahmutov.com) 89 | * [blog](http://glebbahmutov.com/blog/) 90 | 91 | License: MIT - do anything with the code, but don't blame me if it does not work. 92 | 93 | Spread the word: tweet, star on github, etc. 94 | 95 | Support: if you find any problems with this module, email / tweet / 96 | [open issue](https://github.com/bahmutov/copi/issues) on Github 97 | 98 | ## MIT License 99 | 100 | Copyright (c) 2016 Gleb Bahmutov 101 | 102 | Permission is hereby granted, free of charge, to any person 103 | obtaining a copy of this software and associated documentation 104 | files (the "Software"), to deal in the Software without 105 | restriction, including without limitation the rights to use, 106 | copy, modify, merge, publish, distribute, sublicense, and/or sell 107 | copies of the Software, and to permit persons to whom the 108 | Software is furnished to do so, subject to the following 109 | conditions: 110 | 111 | The above copyright notice and this permission notice shall be 112 | included in all copies or substantial portions of the Software. 113 | 114 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 115 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 116 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 117 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 118 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 119 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 120 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 121 | OTHER DEALINGS IN THE SOFTWARE. 122 | 123 | [copi-icon]: https://nodei.co/npm/copi.png?downloads=true 124 | [copi-url]: https://npmjs.org/package/copi 125 | [copi-ci-image]: https://travis-ci.org/bahmutov/copi.png?branch=master 126 | [copi-ci-url]: https://travis-ci.org/bahmutov/copi 127 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 128 | [semantic-url]: https://github.com/semantic-release/semantic-release 129 | -------------------------------------------------------------------------------- /bin/copi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --harmony 2 | 3 | function isNotWord (word, str) { 4 | return word !== str 5 | } 6 | 7 | function isFlag (s) { 8 | return typeof s === 'string' && 9 | s[0] === '-' 10 | } 11 | 12 | function removeFlag (s) { 13 | return !isFlag(s) 14 | } 15 | 16 | const removeInstall = isNotWord.bind(null, 'install') 17 | const removeI = isNotWord.bind(null, 'i') 18 | const removeNode = (s) => { 19 | return s !== 'node' && !/bin\/node$/.test(s) 20 | } 21 | const removeCopi = (s) => { 22 | return !/bin\/copi$/.test(s) 23 | } 24 | 25 | const args = process.argv 26 | .filter(removeNode) 27 | .filter(removeCopi) 28 | .filter(removeInstall) 29 | .filter(removeI) 30 | .filter(removeFlag) 31 | 32 | const flags = process.argv 33 | .filter(isFlag) 34 | 35 | require('simple-bin-help')({ 36 | minArguments: 1, 37 | packagePath: __dirname + '/../package.json', 38 | help: 'use: copi ' 39 | }, args) 40 | 41 | const name = args[0] 42 | require(__dirname + '/..')({ 43 | name: name, 44 | flags: flags 45 | }) 46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copi", 3 | "description": "Installs NPM package by finding it and copying from another local folder. Completely offline ;)", 4 | "main": "src/copi.js", 5 | "version": "0.0.0-semantic-release", 6 | "bin": { 7 | "copi": "bin/copi.js" 8 | }, 9 | "preferGlobal": true, 10 | "private": false, 11 | "scripts": { 12 | "test": "node --harmony node_modules/.bin/rocha src/*-spec.js", 13 | "lint": "standard bin/*.js src/*.js", 14 | "commit": "commit-wizard", 15 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 16 | "issues": "git-issues", 17 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/bahmutov/copi.git" 22 | }, 23 | "keywords": [ 24 | "npm", 25 | "install", 26 | "offline", 27 | "cp", 28 | "copy", 29 | "local", 30 | "registry" 31 | ], 32 | "author": "Gleb Bahmutov ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/bahmutov/copi/issues" 36 | }, 37 | "homepage": "https://github.com/bahmutov/copi#readme", 38 | "dependencies": { 39 | "bluebird": "3.1.1", 40 | "check-more-types": "2.10.0", 41 | "debug": "2.2.0", 42 | "express": "5.0.0-alpha.2", 43 | "glob-promise": "1.0.4", 44 | "lazy-ass": "1.3.0", 45 | "morgan": "1.6.1", 46 | "npm-utils": "1.3.3", 47 | "semver": "5.1.0", 48 | "simple-bin-help": "1.5.1" 49 | }, 50 | "devDependencies": { 51 | "git-issues": "1.2.0", 52 | "pre-git": "3.1.2", 53 | "rocha": "1.6.1", 54 | "semantic-release": "6.0.3", 55 | "standard": "5.4.1" 56 | }, 57 | "files": [ 58 | "bin", 59 | "src/*.js", 60 | "!src/*-spec.js" 61 | ], 62 | "config": { 63 | "pre-git": { 64 | "commit-msg": [ 65 | "simple" 66 | ], 67 | "pre-commit": [ 68 | "npm run lint", 69 | "npm test" 70 | ], 71 | "pre-push": [], 72 | "post-commit": [], 73 | "post-merge": [] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cache-spec.js: -------------------------------------------------------------------------------- 1 | const is = require('check-more-types') 2 | const la = require('lazy-ass') 3 | 4 | /* global describe, it */ 5 | describe('cache in file', () => { 6 | const makeCache = require('./cache') 7 | 8 | it('is a function', () => { 9 | la(is.fn(makeCache)) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | const is = require('check-more-types') 2 | const la = require('lazy-ass') 3 | const fs = require('fs') 4 | const debug = require('debug')('copi') 5 | 6 | function saveData (filename, data) { 7 | la(is.unemptyString(filename), 'missing filename', filename) 8 | 9 | const withTimestamp = { 10 | data: data, 11 | timestamp: new Date() 12 | } 13 | fs.writeFileSync(filename, JSON.stringify(withTimestamp, null, 2), 'utf-8') 14 | debug('saved data to %s', filename) 15 | return data 16 | } 17 | 18 | function loadData (maxAge, filename) { 19 | la(is.unemptyString(filename), 'missing filename', filename) 20 | if (!fs.existsSync(filename)) { 21 | return null 22 | } 23 | const text = fs.readFileSync(filename, 'utf-8') 24 | const withTimestamp = JSON.parse(text) 25 | la(is.has(withTimestamp, 'timestamp'), 'missing timestamp', Object.keys(withTimestamp)) 26 | 27 | const now = new Date() 28 | const elapsed = now - new Date(withTimestamp.timestamp) 29 | debug('cache age', elapsed, 'ms, maxAge', maxAge) 30 | if (elapsed > maxAge) { 31 | return null 32 | } 33 | 34 | la(is.has(withTimestamp, 'data'), 'missing data', Object.keys(withTimestamp)) 35 | return withTimestamp.data 36 | } 37 | 38 | function makeCache (filename, maxAge) { 39 | return { 40 | save: saveData.bind(null, filename), 41 | load: loadData.bind(null, maxAge, filename) 42 | } 43 | } 44 | 45 | module.exports = makeCache 46 | -------------------------------------------------------------------------------- /src/copi.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('copi') 2 | const la = require('lazy-ass') 3 | const is = require('check-more-types') 4 | const npm = require('npm-utils') 5 | const loadDb = require('./load-db') 6 | const startRegistry = require('./start-registry') 7 | 8 | la(is.fn(npm.install), 'missing npm.install method') 9 | 10 | function install (options, db) { 11 | la(is.object(options), 'missing options', options) 12 | la(is.unemptyString(options.name), 'missing name', options) 13 | la(is.has(db, 'find'), 'missing find method in db') 14 | 15 | const found = db.find(options.name) 16 | 17 | if (!found || !found.latest) { 18 | console.error('Could not find locally installed "%s", using NPM to install', options.name) 19 | return npm.install({ 20 | name: options.name, 21 | flags: options.flags 22 | }) 23 | } 24 | console.log('found %s@%s among %d candidate(s)', 25 | options.name, found.latest, found.candidates.length) 26 | debug(found) 27 | 28 | la(is.unemptyString(found.folder), 'missing founder in found object', found) 29 | 30 | return startRegistry(db.find) 31 | .then(function (registry) { 32 | la(is.webUrl(registry.url), 'missing registry url', registry) 33 | la(is.object(registry.server), 'missing server itself', registry) 34 | 35 | return npm.install({ 36 | name: found.name, 37 | flags: options.flags, 38 | registry: registry.url 39 | }).then(function () { 40 | debug('finished install', found.name) 41 | registry.server.close() 42 | // if (saveFlag(options.flags)) { 43 | // setInstalledVersion(options.name, found.folder) 44 | // } 45 | }) 46 | }) 47 | } 48 | 49 | function copi (options) { 50 | console.log('installing %s', options.name) 51 | return loadDb(options.folder) 52 | .then(install.bind(null, options)) 53 | .catch(console.error.bind(console)) 54 | } 55 | 56 | module.exports = copi 57 | -------------------------------------------------------------------------------- /src/db-spec.js: -------------------------------------------------------------------------------- 1 | const is = require('check-more-types') 2 | const la = require('lazy-ass') 3 | 4 | /* global describe, it */ 5 | describe('building db of packages', () => { 6 | const build = require('./db') 7 | 8 | const barPackage = { 9 | name: 'bar', 10 | version: '1.0.0' 11 | } 12 | const bar2Package = { 13 | name: 'bar', 14 | version: '1.2.0' 15 | } 16 | const bazPackage = { 17 | name: 'baz', 18 | version: '2.0.0' 19 | } 20 | const filesystem = { 21 | 'some/path/bar/package.json': barPackage, 22 | 'or/this/bar/package.json': bar2Package, 23 | 'another/baz/package.json': bazPackage 24 | } 25 | const filenames = Object.keys(filesystem) 26 | 27 | const loadFile = (filename) => { 28 | return filesystem[filename] 29 | } 30 | 31 | const fileExists = () => true 32 | 33 | it('is a function', () => { 34 | la(is.fn(build)) 35 | }) 36 | 37 | it('builds db from filenames', () => { 38 | const db = build(filenames, loadFile, fileExists) 39 | la(is.object(db), 'returns an object', db) 40 | }) 41 | 42 | it('can search', () => { 43 | const db = build(filenames, loadFile, fileExists) 44 | la(is.fn(db.find), 'has find method') 45 | }) 46 | 47 | it('can find the latest "bar"', () => { 48 | const db = build(filenames, loadFile, fileExists) 49 | const found = db.find('bar') 50 | la(is.object(found), 'found an object', found) 51 | la(found.latest === bar2Package.version, 'wrong version', found) 52 | }) 53 | 54 | it('can find the latest "baz"', () => { 55 | const db = build(filenames, loadFile, fileExists) 56 | const found = db.find('baz') 57 | la(is.object(found), 'found an object', found) 58 | la(found.latest === bazPackage.version, 'wrong version', found) 59 | }) 60 | 61 | it('can search several times', () => { 62 | const db = build(filenames, loadFile, fileExists) 63 | const found1 = db.find('baz') 64 | la(found1.latest === bazPackage.version, 'wrong version', found1) 65 | const found2 = db.find('baz') 66 | la(found2.latest === bazPackage.version, 'wrong version', found2) 67 | }) 68 | 69 | it('has info about different versions', () => { 70 | const db = build(filenames, loadFile, fileExists) 71 | const bar = db.find('bar') 72 | la(is.object(bar)) 73 | la(is.semver(bar.latest)) 74 | la(bar.latest === bar2Package.version) 75 | la(is.object(bar.versions), 'has versions', bar) 76 | la(is.unemptyString(bar.versions[barPackage.version]), 77 | 'has first version', bar) 78 | la(is.unemptyString(bar.versions[bar2Package.version]), 79 | 'has second version', bar) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('copi') 2 | const is = require('check-more-types') 3 | const la = require('lazy-ass') 4 | const readSync = require('fs').readFileSync 5 | const exists = require('fs').existsSync 6 | const dirname = require('path').dirname 7 | const semver = require('semver') 8 | 9 | function readPackage (filename) { 10 | if (!exists(filename)) { 11 | return 12 | } 13 | return JSON.parse(readSync(filename, 'utf-8')) 14 | } 15 | 16 | function isPackageFile (str) { 17 | return /\/package\.json$/.test(str) 18 | } 19 | 20 | function updateCandidates (db, info) { 21 | la(is.object(db[info.name]), 'missing candidate', info) 22 | db[info.name].candidates.push(info.path) 23 | return db 24 | } 25 | 26 | function isLaterVersion (a, b) { 27 | la(is.semver(a), 'not semver', a) 28 | la(is.semver(b), 'not semver', b) 29 | return semver.gt(a, b) 30 | } 31 | 32 | function addCandidateInfo (info, path, version) { 33 | la(is.object(info.versions), 'cannot find versions in', info) 34 | la(is.unemptyString(path), 'expected path', path) 35 | la(is.semver(version), 'expected semver', version) 36 | 37 | const folder = dirname(path) 38 | 39 | if (!info.latest || isLaterVersion(version, info.latest)) { 40 | info.latest = version 41 | info.folder = folder 42 | } 43 | 44 | if (!info.versions[version]) { 45 | info.versions[version] = folder 46 | } 47 | 48 | return info 49 | } 50 | 51 | // lazy finding - returns all versions 52 | function find (db, loadFile, fileExists, name) { 53 | la(is.unemptyString(name), 'missing package to find', name) 54 | const info = db[name] 55 | if (!info) { 56 | // cannot find package with this name in DB 57 | return null 58 | } 59 | la(info.name === name, 'different name, searching for %s, found %s', 60 | name, info.name) 61 | 62 | if (info.latest && fileExists(info.folder)) { 63 | debug('found latest version for %s', name) 64 | return info 65 | } 66 | 67 | la(is.object(info.versions), 'expected object with versions', info) 68 | if (is.not.empty(info.versions)) { 69 | const valid = Object.keys(info.versions).every(function (version) { 70 | const folder = info.versions[version] 71 | return fileExists(folder) 72 | }) 73 | if (valid) { 74 | debug('every version folder still exists for', name) 75 | debug('returning prefilled info for candidate', name) 76 | return info 77 | } 78 | } 79 | 80 | la(is.array(info.candidates), 'info for', name, 'has not candidates', info) 81 | debug('filtering candidates for "%s" among %d candidates', 82 | name, info.candidates.length) 83 | 84 | info.candidates.forEach(function (candidatePath) { 85 | if (!fileExists(candidatePath)) { 86 | // maybe the folder has been moved or deleted 87 | return 88 | } 89 | const pkg = loadFile(candidatePath) 90 | if (!is.object(pkg)) { 91 | console.error('could not read %s', candidatePath) 92 | return 93 | } 94 | if (!is.semver(pkg.version)) { 95 | console.error('could not read semver from %s got', candidatePath, pkg.version) 96 | return 97 | } 98 | addCandidateInfo(info, candidatePath, pkg.version) 99 | }) 100 | return info 101 | } 102 | 103 | function buildPackageDatabase (filenames, loadFile, fileExists) { 104 | loadFile = loadFile || readPackage 105 | la(is.fn(loadFile), 'invalid load file', loadFile) 106 | 107 | fileExists = fileExists || exists 108 | la(is.fn(fileExists), 'invalid file exists', fileExists) 109 | 110 | const db = is.object(filenames) ? filenames : {} 111 | db.find = find.bind(null, db, loadFile, fileExists) 112 | if (is.object(filenames)) { 113 | // done, we are just restoring an object 114 | return db 115 | } 116 | 117 | la(is.array(filenames), 'expected list of package filenames', filenames) 118 | const infos = filenames.map(function (filename) { 119 | la(isPackageFile(filename), 'not a package filename', filename) 120 | const parts = filename.split('/') 121 | return { 122 | name: parts[parts.length - 2], 123 | path: filename 124 | } 125 | }) 126 | // console.log(infos) 127 | 128 | infos.forEach(function (info) { 129 | if (!db[info.name]) { 130 | db[info.name] = { 131 | name: info.name, 132 | candidates: [], 133 | latest: undefined, 134 | folder: undefined, 135 | versions: {} 136 | } 137 | } 138 | updateCandidates(db, info) 139 | }) 140 | 141 | return db 142 | } 143 | 144 | module.exports = buildPackageDatabase 145 | -------------------------------------------------------------------------------- /src/fake-registry.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('copi') 2 | const la = require('lazy-ass') 3 | const is = require('check-more-types') 4 | const npm = require('npm-utils') 5 | const fs = require('fs') 6 | const fileShasum = require('./file-shasum') 7 | const Promise = require('bluebird') 8 | 9 | // this is read-only registry inspired by 10 | // https://github.com/nolanlawson/local-npm/blob/master/index.js 11 | 12 | function makeRegistry (find, options) { 13 | la(is.fn(find), 'missing find package function') 14 | 15 | const express = require('express') 16 | const morgan = require('morgan') 17 | const app = express() 18 | 19 | function getTarball (req, res) { 20 | console.log('tarball for %s@%s', req.params.name, req.params.version) 21 | 22 | const found = find(req.params.name) 23 | if (!found) { 24 | return res.status(404).send({}) 25 | } 26 | 27 | // console.log('found package') 28 | // console.log(found) 29 | 30 | debug('found package "%s" latest version %s, looking for version %s', 31 | found.name, found.latest, req.params.version) 32 | if (!found.name || 33 | !found.latest || 34 | !found.folder) { 35 | console.error('invalid found info', found) 36 | return res.status(404).send({}) 37 | } 38 | 39 | la(is.object(found.versions) && 40 | is.not.empty(found.versions), 'missing verions', found) 41 | if (!found.versions[req.params.version]) { 42 | console.error('Cannot find version %s@%s among %s', 43 | req.params.version, found.name, Object.keys(found.versions).join(',')) 44 | return res.status(404).send({}) 45 | } 46 | 47 | const folder = found.versions[req.params.version] 48 | la(is.unemptyString(folder), 'expected folder for version', 49 | req.params.version, 'in', found.versions) 50 | 51 | debug('building archive for %s from %s', found.name, folder) 52 | npm.pack({ folder: folder }) 53 | .then(function (tarballFilename) { 54 | debug('built tar archive for %s@%s %s', 55 | found.name, req.params.version, tarballFilename) 56 | if (!tarballFilename) { 57 | console.error('cannot find tar', tarballFilename, 'from', folder) 58 | return res.status(500).send({}) 59 | } 60 | 61 | const length = fs.statSync(tarballFilename).size 62 | res.set('content-type', 'application/octet-stream') 63 | res.set('content-length', length) 64 | const fileStream = fs.createReadStream(tarballFilename) 65 | fileStream.pipe(res) 66 | fileStream.on('end', function () { 67 | debug('sent tarball %s for %s@%s', tarballFilename, 68 | found.name, found.latest) 69 | fs.unlinkSync(tarballFilename) 70 | }) 71 | fileStream.on('error', console.error.bind(console)) 72 | }) 73 | } 74 | 75 | function shaForPacked (folder) { 76 | return npm.pack({ folder: folder }) 77 | .then(function (tarballFilename) { 78 | debug('built tar archive %s from folder %s', tarballFilename, folder) 79 | if (!tarballFilename) { 80 | throw new Error('Cannot tar folder ' + folder) 81 | } 82 | return fileShasum(tarballFilename) 83 | .then(function (shasum) { 84 | // delete tar for now 85 | fs.unlinkSync(tarballFilename) 86 | return shasum 87 | }) 88 | }) 89 | } 90 | 91 | // see example metadata using command 92 | // http http://registry.npmjs.org/foo 93 | // it is like package.json with additional 94 | // object "versions" 95 | // with each key a valid version 96 | // and "dist" object. The url format can be different 97 | /* 98 | "dist": { 99 | "shasum": "943e0ec03df00ebeb6273a5b94b916ba54b47581", 100 | "tarball": "http://registry.npmjs.org/foo/-/foo-1.0.0.tgz" 101 | } 102 | */ 103 | function getPackageMetadata (req, res) { 104 | debug('metadata for package', req.params.name) 105 | const found = find(req.params.name) 106 | if (!found) { 107 | return res.status(404).send({}) 108 | } 109 | debug('found package %s latest %s in %s', 110 | found.name, found.latest, found.folder) 111 | if (!found.name || 112 | !found.latest || 113 | !found.folder) { 114 | console.error('invalid found info', found) 115 | return res.status(404).send({}) 116 | } 117 | la(is.unemptyString(options.url), 'missing server url in', options) 118 | 119 | const pkg = npm.getPackage(found.folder) 120 | // add information about available versions 121 | pkg['dist-tags'] = { 122 | latest: found.latest 123 | } 124 | pkg.versions = {} 125 | 126 | la(is.object(found.versions) && is.not.empty(found.versions), 127 | 'cannot find versions in', found) 128 | const versions = Object.keys(found.versions) 129 | 130 | function versionInfo (version) { 131 | const folder = found.versions[version] 132 | return shaForPacked(folder) 133 | .then(function (shasum) { 134 | const tarUrl = options.url + '/tarballs/' + found.name + '/' + version + '.tgz' 135 | return { 136 | version: version, 137 | shasum: shasum, 138 | tarball: tarUrl 139 | } 140 | }) 141 | } 142 | 143 | Promise.map(versions, versionInfo, { concurrency: 10 }) 144 | .then(distInfos => { 145 | distInfos.forEach(dist => { 146 | la(is.object(dist) && is.unemptyString(dist.version), 147 | 'invalid dist object', dist) 148 | pkg.versions[dist.version] = { 149 | dist: dist 150 | } 151 | }) 152 | }) 153 | .then(() => res.json(pkg)) 154 | } 155 | 156 | app.use(morgan('dev')) 157 | // the only registry API needed 158 | app.get('/:name', getPackageMetadata) 159 | app.get('/tarballs/:name/:version.tgz', getTarball) 160 | 161 | return app 162 | } 163 | 164 | module.exports = makeRegistry 165 | -------------------------------------------------------------------------------- /src/file-shasum.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const crypto = require('crypto') 3 | const la = require('lazy-ass') 4 | const is = require('check-more-types') 5 | 6 | function computeShasum (filename) { 7 | la(is.unemptyString(filename), 'expected filename') 8 | return new Promise(function (resolve) { 9 | const fd = fs.createReadStream(filename) 10 | const hash = crypto.createHash('sha1') 11 | hash.setEncoding('hex') 12 | 13 | fd.on('end', () => { 14 | hash.end() 15 | resolve(hash.read()) 16 | }) 17 | fd.pipe(hash) 18 | }) 19 | } 20 | 21 | module.exports = computeShasum 22 | 23 | if (!module.parent) { 24 | computeShasum(__filename) 25 | .then(function (shasum) { 26 | console.log('%s - shasum %s', __filename, shasum) 27 | }) 28 | .catch(console.error.bind(console)) 29 | } 30 | -------------------------------------------------------------------------------- /src/load-db.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob-promise') 2 | const la = require('lazy-ass') 3 | const is = require('check-more-types') 4 | const makeCache = require('./cache') 5 | const db = require('./db') 6 | const npm = require('npm-utils') 7 | 8 | la(is.fn(npm.install), 'missing npm.install method') 9 | 10 | function findPackageFiles (folder) { 11 | return glob(folder + '/*/node_modules/*/package.json') 12 | } 13 | 14 | function seconds (n) { 15 | return n * 1000 16 | } 17 | 18 | const cacheFolder = __dirname + '/../' 19 | const maxAge = seconds(600) 20 | const filenamesCache = makeCache(cacheFolder + '.package-filenames.json', maxAge) 21 | const packagesCache = makeCache(cacheFolder + '.packages.json', maxAge) 22 | 23 | function loadDb (rootFolder) { 24 | rootFolder = rootFolder || process.cwd() + '/../' 25 | la(is.unemptyString(rootFolder), 'expected root folder', rootFolder) 26 | 27 | return Promise.resolve(packagesCache.load()) 28 | .then(function (dbData) { 29 | if (!dbData) { 30 | return Promise.resolve(filenamesCache.load()) 31 | .then(function (filenames) { 32 | if (!filenames) { 33 | return findPackageFiles(rootFolder) 34 | .then(function (filenames) { 35 | la(is.array(filenames), 'could not package filenames', filenames) 36 | console.log('found %d package filenames', filenames.length) 37 | return filenames 38 | }) 39 | } 40 | console.log('using cached %d package filenames', filenames.length) 41 | return filenames 42 | }) 43 | .then(filenamesCache.save) 44 | .then(db) 45 | .then(packagesCache.save) 46 | } 47 | return db(dbData) 48 | }) 49 | } 50 | 51 | module.exports = loadDb 52 | -------------------------------------------------------------------------------- /src/start-registry.js: -------------------------------------------------------------------------------- 1 | const is = require('check-more-types') 2 | const la = require('lazy-ass') 3 | 4 | function start (find) { 5 | la(is.fn(find), 'expected find function') 6 | 7 | const options = {} 8 | const makeRegistry = require('./fake-registry') 9 | const app = makeRegistry(find, options) 10 | 11 | return new Promise(function (resolve) { 12 | const server = app.listen(function () { 13 | const port = server.address().port 14 | const url = 'http://localhost:' + port 15 | console.log('Started registry app at %s', url) 16 | options.url = url 17 | options.server = server 18 | resolve(options) 19 | }) 20 | }) 21 | } 22 | 23 | module.exports = start 24 | 25 | if (!module.parent) { 26 | !(function tryRegistry () { 27 | const loadDb = require('./load-db') 28 | loadDb() 29 | .then(function (db) { 30 | start(db.find) 31 | .then(function (info) { 32 | console.log('running registry server at', info.url) 33 | /* 34 | const npm = require('npm-utils') 35 | npm.install({ 36 | name: 'ms@0.6.2', 37 | registry: info.url 38 | // flags: ['--verbose'] 39 | })*/ 40 | }) 41 | .catch(console.error.bind(console)) 42 | }) 43 | }()) 44 | } 45 | --------------------------------------------------------------------------------