├── .gitignore ├── test ├── npmPack-1.0.0.tgz ├── apple │ └── package.json ├── empty │ └── package.json ├── @namespace │ └── thing │ │ └── package.json ├── named │ ├── namedProject1 │ │ └── package.json │ └── package.json ├── almond │ └── package.json ├── circular-a │ └── package.json ├── deep │ ├── package.json │ ├── deep-b │ │ └── package.json │ ├── deep-c │ │ └── package.json │ ├── deep-d │ │ └── package.json │ └── deep-a │ │ └── package.json ├── circular-b │ └── package.json ├── circular-c │ └── package.json ├── salad │ └── package.json ├── banana │ └── package.json ├── bowl │ └── package.json ├── named.js ├── scoped.js ├── deep.js ├── index.js ├── nested.js └── circular.js ├── .travis.yml ├── LICENSE ├── package.json ├── HISTORY.md ├── bin └── linklocal.js ├── Readme.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /test/npmPack-1.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoxley/linklocal/HEAD/test/npmPack-1.0.0.tgz -------------------------------------------------------------------------------- /test/apple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apple", 3 | "version": "1.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /test/empty/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empty", 3 | "version": "1.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - "9" 6 | - "8" 7 | - "6" 8 | - "4" 9 | -------------------------------------------------------------------------------- /test/@namespace/thing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@namespace/thing", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /test/named/namedProject1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "namedProject1", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /test/almond/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuts/almond", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "apple": "file:../apple" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/circular-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circular-a", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "circular-b": "file:../circular-b" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/deep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "deep-a": "file:./deep-a" 7 | } 8 | } 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/circular-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circular-b", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "circular-c": "file:../circular-c" 7 | } 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/circular-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circular-c", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "circular-a": "file:../circular-a" 7 | } 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/deep/deep-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-b", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "deep-d": "file:../deep-d" 7 | } 8 | } 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/deep/deep-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-c", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "deep-d": "file:../deep-d" 7 | } 8 | } 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/deep/deep-d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-d", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "deep-a": "file:../deep-a" 7 | } 8 | } 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/deep/deep-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-a", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "deep-e": "file:../deep-b", 7 | "deep-c": "file:../deep-c" 8 | } 9 | } 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/named/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "named", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "namedProject1": "https://somelocation/to/namedProject1", 7 | "banana": "file:../banana", 8 | "localPacked": "file:../npmPack-1.0.0.tgz" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/salad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salad", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Tim Oxley ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bowl": "file:../bowl" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/banana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "banana", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Tim Oxley ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "apple": "file:../apple" 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /test/bowl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bowl", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Tim Oxley ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@nuts/almond": "file:../almond", 13 | "apple": "file:../apple", 14 | "banana": "file:../banana", 15 | "localPacked": "file:../npmPack-1.0.0.tgz" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/named.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | 5 | var path = require('path') 6 | 7 | var linklocal = require('../') 8 | var namedPackagePath = 'test/named' 9 | 10 | var PKG_DIR = path.resolve(__dirname, 'named') 11 | 12 | test('will link a named package', function (t) { 13 | var options = { 14 | recursive: false, 15 | cwd: path.resolve(process.cwd(), namedPackagePath), 16 | packages: ['namedProj1'] 17 | } 18 | 19 | linklocal.named(PKG_DIR, function (err, linked) { 20 | t.ifError(err) 21 | t.ok(linked) 22 | t.end() 23 | }, options) 24 | }) 25 | 26 | test('will unlink a named package', function (t) { 27 | var options = { 28 | recursive: false, 29 | cwd: path.resolve(process.cwd(), namedPackagePath), 30 | packages: ['namedProj1'] 31 | } 32 | 33 | linklocal.unlink.named(PKG_DIR, function testLinked (err, linked) { 34 | t.ifError(err) 35 | linklocal.unlink(PKG_DIR, function testUnlinked (_, unlinked) { 36 | t.ok(unlinked) 37 | t.end() 38 | }) 39 | }, options) 40 | }) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tim Oxley 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/scoped.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | 5 | var path = require('path') 6 | 7 | var linklocal = require('../') 8 | var scopedPackagePath = '@namespace/thing' 9 | 10 | var PKG_DIR = path.resolve(__dirname, scopedPackagePath) 11 | 12 | test('will link a scoped package', function (t) { 13 | var options = { 14 | recursive: false, 15 | cwd: path.resolve(process.cwd(), scopedPackagePath), 16 | scopeRename: 'thing', 17 | packages: ['@namespace/thing', 'thing'] 18 | } 19 | 20 | linklocal.named(PKG_DIR, function (err, linked) { 21 | t.ifError(err) 22 | t.ok(linked) 23 | t.end() 24 | }, options) 25 | }) 26 | 27 | test('will unlink a scoped package', function (t) { 28 | var options = { 29 | recursive: false, 30 | cwd: path.resolve(process.cwd(), scopedPackagePath), 31 | scopeRename: 'thing', 32 | packages: ['@namespace/thing', 'thing'] 33 | } 34 | 35 | linklocal.unlink.named(PKG_DIR, function testLinked (err, linked) { 36 | t.ifError(err) 37 | linklocal.unlink(PKG_DIR, function testUnlinked (err, unlinked) { 38 | t.ifError(err) 39 | t.ok(unlinked) 40 | t.end() 41 | }) 42 | }, options) 43 | }) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linklocal", 3 | "version": "2.8.2", 4 | "description": "Install local dependencies as symlinks.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "nyc -s tape test/*.js | tap-spec && nyc report --reporter=text --reporter=html && standard" 8 | }, 9 | "engines": { 10 | "npm": ">=2.0.0" 11 | }, 12 | "author": "Tim Oxley ", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "npm": "5.0.4", 16 | "nyc": "^11.5.0", 17 | "standard": "^11.0.0", 18 | "tap-spec": "^4.1.1", 19 | "tape": "^4.9.0" 20 | }, 21 | "dependencies": { 22 | "commander": "^2.15.0", 23 | "debug": "^3.1.0", 24 | "map-limit": "0.0.1", 25 | "mkdirp": "^0.5.1", 26 | "rimraf": "^2.6.2" 27 | }, 28 | "bin": { 29 | "linklocal": "bin/linklocal.js" 30 | }, 31 | "directories": { 32 | "test": "test" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/timoxley/linklocal.git" 37 | }, 38 | "keywords": [ 39 | "npm", 40 | "packages", 41 | "symlink", 42 | "dependencies", 43 | "modules" 44 | ], 45 | "bugs": { 46 | "url": "https://github.com/timoxley/linklocal/issues" 47 | }, 48 | "homepage": "https://github.com/timoxley/linklocal", 49 | "nyc": { 50 | "cache": true, 51 | "exclude": [ 52 | "test/*.js", 53 | "node_modules" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/deep.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | 5 | var path = require('path') 6 | var rimraf = require('rimraf') 7 | 8 | var linklocal = require('../') 9 | 10 | var deep = path.resolve(__dirname, 'deep') 11 | var deepA = path.resolve(deep, 'deep-a') 12 | var deepB = path.resolve(deep, 'deep-b') 13 | var deepC = path.resolve(deep, 'deep-c') 14 | var deepD = path.resolve(deep, 'deep-d') 15 | 16 | var expectedLinks = [ 17 | path.resolve(deepA, 'node_modules/deep-b'), 18 | path.resolve(deepA, 'node_modules/deep-c'), 19 | path.resolve(deepB, 'node_modules/deep-d'), 20 | path.resolve(deepC, 'node_modules/deep-d'), 21 | path.resolve(deepD, 'node_modules/deep-a'), 22 | path.resolve(deep, 'node_modules/deep-a') 23 | ] 24 | 25 | function setup () { 26 | rimraf.sync(path.resolve(deep, 'node_modules')) 27 | rimraf.sync(path.resolve(deep, 'package-lock.json')) 28 | rimraf.sync(path.resolve(deepA, 'node_modules')) 29 | rimraf.sync(path.resolve(deepA, 'package-lock.json')) 30 | rimraf.sync(path.resolve(deepB, 'node_modules')) 31 | rimraf.sync(path.resolve(deepB, 'package-lock.json')) 32 | rimraf.sync(path.resolve(deepC, 'node_modules')) 33 | rimraf.sync(path.resolve(deepC, 'package-lock.json')) 34 | rimraf.sync(path.resolve(deepD, 'node_modules')) 35 | rimraf.sync(path.resolve(deepD, 'package-lock.json')) 36 | } 37 | 38 | test('can link deep', function (t) { 39 | setup() 40 | linklocal.recursive(deep, function (err, linked) { 41 | t.ifError(err) 42 | t.ok(linked) 43 | t.deepEqual(linked.map(getFrom).sort(), expectedLinks.sort()) 44 | t.end() 45 | }) 46 | }) 47 | 48 | test('can unlink nested', function (t) { 49 | setup() 50 | linklocal.recursive(deep, function (err, linked) { 51 | t.ifError(err) 52 | t.ok(linked) 53 | linklocal.unlink.recursive(deep, function (err, linked) { 54 | t.ifError(err) 55 | t.ok(linked) 56 | t.deepEqual(linked.map(getFrom).sort(), expectedLinks.sort()) 57 | t.end() 58 | }) 59 | }) 60 | }) 61 | 62 | function getFrom (item) { 63 | return item.from 64 | } 65 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var exec = require('child_process').exec 8 | var rimraf = require('rimraf') 9 | 10 | var linklocal = require('../') 11 | 12 | var PKG_DIR = path.resolve(__dirname, 'bowl') 13 | var NODE_MODULES = path.resolve(__dirname, 'bowl', 'node_modules') 14 | 15 | var LINKS = Object.freeze([ 16 | path.join(NODE_MODULES, 'apple'), 17 | path.join(NODE_MODULES, 'banana'), 18 | path.join(NODE_MODULES, '@nuts', 'almond') 19 | ].sort()) 20 | 21 | function setup () { 22 | rimraf.sync(NODE_MODULES) 23 | rimraf.sync(path.join(PKG_DIR, 'package-lock.json')) 24 | } 25 | 26 | test('test is running on npm > 2.0.0', function (t) { 27 | exec('npm -v', {cwd: PKG_DIR}, function (_, version) { 28 | t.ok(version[0] >= '2') 29 | t.end() 30 | }) 31 | }) 32 | 33 | test('can swap local packages for links', function (t) { 34 | setup() 35 | exec('npm install', {cwd: PKG_DIR}, function (err) { 36 | t.ifError(err) 37 | linklocal(PKG_DIR, function (err, linked) { 38 | t.ifError(err) 39 | t.deepEqual(linked.map(getLink).sort(), LINKS) 40 | LINKS.forEach(function (link) { 41 | var exists = fs.existsSync(link) 42 | t.ok(exists, 'exists') 43 | if (!exists) return 44 | var stat = fs.lstatSync(link) 45 | t.ok(stat.isSymbolicLink(), 'is symbolic link') 46 | }) 47 | t.end() 48 | }) 49 | }) 50 | }) 51 | 52 | test('link works without npm install', function (t) { 53 | setup() 54 | linklocal(PKG_DIR, function (err, linked) { 55 | t.ifError(err) 56 | t.deepEqual(linked.map(getLink).sort(), LINKS) 57 | LINKS.forEach(function (link) { 58 | var exists = fs.existsSync(link) 59 | t.ok(exists, 'exists') 60 | if (!exists) return 61 | var stat = fs.lstatSync(link) 62 | t.ok(stat.isSymbolicLink(), 'is symbolic link') 63 | }) 64 | t.end() 65 | }) 66 | }) 67 | 68 | test('can unlink local packages', function (t) { 69 | setup() 70 | linklocal(PKG_DIR, function testLinked (err, linked) { 71 | t.ifError(err) 72 | t.deepEqual(linked.map(getLink).sort(), LINKS) 73 | linklocal.unlink(PKG_DIR, function testUnlinked (err, unlinked) { 74 | t.ifError(err) 75 | t.deepEqual(unlinked.map(getLink).sort(), LINKS) 76 | LINKS.forEach(function eachLink (link) { 77 | t.notOk(fs.existsSync(link)) 78 | }) 79 | t.end() 80 | }) 81 | }) 82 | }) 83 | 84 | test('unlink ignores if package not linked', function (t) { 85 | setup() 86 | linklocal(PKG_DIR, function (err, linked) { 87 | t.ifError(err) 88 | var links = linked.map(getLink).sort() 89 | t.deepEqual(links, LINKS) 90 | fs.unlink(links[0], function (err) { 91 | t.ifError(err) 92 | linklocal.unlink(PKG_DIR, function (err, linked) { 93 | t.ifError(err) 94 | var expected = LINKS.slice(1) 95 | t.deepEqual(linked.map(getLink).sort(), expected) 96 | expected.forEach(function (link) { 97 | t.notOk(fs.existsSync(link)) 98 | }) 99 | t.end() 100 | }) 101 | }) 102 | }) 103 | }) 104 | 105 | test('can handle zero dependencies', function (t) { 106 | setup() 107 | var PKG_DIR = path.resolve(__dirname, 'empty') 108 | linklocal(PKG_DIR, function (err, linked) { 109 | t.ifError(err) 110 | t.ok(linked) 111 | t.deepEqual(linked, []) 112 | linklocal.unlink(PKG_DIR, function (err, unlinked) { 113 | t.ifError(err) 114 | t.deepEqual(linked, []) 115 | t.end() 116 | }) 117 | }) 118 | }) 119 | 120 | function getLink (item) { 121 | return item.from 122 | } 123 | -------------------------------------------------------------------------------- /test/nested.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var rimraf = require('rimraf') 8 | 9 | var linklocal = require('../') 10 | 11 | var salad = path.resolve(__dirname, 'salad') 12 | var saladModulesPath = path.resolve(salad, 'node_modules') 13 | var bowl = path.resolve(__dirname, 'bowl') 14 | var bowlModulesPath = path.resolve(bowl, 'node_modules') 15 | var banana = path.resolve(__dirname, 'banana') 16 | var apple = path.resolve(__dirname, 'apple') 17 | var almond = path.resolve(__dirname, 'almond') 18 | 19 | var bowlModules = [ 20 | path.resolve(saladModulesPath, 'bowl'), 21 | path.resolve(bowlModulesPath, '@nuts', 'almond'), 22 | path.resolve(bowlModulesPath, 'apple'), 23 | path.resolve(bowlModulesPath, 'banana'), 24 | path.resolve(banana, 'node_modules', 'apple'), 25 | path.resolve(almond, 'node_modules', 'apple') 26 | ] 27 | 28 | function setup () { 29 | rimraf.sync(path.resolve(salad, 'node_modules')) 30 | rimraf.sync(path.resolve(salad, 'package-lock.json')) 31 | rimraf.sync(path.resolve(bowl, 'node_modules')) 32 | rimraf.sync(path.resolve(bowl, 'package-lock.json')) 33 | rimraf.sync(path.resolve(apple, 'node_modules')) 34 | rimraf.sync(path.resolve(apple, 'package-lock.json')) 35 | rimraf.sync(path.resolve(banana, 'node_modules')) 36 | rimraf.sync(path.resolve(banana, 'package-lock.json')) 37 | rimraf.sync(path.resolve(almond, 'node_modules')) 38 | rimraf.sync(path.resolve(almond, 'package-lock.json')) 39 | } 40 | 41 | test('can link nested', function (t) { 42 | setup() 43 | var PKG_DIR = path.resolve(__dirname, 'salad') 44 | linklocal.recursive(PKG_DIR, function (err, linked) { 45 | t.ifError(err) 46 | t.ok(linked) 47 | 48 | var expectedLinks = bowlModules 49 | 50 | t.deepEqual(linked.map(getLink).sort(), expectedLinks.sort()) 51 | 52 | var stat = fs.lstatSync(path.resolve(saladModulesPath, 'bowl')) 53 | t.ok(stat.isSymbolicLink(), 'bowl is symbolic link') 54 | t.ok(fs.existsSync(bowl), 'bowl exists') 55 | 56 | bowlModules.forEach(function (bowlModule) { 57 | var stat = fs.lstatSync(bowlModule) 58 | t.ok(stat.isSymbolicLink(), 'is symbolic link') 59 | t.ok(fs.existsSync(bowlModule), 'exists') 60 | }) 61 | t.end() 62 | }) 63 | }) 64 | 65 | test('can unlink nested', function (t) { 66 | setup() 67 | var PKG_DIR = path.resolve(__dirname, 'salad') 68 | 69 | var expectedLinks = bowlModules 70 | 71 | linklocal.link.recursive(PKG_DIR, function (err, linked) { 72 | t.ifError(err) 73 | t.deepEqual(linked.map(getLink).sort(), expectedLinks.sort()) 74 | linklocal.unlink.recursive(PKG_DIR, function (err, unlinked) { 75 | t.ifError(err) 76 | t.ok(unlinked) 77 | t.deepEqual(unlinked.map(getLink).sort(), expectedLinks.sort()) 78 | t.notOk(fs.existsSync(path.join(saladModulesPath, bowl)), 'bowl does not exist') 79 | 80 | bowlModules.forEach(function (bowlModule) { 81 | t.notOk(fs.existsSync(bowlModule), 'exists') 82 | }) 83 | t.end() 84 | }) 85 | }) 86 | }) 87 | 88 | test('can link no dependencies', function (t) { 89 | setup() 90 | var PKG_DIR = path.resolve(__dirname, 'empty') 91 | linklocal.recursive(PKG_DIR, function (err, linked) { 92 | t.ifError(err) 93 | t.ok(linked) 94 | t.deepEqual(linked, []) 95 | t.end() 96 | }) 97 | }) 98 | 99 | test('unlink can handle zero dependencies', function (t) { 100 | setup() 101 | var PKG_DIR = path.resolve(__dirname, 'empty') 102 | 103 | linklocal.unlink.recursive(PKG_DIR, function (err, unlinked) { 104 | t.ifError(err) 105 | t.ok(unlinked) 106 | t.deepEqual(unlinked, []) 107 | t.end() 108 | }) 109 | }) 110 | 111 | test('unlink can handle not linked', function (t) { 112 | setup() 113 | var PKG_DIR = path.resolve(__dirname, 'salad') 114 | 115 | linklocal.unlink.recursive(PKG_DIR, function (err, unlinked) { 116 | t.ifError(err) 117 | t.ok(unlinked) 118 | t.deepEqual(unlinked, []) 119 | t.end() 120 | }) 121 | }) 122 | 123 | function getLink (item) { 124 | return item.from 125 | } 126 | -------------------------------------------------------------------------------- /test/circular.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | 5 | var path = require('path') 6 | var exec = require('child_process').exec 7 | var rimraf = require('rimraf') 8 | 9 | var linklocal = require('../') 10 | 11 | var PKG_A = path.resolve(__dirname, 'circular-a') 12 | var PKG_B = path.resolve(__dirname, 'circular-b') 13 | var PKG_C = path.resolve(__dirname, 'circular-c') 14 | 15 | var A_TO_B = path.join(PKG_A, 'node_modules', 'circular-b') 16 | var B_TO_C = path.join(PKG_B, 'node_modules', 'circular-c') 17 | var C_TO_A = path.join(PKG_C, 'node_modules', 'circular-a') 18 | 19 | var LINKS = Object.freeze([ 20 | A_TO_B, 21 | B_TO_C, 22 | C_TO_A 23 | ].sort()) 24 | 25 | function setup () { 26 | rimraf.sync(path.resolve(PKG_A, 'node_modules')) 27 | rimraf.sync(path.resolve(PKG_A, 'package-lock.json')) 28 | rimraf.sync(path.resolve(PKG_B, 'node_modules')) 29 | rimraf.sync(path.resolve(PKG_B, 'package-lock.json')) 30 | rimraf.sync(path.resolve(PKG_C, 'node_modules')) 31 | rimraf.sync(path.resolve(PKG_C, 'package-lock.json')) 32 | } 33 | 34 | test('link circular dependencies recursive', function (t) { 35 | setup() 36 | linklocal.link.recursive(PKG_A, function (err, linked) { 37 | t.ifError(err) 38 | var expected = LINKS.slice() 39 | t.deepEqual(linked.map(getLink), expected) 40 | t.end() 41 | }) 42 | }) 43 | 44 | test('link circular dependencies recursive after install', function (t) { 45 | setup() 46 | exec('npm install --silent --cache-min=Infinity', {cwd: PKG_A}, function (err) { 47 | t.ifError(err) 48 | linklocal.link.recursive(PKG_A, function (err, linked) { 49 | t.ifError(err) 50 | var expected = LINKS.slice() 51 | t.deepEqual(linked.map(getLink), expected) 52 | t.end() 53 | }) 54 | }) 55 | }) 56 | 57 | test('link circular dependencies non-recursive', function (t) { 58 | setup() 59 | linklocal.link(PKG_A, function (err, linked) { 60 | t.ifError(err) 61 | t.deepEqual(linked.map(getLink), [A_TO_B]) 62 | t.end() 63 | }) 64 | }) 65 | 66 | test('unlink circular dependencies not linked', function (t) { 67 | setup() 68 | linklocal.unlink.recursive(PKG_A, function (err, linked) { 69 | t.ifError(err) 70 | t.deepEqual(linked, []) 71 | t.end() 72 | }) 73 | }) 74 | 75 | test('unlink circular dependencies installed, not linked', function (t) { 76 | setup() 77 | exec('npm --version', function (ignoreErr, obj) { 78 | var npmVersion = Number(obj.trim().split('.')[0]) 79 | exec('npm install', {cwd: PKG_A}, function (ignoreErr) { 80 | linklocal.unlink.recursive(PKG_A, function (err, linked) { 81 | t.ifError(err) 82 | if (npmVersion < 5) { 83 | t.deepEqual(linked, []) 84 | } else { 85 | t.equal(linked.length, 3) 86 | } 87 | t.end() 88 | }) 89 | }) 90 | }) 91 | }) 92 | 93 | test('unlink circular dependencies recursive', function (t) { 94 | setup() 95 | linklocal.link.recursive(PKG_A, function (err, linked) { 96 | t.ifError(err) 97 | var expected = LINKS.slice() 98 | t.deepEqual(linked.map(getLink), expected) 99 | 100 | linklocal.unlink.recursive(PKG_A, function (err, linked) { 101 | t.ifError(err) 102 | var expected = LINKS.slice() 103 | t.deepEqual(linked.map(getLink), expected) 104 | t.end() 105 | }) 106 | }) 107 | }) 108 | 109 | test('unlink circular dependencies recursive after install', function (t) { 110 | setup() 111 | exec('npm install', {cwd: PKG_A}, function (err) { 112 | t.ifError(err) 113 | linklocal.link.recursive(PKG_A, function (err, linked) { 114 | t.ifError(err) 115 | linklocal.unlink.recursive(PKG_A, function (err, linked) { 116 | t.ifError(err) 117 | var expected = LINKS.slice() 118 | t.deepEqual(linked.map(getLink), expected) 119 | t.end() 120 | }) 121 | }) 122 | }) 123 | }) 124 | 125 | test('unlink circular dependencies non-recursive', function (t) { 126 | setup() 127 | linklocal.link.recursive(PKG_A, function (err, linked) { 128 | t.ifError(err) 129 | linklocal.unlink(PKG_A, function (err, linked) { 130 | t.ifError(err) 131 | t.deepEqual(linked.map(getLink), [A_TO_B]) 132 | t.end() 133 | }) 134 | }) 135 | }) 136 | 137 | function getLink (item) { 138 | return item.from 139 | } 140 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2 | 2.8.2 / 2018-03-13 3 | ================== 4 | 5 | * Prevent busting exec buffer + speed up npm install step in test. 6 | * Update dependencies. 7 | * Fix tests for npm@^5. Hardcode npm@5.0.4 dep as npm >5 is broken. 8 | * Linting. 9 | * Readme.md grammar fix by @deltaskelta 10 | * Add callback to fs.unlink by @BridgeAR 11 | * Drop CI support for node 0.10 & 0.12, add support for node 9. 12 | 13 | 2.8.1 / 2017-07-04 14 | ================== 15 | 16 | * Update dependencies & linting. 17 | * Remove `npm cache clear` from travis CI setup. 18 | * Update Readme.md via pull request #32 from tomazy 19 | * Update Readme.md via pull request #29 from briandipalma 20 | 21 | 2.8.0 / 2017-01-04 22 | ================== 23 | 24 | * Add Contributors to Readme 25 | * Merge pull request #26 from bencripps/master 26 | * Adding @scope functionality for --named function 27 | 28 | 2.7.0 / 2016-12-09 29 | ================== 30 | 31 | * Add yarn.lock. 32 | * Use caret instead of tilde for dependencies. 33 | * Update devDependencies. 34 | * Merge pull request #27 from jalex/master 35 | * Fix #22 - Always use Junctions on Windows 36 | 37 | 2.6.1 / 2016-09-14 38 | ================== 39 | 40 | * Update and tweak --help text. 41 | * Use standard style. 42 | * Add code coverage report. 43 | * Update dependencies. 44 | 45 | 2.6.0 / 2016-02-23 46 | ================== 47 | 48 | * Merge pull request #25 from bencripps/master 49 | * Added linklocal.name, .nameRecursive, name.unlink, and name.unLinkRecursive. Updated readme. Added tests for new functionality. 50 | * See if intermittent build failure still occurs on dist: trusty. 51 | * Clear npm cache before running travis tests. 52 | * Merge pull request #21 from mikaelbr/ignoreLocalPacks 53 | * Fix issue with having local npm packed modules 54 | * Update dependencies. 55 | * Merge pull request #19 from timoxley/docs-shell 56 | * Make example portable 57 | * Attempt to fix build on travis. npm bug prevails. 58 | * Update travis node targets. 59 | 60 | 2.5.2 / 2015-05-14 61 | ================== 62 | 63 | * Update dependencies. 64 | * Merge pull request #17 from crcn/patch-1 65 | * fix typo 66 | * Add node.ico badges. 67 | 68 | 2.5.1 / 2015-03-10 69 | ================== 70 | 71 | * deps: pin commander to 2.6.x - Yoshua Wuyts. 72 | * Update Readme.md 73 | * Add references to local deps docs in npm. 74 | 75 | 2.5.0 / 2015-01-28 76 | ================== 77 | 78 | * Add --no-summary option. 79 | * Sort options alphabetically. 80 | * Update tape to 3.4.0, make tests pretty with tap-spec. 81 | 82 | 2.4.3 – 2.4.4 / 2015-01-02 83 | ========================== 84 | 85 | * Update commander from 2.5.0 to 2.6.0. 86 | * Indicate source file for JSON parse errors. 87 | 88 | 2.4.2 / 2014-12-11 89 | ================== 90 | 91 | * Update dependencies. 92 | * Absolute junctions for winxp - Vincent Weevers. 93 | * Add travis CI badge. 94 | 95 | 2.4.1 / 2014-11-24 96 | ================== 97 | 98 | * Fix issue with duplicated deep links. 99 | 100 | 2.4.0 / 2014-11-20 101 | ================== 102 | 103 | * Massive refactoring. 104 | * Add list command 105 | * Add new formatting aliases. 106 | * Fix bulk example - Yoshua Wuyts. 107 | 108 | 2.3.0 - 2.3.1 / 2014-11-12 109 | ========================== 110 | 111 | * Update docs. 112 | * Add relative/absolute format tokens. 113 | * Fix linklocal unlink destination paths. 114 | 115 | 2.2.0 - 2.2.1 / 2014-11-12 116 | ========================== 117 | 118 | * Improve recursive linking. 119 | * Add --format. 120 | 121 | 2.1.0 - 2.1.3 / 2014-11-10 122 | ========================== 123 | 124 | * Deal with long paths from circular dependencies during unlink. 125 | * Ensure we've found a symbolic link before attempting removal. 126 | * Resolve minimal linkage before unlinking. 127 | * Handle circular dependencies. 128 | * Normalize link output to minimum linkage. 129 | 130 | 2.0.1 / 2014-09-22 131 | ================== 132 | 133 | * bin: update console message 134 | 135 | 2.0.0 / 2014-09-22 136 | ================== 137 | 138 | * Link production and development dependencies. 139 | 140 | 1.1.2 / 2014-09-22 141 | ================== 142 | 143 | * Bugfix. 144 | 145 | 1.1.1 / 2014-09-22 146 | ================== 147 | 148 | * Update documentation to include recursion. 149 | 150 | 1.1.0 / 2014-09-22 151 | ================== 152 | 153 | * Add recursive linking/unlinking. 154 | 155 | 1.0.2 / 2014-09-21 156 | ================== 157 | 158 | * Add total links/unlinks to output. 159 | 160 | 1.0.1 / 2014-09-21 161 | ================== 162 | 163 | * Fix broken executable. #1 164 | 165 | 1.0.0 / 2014-09-20 166 | ================== 167 | 168 | * Initial Release. 169 | -------------------------------------------------------------------------------- /bin/linklocal.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var linklocal = require('../') 5 | var program = require('commander') 6 | var pkg = require('../package.json') 7 | 8 | program 9 | .usage('[options] ') 10 | .option('-f, --format [format]', 'output format', String, '%h') 11 | .option('-l, --link', 'Link local dependencies [default]') 12 | .option('-r, --recursive', 'Link recursively') 13 | .option('-q, --unique', 'Only unique lines of output') 14 | .option('-u, --unlink', 'Unlink local dependencies') 15 | .option('-n, --named', 'Link only named packages, last argument is cwd') 16 | .option('--absolute', 'Format output paths as absolute paths') 17 | .option('--files', 'Output only symlink targets (--format="%h") [default]') 18 | .option('--links', 'Output only symlinks (--format="%s")') 19 | .option('--list', 'Only list local dependencies. Does not link.') 20 | .option('--long', 'Output the symlink to hardlink mapping (--format="%s -> %h")') 21 | .option('--no-summary', 'Exclude summary i.e. "Listed 22 dependencies"') 22 | .version(pkg.version) 23 | 24 | program.on('--help', function () { 25 | console.info(' Examples:') 26 | console.info('') 27 | console.info(' linklocal # link local deps in current dir') 28 | console.info(' linklocal link # link local deps in current dir') 29 | console.info(' linklocal -r # link local deps recursively') 30 | console.info(' linklocal unlink # unlink only in current dir') 31 | console.info(' linklocal unlink -r # unlink recursively') 32 | console.info('') 33 | console.info(' linklocal list # list all local deps, ignores link status') 34 | console.info(' linklocal list -r # list all local deps recursively, ignoring link status') 35 | console.info('') 36 | console.info(' linklocal -- mydir # link local deps in mydir') 37 | console.info(' linklocal unlink -- mydir # unlink local deps in mydir') 38 | console.info(' linklocal --named pkgname ../to/pkg # link local dep by name/path') 39 | console.info(' linklocal --named pkgname1 pkgname2 ../to/pkg # link local deps by name/path') 40 | console.info(' linklocal unlink --named pkgname ../to/pkg # unlink local dep by name/') 41 | console.info(' linklocal --named -r pkgname ../to/pkg # link local deps recursively by name/') 42 | console.info(' linklocal --named -r @scope/pkgname pkgname ../to/pkg # link local deps recursively by name/ with npm @scope') 43 | console.info('') 44 | console.info(' Formats:') 45 | console.info('') 46 | console.info(' %s: relative path to symlink') 47 | console.info(' %S: absolute path to symlink') 48 | console.info(' %h: relative real path to symlink target') 49 | console.info(' %H: absolute real path to symlink target') 50 | console.info('') 51 | console.info(' relative paths are relative to cwd') 52 | console.info('') 53 | }) 54 | program.parse(process.argv) 55 | 56 | var command = program.unlink ? 'unlink' : 'link' 57 | 58 | if (program.list) command = 'list' 59 | 60 | program.args[0] = program.args[0] || '' 61 | 62 | var named = !!program.named 63 | var dir = path.resolve(process.cwd(), program.args[0]) || process.cwd() 64 | var recursive = !!program.recursive 65 | 66 | var fn = linklocal[command] 67 | if (recursive) fn = fn.recursive 68 | if (named) { 69 | fn = linklocal[command].named 70 | dir = process.cwd() 71 | } 72 | 73 | var format = '' 74 | if (program.files) format = '%h' 75 | if (program.links) format = '%s' 76 | if (program.long) format = '%s -> %h' 77 | if (!format) format = program.format 78 | 79 | if (program.absolute) format = format.toUpperCase() 80 | 81 | var options = !named ? {} : { 82 | cwd: program.args[program.args.length - 1], 83 | packages: program.args.slice(0, program.args.length - 1), 84 | recursive: recursive 85 | } 86 | 87 | if (named) { 88 | var renameIndex = program.args.findIndex(function (arg) { return arg.indexOf('@') !== -1 }) 89 | var rename = renameIndex !== -1 ? program.args[renameIndex + 1] : null 90 | 91 | options = { 92 | cwd: program.args[program.args.length - 1], 93 | packages: program.args.slice(0, program.args.length - 1), 94 | scopeRename: rename, 95 | recursive: recursive 96 | } 97 | } 98 | 99 | fn(dir, function (err, items) { 100 | if (err) throw err 101 | items = items || [] 102 | var formattedItems = getFormattedItems(items, format).filter(Boolean) 103 | 104 | if (program.unique) { 105 | formattedItems = formattedItems.filter(function (item, index, arr) { 106 | // uniqueness 107 | return arr.lastIndexOf(item) === index 108 | }) 109 | } 110 | 111 | formattedItems.forEach(function (str) { 112 | console.log('%s', str) 113 | }) 114 | 115 | summary(command, program.list ? formattedItems : items) 116 | }, options) 117 | 118 | var formats = { 119 | '%S': function (obj) { 120 | return obj.from 121 | }, 122 | '%H': function (obj) { 123 | return obj.to 124 | }, 125 | '%s': function (obj) { 126 | return path.relative(process.cwd(), obj.from) 127 | }, 128 | '%h': function (obj) { 129 | return path.relative(process.cwd(), obj.to) 130 | } 131 | } 132 | 133 | function getFormattedItems (items, format) { 134 | return items.map(function (item) { 135 | return formatOut(item, format) 136 | }) 137 | } 138 | 139 | function formatOut (input, format) { 140 | var output = format 141 | for (var key in formats) { 142 | output = output.replace(new RegExp(key, 'gm'), formats[key](input)) 143 | } 144 | return output 145 | } 146 | 147 | function summary (commandName, items) { 148 | if (!program['summary']) return 149 | var length = items.length 150 | commandName = command[0].toUpperCase() + command.slice(1) 151 | console.error('\n%sed %d dependenc' + (length === 1 ? 'y' : 'ies'), commandName, length) 152 | } 153 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # linklocal 2 | 3 | ### Create symlinks to local dependencies in your package.json. 4 | 5 | `linklocal` is a development tool that reduces overheads of breaking your application into small packages. It gives you more expressive power than simple files and folders, yet requires far less overhead than versioning and publishing packages to a local private registry. 6 | 7 | Requires npm 2.0.0 and above in order for npm to recognise [local paths as dependencies](https://docs.npmjs.com/files/package.json#local-paths). 8 | 9 | [![Build Status](https://travis-ci.org/timoxley/linklocal.svg?branch=master)](https://travis-ci.org/timoxley/linklocal) 10 | 11 | [![NPM](https://nodei.co/npm-dl/linklocal.png?months=6&height=3)](https://nodei.co/npm-dl/linklocal/) 12 | [![NPM](https://nodei.co/npm/linklocal.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/linklocal/) 13 | 14 | ## Installation 15 | 16 | ``` 17 | npm install -g linklocal 18 | ``` 19 | 20 | ## Usage 21 | 22 | ``` 23 | linklocal --help 24 | 25 | Usage: linklocal [options] 26 | 27 | Options: 28 | 29 | -h, --help output usage information 30 | -f, --format [format] output format 31 | -l, --link Link local dependencies [default] 32 | -r, --recursive Link recursively 33 | -q, --unique Only unique lines of output 34 | -u, --unlink Unlink local dependencies 35 | -n, --named Link only named packages, last argument is cwd 36 | --absolute Format output paths as absolute paths 37 | --files Output only symlink targets (--format="%h") [default] 38 | --links Output only symlinks (--format="%s") 39 | --list Only list local dependencies. Does not link. 40 | --long Output the symlink to hardlink mapping (--format="%s -> %h") 41 | --no-summary Exclude summary i.e. "Listed 22 dependencies" 42 | -V, --version output the version number 43 | 44 | Examples: 45 | 46 | linklocal # link local deps in current dir 47 | linklocal link # link local deps in current dir 48 | linklocal -r # link local deps recursively 49 | linklocal unlink # unlink only in current dir 50 | linklocal unlink -r # unlink recursively 51 | 52 | linklocal list # list all local deps, ignores link status 53 | linklocal list -r # list all local deps recursively, ignoring link status 54 | 55 | linklocal -- mydir # link local deps in mydir 56 | linklocal unlink -- mydir # unlink local deps in mydir 57 | linklocal --named pkgname ../to/pkg # link local dep by name/path 58 | linklocal --named pkgname1 pkgname2 ../to/pkg # link local deps by name/path 59 | linklocal unlink --named pkgname ../to/pkg # unlink local dep by name/ 60 | linklocal --named -r pkgname ../to/pkg # link local deps recursively by name/ 61 | linklocal --named -r @scope/pkgname pkgname ../to/pkg # link local deps recursively by name/ with npm @scope 62 | 63 | Formats: 64 | 65 | %s: relative path to symlink 66 | %S: absolute path to symlink 67 | %h: relative real path to symlink target 68 | %H: absolute real path to symlink target 69 | 70 | relative paths are relative to cwd 71 | ``` 72 | 73 | ## About 74 | 75 | npm 2.0.0 [supports specifying local dependencies in your package.json](https://docs.npmjs.com/files/package.json#local-paths): 76 | 77 | ``` 78 | > npm install --save ../apple 79 | > cat package.json 80 | { 81 | "name": "bowl", 82 | "version": "1.0.0", 83 | "dependencies": { 84 | "apple": "file:../apple" 85 | } 86 | } 87 | ``` 88 | 89 | `npm install` will copy (and `npm install`) the package into the target's node_module's hierarchy. 90 | 91 | This is not an ideal workflow during development: any time you modify your local dependency, you must reinstall it 92 | in every location that depends on it. If you do not update all copies, you will have different versions of the same code, probably under the same version number. 93 | 94 | Global `npm link` dependencies are also not ideal as packages clobber each other across projects. 95 | 96 | By symlinking local dependencies while in development, 97 | changes can be instantly consumed by dependees, effects 98 | are limited to the current package and you can be more 99 | certain local dependees are using the latest changes. 100 | 101 | `linklocal` symlinks both development and production dependencies, and ignores modules packed by NPM (`.tgz`). 102 | 103 | ## Examples 104 | 105 | ### Linking 106 | 107 | `linklocal` creates symlinks to any local dependencies it finds in your package.json. 108 | 109 | e.g. test/banana/package.json. 110 | 111 | ```json 112 | { 113 | "name": "banana", 114 | "version": "1.0.0", 115 | "private": true, 116 | "dependencies": { 117 | "apple": "file:../apple" 118 | } 119 | } 120 | ``` 121 | Note `file:` dependencies are [standard syntax in npm 2.x](https://docs.npmjs.com/files/package.json#local-paths), just so that npm will copy the dependency into place, rather than symlink it. That's what `linklocal` is for: 122 | ``` 123 | # from test/banana 124 | # find local dependencies and symlink them 125 | > linklocal 126 | node_modules/apple -> ../apple 127 | 128 | Linked 1 dependency 129 | > # proof: 130 | > ls -l node_modules 131 | total 8 132 | lrwxr-xr-x 1 timoxley staff 11 20 Sep 01:39 apple -> ../../apple 133 | ``` 134 | 135 | ## Unlinking 136 | 137 | You can unlink all local links using `linklocal --unlink`. 138 | 139 | ``` 140 | # from test/banana 141 | > linklocal --unlink 142 | node_modules/apple -> ../apple 143 | 144 | Unlinked 1 dependency 145 | 146 | > ls -l node_modules 147 | 148 | > 149 | ``` 150 | 151 | ### Recursively Linking local dependencies in local dependencies 152 | 153 | If your local dependencies have local dependencies, you can use 154 | `linklocal -r` to recursively link all local dependencies: 155 | 156 | `bowl` depends on `banana` 157 | `banana` depends on `apple` 158 | 159 | #### With Recursion 160 | 161 | `apple` gets linked into `banana` 162 | ``` 163 | node_modules/apple -> ../apple 164 | node_modules/banana -> ../banana 165 | ../banana/node_modules/apple -> ../apple 166 | node_modules/@nuts/almond -> ../almond 167 | 168 | Linked 4 dependencies 169 | ``` 170 | 171 | #### Without Recursion 172 | 173 | `apple` does not get linked into `banana` 174 | 175 | ``` 176 | # from test/bowl 177 | > linklocal 178 | node_modules/apple -> ../apple 179 | node_modules/banana -> ../banana 180 | node_modules/@nuts/almond -> ../almond 181 | 182 | Linked 3 dependencies 183 | ``` 184 | 185 | #### Linking and Unlinking Packages by Name 186 | 187 | ``` 188 | # from test/named 189 | > linklocal --named mypackagename ../ 190 | node_modules/mypackagename -> ../mypackagename 191 | 192 | Linked 1 dependency 193 | ``` 194 | 195 | When linking local named packages, you may do so regularly or with recursion. The package names should be entered as an unordered list of strings, where the final argument is the relative path to where the source files for these packages are located. 196 | 197 | 198 | ## Recommendations 199 | 200 | `linklocal` does not install dependencies of linked dependencies. To have dependencies installed, use [timoxley/bulk](https://github.com/timoxley/bulk) or `xargs` in a script like: 201 | ```json 202 | { 203 | "name": "my-app", 204 | "scripts": { 205 | "dev": "linklocal link -r && linklocal list -r | bulk -c 'npm install --production'", 206 | "prepublish": "if [ \"$NODE_ENV\" != \"production\" ]; then npm run dev; fi" 207 | } 208 | } 209 | ``` 210 | 211 | ## Caveats 212 | 213 | * `linklocal` does not install dependencies of linked dependencies, as such you typically end up installing dependencies of linked dependencies twice: once during npm install, then again after linklocal 214 | 215 | ## See Also 216 | 217 | * [aperture](https://github.com/requireio/aperture) 218 | * [district](https://github.com/hughsk/district) 219 | * [symlink](https://github.com/clux/symlink) 220 | 221 | ## Contributors 222 | 223 | Big thanks to: 224 | 225 | * Yoshua Wuyts 226 | * ben_cripps 227 | * Vincent Weevers 228 | * Craig Jefferds 229 | * amalygin 230 | * Mikael Brevik 231 | 232 | # License 233 | 234 | MIT 235 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var mkdirp = require('mkdirp') 6 | var rimraf = require('rimraf') 7 | var assert = require('assert') 8 | var map = require('map-limit') 9 | var os = require('os') 10 | 11 | // Use junctions on Windows 12 | if (os.platform() === 'win32') { 13 | var symlinkType = 'junction' 14 | } else { 15 | symlinkType = 'dir' 16 | } 17 | 18 | module.exports = function linklocal (dirpath, _done) { 19 | function done (err, items) { 20 | _done(err, items || []) 21 | } 22 | assert.equal(typeof dirpath, 'string', 'dirpath should be a string') 23 | assert.equal(typeof done, 'function', 'done should be a function') 24 | 25 | // please enjoy this pyramid 26 | readPackage(dirpath, function (err, pkg) { 27 | if (err) return done(err) 28 | getLinks(pkg, function (err, links) { 29 | if (err) return done(err) 30 | filterAllLinksToUnlink(links, function (err, toUnlink) { 31 | if (err) return done(err) 32 | unlinkLinks(toUnlink, function (err) { 33 | if (err) return done(err) 34 | linkLinks(links, function (err) { 35 | if (err) return done(err) 36 | done(null, links) 37 | }) 38 | }) 39 | }) 40 | }) 41 | }) 42 | } 43 | 44 | module.exports.link = module.exports 45 | 46 | module.exports.link.named = function (dirpath, done, options) { 47 | assert.equal(typeof dirpath, 'string', 'dirpath should be a string') 48 | assert.equal(typeof done, 'function', 'done should be a function') 49 | 50 | var getLinksFn = options.recursive ? getLinksRecursive : getLinks 51 | 52 | readPackage(dirpath, function (err, pkg) { 53 | if (err) return done(err) 54 | getLinksFn(pkg, function (err, links) { 55 | if (err) return done(err) 56 | filterAllLinksToUnlink(links, function (err, toUnlink) { 57 | if (err) return done(err) 58 | unlinkLinks(toUnlink, function (err) { 59 | if (err) return done(err) 60 | linkLinks(links, function (err) { 61 | if (err) return done(err) 62 | done(null, links) 63 | }) 64 | }) 65 | }) 66 | }, options) 67 | }) 68 | } 69 | 70 | module.exports.link.recursive = function linklocalRecursive (dirpath, done) { 71 | assert.equal(typeof dirpath, 'string', 'dirpath should be a string') 72 | assert.equal(typeof done, 'function', 'done should be a function') 73 | 74 | readPackage(dirpath, function (err, pkg) { 75 | if (err) return done(err) 76 | getLinksRecursive(pkg, function (err, links) { 77 | if (err) return done(err) 78 | filterAllLinksToUnlink(links, function (err, toUnlink) { 79 | if (err) return done(err) 80 | unlinkLinks(toUnlink, function (err) { 81 | if (err) return done(err) 82 | linkLinks(links, function (err) { 83 | if (err) return done(err) 84 | done(null, links) 85 | }) 86 | }) 87 | }) 88 | }) 89 | }) 90 | } 91 | 92 | module.exports.unlink = function unlinklocal (dirpath, done) { 93 | readPackage(dirpath, function (err, pkg) { 94 | if (err) return done(err) 95 | getLinks(pkg, function (err, links) { 96 | if (err) return done(err) 97 | filterLinksToUnlink(links, function (err, toUnlink) { 98 | if (err) return done(err) 99 | unlinkLinks(toUnlink, function (err) { 100 | if (err) return done(err) 101 | done(null, toUnlink) 102 | }) 103 | }) 104 | }) 105 | }) 106 | } 107 | 108 | module.exports.unlink.named = function (dirpath, done, options) { 109 | var getLinksFn = options.recursive ? getLinksRecursive : getLinks 110 | 111 | readPackage(dirpath, function (err, pkg) { 112 | if (err) return done(err) 113 | getLinksFn(pkg, function (err, links) { 114 | if (err) return done(err) 115 | filterLinksToUnlink(links, function (err, toUnlink) { 116 | if (err) return done(err) 117 | unlinkLinks(toUnlink, function (err) { 118 | if (err) return done(err) 119 | done(null, toUnlink) 120 | }) 121 | }) 122 | }, options) 123 | }) 124 | } 125 | 126 | module.exports.unlink.recursive = function unlinklocalRecursive (dirpath, done) { 127 | assert.equal(typeof dirpath, 'string', 'dirpath should be a string') 128 | assert.equal(typeof done, 'function', 'done should be a function') 129 | 130 | readPackage(dirpath, function (err, pkg) { 131 | if (err) return done(err) 132 | getLinksRecursive(pkg, function (err, links) { 133 | if (err) return done(err) 134 | filterLinksToUnlink(links, function (err, toUnlink) { 135 | if (err) return done(err) 136 | unlinkLinks(toUnlink, function (err) { 137 | if (err) return done(err) 138 | done(null, toUnlink) 139 | }) 140 | }) 141 | }) 142 | }) 143 | } 144 | 145 | module.exports.list = function list (dirpath, done) { 146 | readPackage(dirpath, function (err, pkg) { 147 | if (err) return done(err) 148 | getLinks(pkg, done) 149 | }) 150 | } 151 | 152 | module.exports.list.recursive = function listRecursive (dirpath, done) { 153 | readPackage(dirpath, function (err, pkg) { 154 | if (err) return done(err) 155 | getLinksRecursive(pkg, done) 156 | }) 157 | } 158 | 159 | function getLinksRecursive (pkg, done, options) { 160 | var _cache = {} 161 | 162 | return (function _getLinksRecursive (pkg, done) { 163 | if (_cache[pkg.dirpath]) return done(null, _cache[pkg.dirpath]) 164 | getLinks(pkg, function (err, links) { 165 | _cache[pkg.dirpath] = _cache[pkg.dirpath] || [] 166 | if (err) return done(err) 167 | _cache[pkg.dirpath] = _cache[pkg.dirpath].concat(links) 168 | map(links, Infinity, function (link, next) { 169 | readPackage(link.to, function (err, pkg) { 170 | if (err) return next(err) 171 | _getLinksRecursive(pkg, next) 172 | }) 173 | }, done) 174 | }, options) 175 | })(pkg, function (err) { 176 | if (err) return done(err) 177 | var result = Object.keys(_cache).reduce(function (result, key) { 178 | return result.concat(_cache[key]) 179 | }, []) 180 | result = uniqueKeys(result, 'from', 'to') 181 | return done(null, result) 182 | }) 183 | } 184 | 185 | function getRealPaths (links, done) { 186 | map(links, Infinity, function (link, next) { 187 | fs.realpath(path.dirname(link), function (err, realPath) { 188 | if (err) return next(err) 189 | next(null, path.join(realPath, path.basename(link))) 190 | }) 191 | }, function (err, links) { 192 | done(err, sortDirs(unique(links || []))) 193 | }) 194 | } 195 | 196 | function readPackage (dirpath, done) { 197 | assert.equal(typeof dirpath, 'string', 'dirpath should be a string') 198 | var pkgpath = path.join(dirpath, 'package.json') 199 | fs.readFile(pkgpath, function (err, data) { 200 | if (err) return done(err) 201 | try { 202 | var pkg = JSON.parse(data) 203 | } catch (e) { 204 | return done(new Error('Error parsing JSON in ' + pkgpath + ':\n' + e.message)) 205 | } 206 | pkg.dirpath = dirpath 207 | return done(null, pkg) 208 | }) 209 | } 210 | 211 | function getLocalDependencies (pkg, done, options) { 212 | assert.equal(typeof pkg, 'object', 'pkg should be an object') 213 | var deps = getDependencies(pkg) 214 | var localDependencies = getPackageLocalDependencies(pkg, options).map(function (name) { 215 | var pkgPath = deps[name] 216 | pkgPath = pkgPath.replace(/^file:/g, '') 217 | 218 | if (options && options.scopeRename) { 219 | return path.resolve(options.cwd, options.scopeRename) 220 | } 221 | 222 | if (options && options.packages.length > 0) { 223 | return path.resolve(options.cwd, name) 224 | } 225 | 226 | return path.resolve(pkg.dirpath, pkgPath) 227 | }) 228 | 229 | getRealPaths(localDependencies, done) 230 | } 231 | 232 | function getPackageLocalDependencies (pkg, options) { 233 | assert.equal(typeof pkg, 'object', 'pkg should be an object') 234 | var deps = getDependencies(pkg) 235 | return Object.keys(deps).filter(function (name) { 236 | var dep = deps[name] 237 | return isLocalDependency(dep, name, options) 238 | }) 239 | } 240 | 241 | function getDependencies (pkg) { 242 | var deps = pkg.dependencies || {} 243 | var devDependencies = pkg.devDependencies || {} 244 | for (var name in devDependencies) { 245 | deps[name] = devDependencies[name] 246 | } 247 | return deps 248 | } 249 | 250 | function isLocalDependency (val, name, options) { 251 | var ignoreExt = '.tgz' 252 | 253 | if (options && options.scopeRename && options.packages) { 254 | return isScopedDependency(name, options) 255 | } 256 | 257 | if (options && options.packages) { 258 | return options.packages.indexOf(name) !== -1 259 | } 260 | 261 | return ( 262 | (val.indexOf('.') === 0 || 263 | val.indexOf('/') === 0 || 264 | val.indexOf('file:') === 0) && 265 | val.lastIndexOf(ignoreExt) !== val.length - ignoreExt.length 266 | ) 267 | } 268 | 269 | function getLinks (pkg, done, options) { 270 | getLocalDependencies(pkg, function (err, localDependencies) { 271 | if (err) return done(err) 272 | var destination = path.join(pkg.dirpath, 'node_modules') 273 | map(localDependencies, Infinity, readPackage, function (err, localDependencyPackages) { 274 | if (err) return done(err) 275 | var links = localDependencyPackages.map(function (localDependency) { 276 | return { 277 | from: path.resolve(destination, localDependency.name), 278 | to: localDependency.dirpath 279 | } 280 | }) 281 | done(null, links) 282 | }) 283 | }, options) 284 | } 285 | 286 | function isScopedDependency (name, options) { 287 | return name.indexOf('@') !== -1 && options.packages.indexOf(name) !== -1 288 | } 289 | 290 | function isSymbolicLink (filepath, done) { 291 | exists(filepath, function (err, doesExist) { 292 | if (err) return done(err) 293 | if (!doesExist) return done(null, false) 294 | fs.lstat(filepath, function (err, stat) { 295 | if (err) return done(err) 296 | return done(null, stat.isSymbolicLink()) 297 | }) 298 | }) 299 | } 300 | 301 | function linksTo (from, to, done) { 302 | to = path.resolve(path.dirname(from), to) 303 | isSymbolicLink(from, function (err, isLink) { 304 | if (err) return done(err) 305 | if (!isLink) return done(null, false) 306 | fs.readlink(from, function (err, currentLink) { 307 | if (err) return done(err) 308 | currentLink = path.resolve(path.dirname(from), currentLink) 309 | return done(null, currentLink === to) 310 | }) 311 | }) 312 | } 313 | 314 | function filterLinksToUnlink (links, done) { 315 | links = uniqueKey(links, 'from') 316 | filter(links, Infinity, function (link, next) { 317 | linksTo(link.from, link.to, next) 318 | }, done) 319 | } 320 | 321 | function filterAllLinksToUnlink (links, done) { 322 | links = uniqueKey(links, 'from') 323 | filter(links, Infinity, function (link, next) { 324 | exists(link.from, function (err, ex) { 325 | next(err, ex) 326 | }) 327 | }, function (err, toUnlink) { 328 | if (err) return done(err) 329 | return done(null, toUnlink) 330 | }) 331 | } 332 | 333 | function linkLinks (links, done) { 334 | assert.ok(Array.isArray(links), 'links should be an array') 335 | map(links, Infinity, function (link, next) { 336 | mkdirp(path.dirname(link.from), function (err) { 337 | if (err && err.code !== 'EEXISTS') return next(err) 338 | 339 | var from = link.from 340 | var to = link.to 341 | 342 | // Junction points can't be relative 343 | if (symlinkType !== 'junction') { 344 | to = path.relative(path.dirname(from), to) 345 | } 346 | 347 | fs.symlink(to, from, symlinkType, function (err) { 348 | if (err) return next(new Error('Error linking ' + from + ' to ' + to + ':\n' + err.message)) 349 | next(null, link) 350 | }) 351 | }) 352 | }, done) 353 | } 354 | 355 | function unlinkLinks (links, done) { 356 | assert.ok(Array.isArray(links), 'links should be an array') 357 | map(links, Infinity, function (link, next) { 358 | isSymbolicLink(link.from, function (err, isSymlink) { 359 | if (err) return next(err) 360 | var remove = isSymlink ? fs.unlink : rimraf 361 | return remove(link.from, function (err) { 362 | if (err) return next(new Error('Error removing ' + link.from + ':\n' + err.message)) 363 | next(err, link) 364 | }) 365 | }) 366 | }, done) 367 | } 368 | 369 | function uniqueKeys (items, key1, key2) { 370 | var keys = [].slice.call(arguments, 1) 371 | return items.filter(function (item, index, arr) { 372 | return indexOf(items, function (otherItem) { 373 | return keys.every(function (key) { 374 | return item[key] === otherItem[key] 375 | }) 376 | }) === index 377 | }) 378 | } 379 | 380 | function indexOf (items, fn) { 381 | return items.reduce(function (foundIndex, item, index) { 382 | if (foundIndex !== -1) return foundIndex 383 | if (fn(item)) return index 384 | return foundIndex 385 | }, -1) 386 | } 387 | 388 | function uniqueKey (items, key) { 389 | var values = items.map(function (item) { 390 | return item[key] 391 | }) 392 | values = unique(values) 393 | return items.filter(function (item) { 394 | if (!values.length) return 395 | var indexOfValue = values.indexOf(item[key]) 396 | if (indexOfValue === -1) return false 397 | values.splice(indexOfValue, 1) 398 | return true 399 | }) 400 | } 401 | 402 | function unique (arr) { 403 | return arr.filter(function (item, index, arr) { 404 | return arr.indexOf(item) === index 405 | }) 406 | } 407 | 408 | function sortDirs (dirs) { 409 | return dirs.sort(function (dirA, dirB) { 410 | if (dirA === dirB) return 0 411 | if (dirB.indexOf(dirA) === 0) return 1 412 | return -1 413 | }) 414 | } 415 | 416 | function filter (arr, num, filterFn, done) { 417 | map(arr, num, filterFn, function (err, matches) { 418 | return done(err, arr.filter(function (item, index) { 419 | return !!matches[index] 420 | })) 421 | }) 422 | } 423 | 424 | function exists (filepath, done) { 425 | fs.lstat(filepath, function (err, stat) { 426 | if (err) { 427 | if (err.code !== 'ENOENT') return done(err) 428 | return done(null, false) 429 | } 430 | done(null, !!stat) 431 | }) 432 | } 433 | --------------------------------------------------------------------------------