├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE-MIT ├── README.md ├── appveyor.yml ├── gulpfile.js ├── index.js ├── package.json └── test ├── fixtures ├── test └── test_dir │ └── .gitkeep └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | test/fixtures/links 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "expr": true, 3 | "strict": true, 4 | "trailing": true, 5 | "undef": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "latedef": true, 10 | "noarg": true, 11 | "noempty": true, 12 | "unused": true, 13 | "indent": 4 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | - '0.12' 6 | - '0.10' 7 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Ben Briggs 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice 2 | 3 | gulp 4 adds built-in symlinks to the public API, making it really easy to 4 | create streams of symlinks. Therefore, this module has been deprecated. 5 | 6 | You may replace this module with a call to [vinyl-fs][vfs] for gulp 3.x: 7 | 8 | ```js 9 | var vfs = require('vinyl-fs'); 10 | 11 | gulp.task('symlink', function () { 12 | return vfs.src('assets/some-large-video.mp4', {followSymlinks: false}) 13 | .pipe(vfs.symlink('build/videos')); 14 | }); 15 | ``` 16 | 17 | [vfs]: https://github.com/gulpjs/vinyl-fs 18 | 19 | # [gulp](https://github.com/gulpjs/gulp)-symlink 20 | 21 | > Create symlinks during your gulp build. 22 | 23 | ## Install 24 | 25 | With [npm](https://npmjs.org/package/gulp-symlink) do: 26 | 27 | ``` 28 | npm install gulp-symlink --save-dev 29 | ``` 30 | 31 | ## Example 32 | 33 | ```js 34 | var symlink = require('gulp-symlink'); 35 | 36 | gulp.task('default', function () { 37 | return gulp.src('assets/some-large-video.mp4') 38 | .pipe(symlink('build/videos')) // Write to the destination folder 39 | .pipe(symlink('build/videos/renamed-video.mp4')) // Write a renamed symlink to the destination folder 40 | }); 41 | ``` 42 | 43 | ## API 44 | 45 | ### symlink(path, [options]), symlink.relative(path, [options]) or symlink.absolute(path, [options]) 46 | 47 | Pass a `string` or a `function` to create the symlink. 48 | The function is passed the [vinyl](https://github.com/wearefractal/vinyl) object, so you can use `file.base`, `file.path` etc. 49 | For example: 50 | 51 | ```js 52 | gulp.task('symlink', function () { 53 | return gulp.src('assets/some-large-video.mp4') 54 | .pipe(symlink(function (file) { 55 | // Here we return a path as string 56 | return path.join(file.base, 'build', file.relative.replace('some-large', '')); 57 | })); 58 | }); 59 | 60 | gulp.task('symlink-vinyl', function () { 61 | return gulp.src('assets/some-large-video.mp4') 62 | .pipe(symlink.absolute(function (file) { 63 | // Here we return a new Vinyl instance 64 | return new symlink.File({ 65 | path: 'build/videos/video.mp4', 66 | cwd: process.cwd() 67 | }); 68 | }, {force: true})); 69 | }) 70 | ``` 71 | 72 | The string options work in the same way. If you pass a string like 'build/videos', the symlink will be created in that directory. If you pass 'build/videos/video.mp4', the symlink will also be renamed. 73 | The function will be called as many times as there are sources. 74 | 75 | You might also want to give an array of destination paths: 76 | 77 | ```js 78 | gulp.task('symlink-array', function () { 79 | return gulp.src(['modules/assets/', 'modules/client/']) 80 | .pipe(symlink(['./assets', './client'])); 81 | }); 82 | ``` 83 | 84 | The default `symlink` performs a relative link. If you want an *absolute symlink* use `symlink.absolute` instead. 85 | 86 | ### symlink.File 87 | 88 | The [vinyl module](https://github.com/wearefractal/vinyl) is exposed here. If you are creating new files with the function as shown above, please use this one. 89 | 90 | ## Contributing 91 | 92 | Pull requests are welcome. If you add functionality, then please add unit tests 93 | to cover it. 94 | 95 | ## License 96 | 97 | MIT © [Ben Briggs](http://beneb.info) 98 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # AppVeyor file 2 | # http://www.appveyor.com/docs/appveyor-yml 3 | 4 | environment: 5 | matrix: 6 | - nodejs_version: 0.10 7 | - nodejs_version: 0.12 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | 13 | build: off 14 | 15 | test_script: 16 | # Output useful info for debugging. 17 | - node --version && npm --version 18 | - ps: "npm test # PowerShell" # Pass comment to PS for easier debugging 19 | - cmd: npm test 20 | 21 | matrix: 22 | fast_finish: true 23 | 24 | cache: 25 | - C:\Users\appveyor\AppData\Roaming\npm\node_modules -> package.json # global npm modules 26 | - C:\Users\appveyor\AppData\Roaming\npm-cache -> package.json # npm cache 27 | - node_modules -> package.json # local npm modules 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | 'use strict'; 4 | 5 | var gulp = require('gulp'), 6 | gutil = require('gulp-util'), 7 | clear = require('clear'), 8 | mocha = require('gulp-mocha'), 9 | jshint = require('gulp-jshint'); 10 | 11 | gulp.task('lint', function () { 12 | gulp.src(['*.js', 'test/**/*.js']) 13 | .pipe(jshint()) 14 | .pipe(jshint.reporter('jshint-stylish')) 15 | .pipe(mocha()); 16 | }); 17 | 18 | gulp.task('default', function() { 19 | gulp.run('lint'); 20 | gulp.watch('*.js', function(event) { 21 | clear(); 22 | gutil.log(gutil.colors.cyan(event.path.replace(process.cwd(), '')) + ' ' + event.type + '. (' + gutil.colors.magenta(gutil.date('HH:MM:ss')) + ')'); 23 | gulp.run('lint'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | 'use strict'; 4 | 5 | var through = require('through2'), 6 | mkdirp = require('mkdirp'), 7 | gutil = require('gulp-util'), 8 | path = require('path'), 9 | fs = require('fs'), 10 | async = require('async'), 11 | PluginError = gutil.PluginError, 12 | File = gutil.File, 13 | isWin = process.platform === 'win32', 14 | debug; 15 | 16 | var PLUGIN_NAME = 'gulp-symlink'; 17 | 18 | /** 19 | * Wrapper to log when debug === true 20 | * it's basically a console.log 21 | */ 22 | var log = function() { 23 | if(debug === true) { 24 | console.log.apply(console, [].slice.call(arguments)); 25 | return console.log; 26 | } else { 27 | return function() { }; 28 | } 29 | 30 | }; 31 | 32 | /** 33 | * Error wrapper - this is called in the through context 34 | * @param {Error} error The error 35 | * @return {Function} cb The through callback 36 | */ 37 | var errored = function(error, cb) { 38 | this.emit('error', new PluginError(PLUGIN_NAME, error)); 39 | //Push the file so that the stream is piped to the next task even if it has errored 40 | //might be discussed 41 | this.push(this.source); 42 | return cb(); 43 | }; 44 | 45 | var symlinker = function(destination, resolver, options) { 46 | 47 | if(typeof resolver === 'object') { 48 | options = resolver; 49 | resolver = 'relative'; 50 | } 51 | 52 | options = typeof options === 'object' ? options : {}; 53 | options.force = options.force === undefined ? false : options.force; 54 | options.log = options.log === undefined ? true : options.log; 55 | 56 | //Handling array of destinations, this test is because "instance of" isn't safe 57 | if( Object.prototype.toString.call( destination ) === '[object Array]' ) { 58 | //copy array because we'll shift values 59 | var destinations = destination.slice(); 60 | } 61 | 62 | return through.obj(function(source, encoding, callback) { 63 | 64 | var self = this, symlink; 65 | 66 | this.source = source; //error binding 67 | 68 | //else if we've got an array from before take the next element as a destination path 69 | symlink = destinations !== undefined ? destinations.shift() : symlink; 70 | 71 | //if destination is a function pass the source to it 72 | if(symlink === undefined) { 73 | symlink = typeof destination === 'function' ? destination(source) : destination; 74 | } 75 | 76 | //if symlink is still undefined there is a problem! 77 | if (symlink === undefined) { 78 | return errored.call(self, 'An output destination is required.', callback); 79 | } 80 | 81 | // Convert the destination path to a new vinyl instance 82 | symlink = symlink instanceof File ? symlink : new File({ path: symlink }); 83 | 84 | log('Before resolving')('Source: %s – dest: %s', source.path, symlink.path); 85 | 86 | symlink.directory = path.dirname(symlink.path); //this is the parent directory of the symlink 87 | 88 | // Resolve the path to the symlink 89 | if(resolver === 'relative' || options.relative === true) { 90 | source.resolved = path.relative(symlink.directory, source.path); 91 | } else { 92 | //resolve the absolute path from the source. It need to be from the current working directory to handle relative sources 93 | source.resolved = path.resolve(source.cwd, source.path); 94 | } 95 | 96 | log('After resolving')(source.resolved + ' in ' + symlink.path); 97 | 98 | fs.exists(symlink.path, function(exists) { 99 | 100 | //No force option, we can't override! 101 | if(exists && !options.force) { 102 | return errored.call(self, 'Destination file exists ('+symlink.path+') - use force option to replace', callback); 103 | } else { 104 | 105 | async.waterfall([ 106 | function(next){ 107 | //remove destination if it exists already 108 | if(exists && options.force === true) { 109 | fs.unlink(symlink.path, function(err) { 110 | if(err) { 111 | return errored.call(self, err, callback); 112 | } 113 | 114 | next(); 115 | }); 116 | } else { 117 | next(); 118 | } 119 | }, 120 | //checking if the parent directory exists 121 | function(next) { 122 | mkdirp(symlink.directory, function(err) { 123 | //ignoring directory err if it exists 124 | if(err && err.code !== 'EEXIST') { 125 | return errored.call(self, err, callback); 126 | } 127 | 128 | next(); 129 | }); 130 | } 131 | ], function () { 132 | //this is a windows check as specified in http://nodejs.org/api/fs.html#fs_fs_symlink_srcpath_dstpath_type_callback 133 | fs.stat(source.path, function(err, stat) { 134 | if(err) { 135 | return errored.call(self, err, callback); 136 | } 137 | 138 | source.stat = stat; 139 | 140 | fs.symlink(source.resolved, symlink.path, source.stat.isDirectory() ? 'dir' : 'file', function(err) { 141 | var success = function(){ 142 | if (options.log) { 143 | gutil.log(PLUGIN_NAME + ':' + gutil.colors.magenta(source.path), 'symlinked to', gutil.colors.magenta(symlink.path)); 144 | } 145 | self.push(source); 146 | return callback(); 147 | }; 148 | 149 | if(err) { 150 | if (!isWin || err.code !== 'EPERM') { 151 | return errored.call(self, err, callback); 152 | } 153 | 154 | // Try with type "junction" on Windows 155 | // Junctions behave equally to true symlinks and can be created in 156 | // non elevated terminal (well, not always..) 157 | fs.symlink(source.path, symlink.path, 'junction', function(err) { 158 | if (err) { 159 | return errored.call(self, err, callback); 160 | } 161 | 162 | return success(); 163 | }); 164 | } 165 | 166 | return success(); 167 | 168 | }); 169 | 170 | }); 171 | 172 | }); 173 | 174 | } 175 | 176 | }); 177 | 178 | }); 179 | }; 180 | 181 | var relativesymlinker = function(symlink, options) { 182 | return symlinker(symlink, 'relative', options); 183 | }; 184 | 185 | var absolutesymlinker = function(symlink, options) { 186 | return symlinker(symlink, 'absolute', options); 187 | }; 188 | 189 | // Expose main functionality under relative, for convenience 190 | module.exports = relativesymlinker; 191 | module.exports.relative = relativesymlinker; 192 | module.exports.absolute = absolutesymlinker; 193 | module.exports.File = File; 194 | 195 | module.exports._setDebug = function(value) { 196 | debug = value; 197 | }; 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-symlink", 3 | "version": "2.1.4", 4 | "description": "Create symlinks during your gulp build.", 5 | "license": "MIT", 6 | "homepage": "https://github.com/ben-eb/gulp-symlink", 7 | "author": { 8 | "name": "Ben Briggs", 9 | "email": "beneb.info@gmail.com", 10 | "url": "http://beneb.info" 11 | }, 12 | "scripts": { 13 | "test": "mocha" 14 | }, 15 | "repository": "ben-eb/gulp-symlink", 16 | "keywords": [ 17 | "gulpplugin", 18 | "symlink" 19 | ], 20 | "dependencies": { 21 | "gulp-util": "~3.0.6", 22 | "mkdirp": "~0.5.1", 23 | "through2": "~2.0.0", 24 | "async": "~1.4.0" 25 | }, 26 | "devDependencies": { 27 | "chai": "~3.2.0", 28 | "clear": "0.0.1", 29 | "gulp": "~3.9.0", 30 | "gulp-jshint": "~1.11.2", 31 | "gulp-mocha": "~2.1.3", 32 | "jshint-stylish": "~2.0.1", 33 | "mocha": "^2.2.5", 34 | "rimraf": "~2.4.2" 35 | }, 36 | "main": "index.js" 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/test: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /test/fixtures/test_dir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-eb/gulp-symlink/2dc774cc8cc9d80569a778e520600c4fdeae632d/test/fixtures/test_dir/.gitkeep -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* global describe, it, beforeEach, before */ 3 | 4 | 'use strict'; 5 | 6 | var expect = require('chai').expect, 7 | rimraf = require('rimraf'), 8 | gutil = require('gulp-util'), 9 | symlink = require('../'), 10 | path = require('path'), 11 | fs = require('fs'), 12 | File = gutil.File, 13 | testDir = path.join(__dirname, './fixtures'); 14 | 15 | symlink._setDebug(true); 16 | 17 | /** 18 | * Expect that we created a symlink which contains the desired link text; 19 | * i.e. whether it is relative or absolute 20 | * @param {string} originalPath The original file's path 21 | * @param {string} symlinkPath The symlink's path 22 | * @param {Function} resolver Method to check that the link between the files is accurate 23 | * @param {Function} callback Call this when we're done, if specified 24 | */ 25 | var assertion = function(originalPath, symlinkPath, resolver, callback) { 26 | fs.lstat(symlinkPath, function(err, stats) { 27 | if(err) { 28 | throw err; 29 | } 30 | 31 | expect(stats.isSymbolicLink()).to.be.true; 32 | 33 | fs.stat(originalPath, function(err, originalStat) { 34 | 35 | if(err) { 36 | throw err; 37 | } 38 | 39 | if(originalStat.isFile()) { 40 | expect(fs.readFileSync(originalPath).toString()).to.equal(fs.readFileSync(symlinkPath).toString()); 41 | } 42 | 43 | fs.readlink(symlinkPath, function(err, link) { 44 | if(resolver === 'relative') { 45 | expect(link).to.equal(path[resolver].call(this, path.dirname(symlinkPath), originalPath)); 46 | } else { 47 | expect(link).to.equal(path.resolve(path.dirname(symlinkPath), path.join(process.cwd(), originalPath.replace(process.cwd(), '')))); 48 | } 49 | callback && callback(); 50 | }); 51 | 52 | }); 53 | }); 54 | }; 55 | 56 | describe('gulp-symlink', function() { 57 | function test(source, method, pathMethod) { 58 | 59 | var destination = path.join(testDir, 'links', path.basename(source)); 60 | 61 | destination = path[pathMethod].call(null, process.cwd(), destination); 62 | 63 | beforeEach(function() { 64 | this.gutilFile = new File({ 65 | path: source 66 | }); 67 | }); 68 | 69 | it('should emit an error if no destination was specified', function(cb) { 70 | var stream = method(); 71 | 72 | stream.on('error', function(err) { 73 | expect(err instanceof gutil.PluginError).to.be.true; 74 | expect(err.toString()).to.contain.string('An output destination is required.'); 75 | 76 | cb(); 77 | }); 78 | 79 | stream.write(this.gutilFile); 80 | }); 81 | 82 | it('should create symlinks', function(cb) { 83 | var stream = method(destination); 84 | 85 | stream.on('data', function() { 86 | assertion(this.gutilFile.path, destination, pathMethod, cb); 87 | }.bind(this)); 88 | 89 | stream.write(this.gutilFile); 90 | }); 91 | 92 | it('should emit an error because it exists already', function(cb) { 93 | var stream = method(destination); 94 | 95 | stream.on('error', function(err) { 96 | expect(err instanceof gutil.PluginError).to.be.true; 97 | expect(err.toString()).to.contain.string('Destination file exists'); 98 | 99 | cb(); 100 | }); 101 | 102 | stream.write(this.gutilFile); 103 | }); 104 | 105 | it('should override symlinks', function(cb) { 106 | var stream = method(destination, {force: true}); 107 | 108 | stream.on('data', function() { 109 | assertion(this.gutilFile.path, destination, pathMethod, cb); 110 | }.bind(this)); 111 | 112 | stream.write(this.gutilFile); 113 | }); 114 | 115 | it('should create symlinks renamed as a result of a function', function(cb) { 116 | var newName = 'renamed-link'; 117 | var newTestPath = path.join(testDir, 'links', newName); 118 | 119 | var stream = method(function() { 120 | return newTestPath; 121 | }); 122 | 123 | stream.on('data', function() { 124 | assertion(this.gutilFile.path, newTestPath, pathMethod, cb); 125 | }.bind(this)); 126 | 127 | stream.write(this.gutilFile); 128 | }); 129 | 130 | it('should create symlinks from functions that return vinyl objects', function(cb) { 131 | var file = new File({ 132 | path: destination+'2' 133 | }); 134 | 135 | var stream = method(function() { 136 | return file; 137 | }); 138 | 139 | stream.on('data', function() { 140 | assertion(this.gutilFile.path, file.path, pathMethod, cb); 141 | }.bind(this)); 142 | 143 | stream.write(this.gutilFile); 144 | }); 145 | 146 | it('should create renamed symlinks', function(cb) { 147 | var newName = 'renamed-link-2'; 148 | var newTestPath = path.join(testDir, 'links', newName); 149 | 150 | var stream = method(newTestPath); 151 | 152 | stream.on('data', function() { 153 | assertion(this.gutilFile.path, newTestPath, pathMethod, cb); 154 | }.bind(this)); 155 | 156 | stream.write(this.gutilFile); 157 | }); 158 | 159 | it('should create symlinks in nested directories', function(cb) { 160 | var subTestPath = path.join(testDir, 'links', 'subDir', path.basename(source)); 161 | 162 | var stream = method(subTestPath); 163 | 164 | stream.on('data', function() { 165 | assertion(this.gutilFile.path, subTestPath, pathMethod, cb); 166 | }.bind(this)); 167 | 168 | stream.write(this.gutilFile); 169 | }); 170 | 171 | before(function(cb) { 172 | rimraf(path.join(testDir, 'links'), cb); 173 | }); 174 | 175 | } 176 | 177 | describe('using relative paths & relative symlinks', function() { 178 | test('./test/fixtures/test', symlink.relative, 'relative'); 179 | }); 180 | 181 | describe('using full paths & relative symlinks', function() { 182 | test(path.join(__dirname, '/fixtures/test'), symlink.relative, 'relative'); 183 | }); 184 | 185 | describe('using relative paths & absolute symlinks', function() { 186 | test('./test/fixtures/test', symlink.absolute, 'resolve'); 187 | }); 188 | 189 | describe('using full paths & absolute symlinks', function() { 190 | test(path.join(__dirname, '/fixtures/test'), symlink.absolute, 'resolve'); 191 | }); 192 | 193 | describe('using a directory', function() { 194 | test('./test/fixtures/test_dir', symlink, 'relative'); 195 | }); 196 | 197 | describe('e2e tests', function() { 198 | 199 | it('should symlink through path', function(cb) { 200 | var src = new File({path: path.join(testDir, 'test')}), dest = './test/fixtures/links/test'; 201 | 202 | var stream = symlink(dest, {force: true}); 203 | 204 | stream.on('data', function() { 205 | assertion(src.path, dest, 'relative', cb); 206 | }); 207 | 208 | stream.write(src); 209 | }); 210 | 211 | it('should symlink 2 sources to 2 different destinations [array]', function(cb) { 212 | var srcs = [path.join(testDir, 'test'), path.join(testDir, 'test_dir')], dests = ['./test/fixtures/links/test', './test/fixtures/links/test_dir']; 213 | 214 | var stream = symlink(dests, {force: true}); 215 | 216 | stream.on('data', function() { }); 217 | 218 | stream.on('end', function() { 219 | for(var j in dests) { 220 | expect(fs.existsSync(dests[j])).to.be.true; 221 | } 222 | 223 | cb(); 224 | }); 225 | 226 | stream.write(new File({path: srcs[0]})); 227 | stream.write(new File({path: srcs[1]})); 228 | stream.end(); 229 | }); 230 | }); 231 | 232 | }); 233 | --------------------------------------------------------------------------------