Repository: gulpjs/vinyl-fs (js)

Issue:
Symlinks on Modern Windows with latest node/npm.
Theree are not only two options to creating a symlink. Either Node should change it's default behavior for the symlink type 'dir', or module makes should start using 'junction' instead of 'dir' when on windows. The symlink 'dir' on Windows does not behave the same way as a POSIX style symlink. 

The proper symlink for windows is a Junction which makes more sense when comparing it to a *nix style symlink. 

symlink's of type 'Dir' cannot be created on windows without a Run As verb which is completely unacceptable. I've patched dep-linker to work on all platforms and not require the process to be ran as an actual Administrator. 

This is the equivalent of REQUIRING a Linux user to run gulp as root. Not sudo'ing the process. But literally requiring them to run gulp as root user. I cannot imagine that would ever make it into a pull request, why does it make it into pull requests on the Windows side?


Pull request description:
Fix #210 Windows symbolic links requiring admin
This is just a reference PR for #211 because I'm not sure if the original author has time to finish this up.

TODO:
* [x] Remove new `link` method
* [x] Remove the added `src` tests because they don't do anything
* [x] Change author of the commit to give original author credit
* [x] Documentation
* [x] Don't allow the `relative` option with junctions
* [x] Need test for `forceJunctionDir` (and rename `useJunctions`) - Support function

Patch:
diff --git a/README.md b/README.md
index 0f61865..ae10764 100644
--- a/README.md
+++ b/README.md
@@ -233,6 +233,8 @@ Returns a stream that accepts [vinyl] `File` objects, create a symbolic link (i.
 __Note: The file will be modified after being written to this stream.__
   - `cwd`, `base`, and `path` will be overwritten to match the folder.
 
+__Note: On Windows, directory links are created using Junctions by default. Use the `useJunctions` option to disable this behavior.__
+
 #### Options
 
 - Values passed to the options must be of the right type, otherwise they will be ignored.
@@ -260,7 +262,15 @@ Whether or not the symlink should be relative or absolute.
 
 Type: `Boolean`
 
-Default: `false`.
+Default: `false`
+
+##### `options.useJunctions`
+
+Whether or not a directory symlink should be created as a `junction`.
+
+Type: `Boolean`
+
+Default: `true` on Windows, `false` on all other platforms
 
 ##### other
 
diff --git a/lib/dest/write-contents/write-stream.js b/lib/dest/write-contents/write-stream.js
index 4541b13..8fff529 100644
--- a/lib/dest/write-contents/write-stream.js
+++ b/lib/dest/write-contents/write-stream.js
@@ -41,7 +41,7 @@ function writeStream(file, onWritten) {
     file.contents.removeListener('error', onComplete);
 
     // TODO: this is doing sync stuff & the callback seems unnecessary
-    // TODO: do we really want to replace the contents stream or should we use a clone
+    // TODO: Replace the contents stream or use a clone?
     readStream(file, complete);
 
     function complete() {
diff --git a/lib/symlink/index.js b/lib/symlink/index.js
index 9b452a6..eaa3200 100644
--- a/lib/symlink/index.js
+++ b/lib/symlink/index.js
@@ -1,6 +1,7 @@
 'use strict';
 
 var path = require('path');
+var os = require('os');
 
 var fs = require('graceful-fs');
 var through2 = require('through2');
@@ -12,6 +13,8 @@ var prepareWrite = require('../prepare-write');
 
 var boolean = valueOrFunction.boolean;
 
+var isWindows = (os.platform() === 'win32');
+
 function symlink(outFolder, opt) {
   if (!opt) {
     opt = {};
@@ -19,7 +22,24 @@ function symlink(outFolder, opt) {
 
   function linkFile(file, enc, callback) {
     var srcPath = file.path;
-    var symType = (file.isDirectory() ? 'dir' : 'file');
+
+    var isDirectory = file.isDirectory();
+
+    // This option provides a way to create a Junction instead of a
+    // Directory symlink on Windows. This comes with the following caveats:
+    // * NTFS Junctions cannot be relative.
+    // * NTFS Junctions MUST be directories.
+    // * NTFS Junctions must be on the same file system.
+    // * Most products CANNOT detect a directory is a Junction:
+    //    This has the side effect of possibly having a whole directory
+    //    deleted when a product is deleting the Junction directory.
+    //    For example, IntelliJ product lines will delete the entire
+    //    contents of the TARGET directory because the product does not
+    //    realize it's a symlink as the JVM and Node return false for isSymlink.
+    var useJunctions = koalas(boolean(opt.useJunctions, file), (isWindows && isDirectory));
+
+    var symDirType =  useJunctions ? 'junction' : 'dir';
+    var symType = isDirectory ? symDirType : 'file';
     var isRelative = koalas(boolean(opt.relative, file), false);
 
     prepareWrite(outFolder, file, opt, onPrepare);
@@ -30,7 +50,7 @@ function symlink(outFolder, opt) {
       }
 
       // This is done inside prepareWrite to use the adjusted file.base property
-      if (isRelative) {
+      if (isRelative && !useJunctions) {
         srcPath = path.relative(file.base, srcPath);
       }
 


Test patch:
diff --git a/test/symlink.js b/test/symlink.js
index 982bd71..9c0dcb6 100644
--- a/test/symlink.js
+++ b/test/symlink.js
@@ -225,7 +225,12 @@ describe('symlink stream', function() {
     ], done);
   });
 
-  it('creates a link for a directory', function(done) {
+  it('(*nix) creates a link for a directory', function(done) {
+    if (isWindows) {
+      this.skip();
+      return;
+    }
+
     var file = new File({
       base: inputBase,
       path: inputDirpath,
@@ -256,7 +261,127 @@ describe('symlink stream', function() {
     ], done);
   });
 
-  it('can create relative links for directories', function(done) {
+  it('(windows) creates a junction for a directory', function(done) {
+    if (!isWindows) {
+      this.skip();
+      return;
+    }
+
+    var file = new File({
+      base: inputBase,
+      path: inputDirpath,
+      contents: null,
+      stat: {
+        isDirectory: isDirectory,
+      },
+    });
+
+    function assert(files) {
+      var stats = fs.statSync(outputDirpath);
+      var lstats = fs.lstatSync(outputDirpath);
+      var outputLink = fs.readlinkSync(outputDirpath);
+
+      expect(files.length).toEqual(1);
+      expect(files).toInclude(file);
+      expect(files[0].base).toEqual(outputBase, 'base should have changed');
+      expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
+      // When creating a junction, it seems Windows appends a separator
+      expect(outputLink).toEqual(inputDirpath + path.sep);
+      expect(stats.isDirectory()).toEqual(true);
+      expect(lstats.isDirectory()).toEqual(false);
+    }
+
+    pipe([
+      from.obj([file]),
+      vfs.symlink(outputBase),
+      concat(assert),
+    ], done);
+  });
+
+  it('(windows) options can disable junctions for a directory', function(done) {
+    if (!isWindows) {
+      this.skip();
+      return;
+    }
+
+    var file = new File({
+      base: inputBase,
+      path: inputDirpath,
+      contents: null,
+      stat: {
+        isDirectory: isDirectory,
+      },
+    });
+
+    function assert(files) {
+      var stats = fs.statSync(outputDirpath);
+      var lstats = fs.lstatSync(outputDirpath);
+      var outputLink = fs.readlinkSync(outputDirpath);
+
+      expect(files.length).toEqual(1);
+      expect(files).toInclude(file);
+      expect(files[0].base).toEqual(outputBase, 'base should have changed');
+      expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
+      expect(outputLink).toEqual(inputDirpath);
+      expect(stats.isDirectory()).toEqual(true);
+      expect(lstats.isDirectory()).toEqual(false);
+    }
+
+    pipe([
+      from.obj([file]),
+      vfs.symlink(outputBase, { useJunctions: false }),
+      concat(assert),
+    ], done);
+  });
+
+  it('(windows) options can disable junctions for a directory (as a function)', function(done) {
+    if (!isWindows) {
+      this.skip();
+      return;
+    }
+
+    var file = new File({
+      base: inputBase,
+      path: inputDirpath,
+      contents: null,
+      stat: {
+        isDirectory: isDirectory,
+      },
+    });
+
+    function useJunctions(f) {
+      expect(f).toExist();
+      expect(f).toBe(file);
+      return false;
+    }
+
+    function assert(files) {
+      var stats = fs.statSync(outputDirpath);
+      var lstats = fs.lstatSync(outputDirpath);
+      var outputLink = fs.readlinkSync(outputDirpath);
+
+      expect(files.length).toEqual(1);
+      expect(files).toInclude(file);
+      expect(files[0].base).toEqual(outputBase, 'base should have changed');
+      expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
+      expect(outputLink).toEqual(inputDirpath);
+      expect(stats.isDirectory()).toEqual(true);
+      expect(lstats.isDirectory()).toEqual(false);
+    }
+
+    pipe([
+      from.obj([file]),
+      vfs.symlink(outputBase, { useJunctions: useJunctions }),
+      concat(assert),
+    ], done);
+  });
+
+  it('(*nix) can create relative links for directories', function(done) {
+    if (isWindows) {
+      this.skip();
+      return;
+    }
+
     var file = new File({
       base: inputBase,
       path: inputDirpath,
@@ -287,6 +412,79 @@ describe('symlink stream', function() {
     ], done);
   });
 
+  it('(windows) relative option is ignored when junctions are used', function(done) {
+    if (!isWindows) {
+      this.skip();
+      return;
+    }
+
+    var file = new File({
+      base: inputBase,
+      path: inputDirpath,
+      contents: null,
+      stat: {
+        isDirectory: isDirectory,
+      },
+    });
+
+    function assert(files) {
+      var stats = fs.statSync(outputDirpath);
+      var lstats = fs.lstatSync(outputDirpath);
+      var outputLink = fs.readlinkSync(outputDirpath);
+
+      expect(files.length).toEqual(1);
+      expect(files).toInclude(file);
+      expect(files[0].base).toEqual(outputBase, 'base should have changed');
+      expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
+      // When creating a junction, it seems Windows appends a separator
+      expect(outputLink).toEqual(inputDirpath + path.sep);
+      expect(stats.isDirectory()).toEqual(true);
+      expect(lstats.isDirectory()).toEqual(false);
+    }
+
+    pipe([
+      from.obj([file]),
+      vfs.symlink(outputBase, { useJunctions: true, relative: true }),
+      concat(assert),
+    ], done);
+  });
+
+  it('(windows) can create relative links for directories when junctions are disabled', function(done) {
+    if (!isWindows) {
+      this.skip();
+      return;
+    }
+
+    var file = new File({
+      base: inputBase,
+      path: inputDirpath,
+      contents: null,
+      stat: {
+        isDirectory: isDirectory,
+      },
+    });
+
+    function assert(files) {
+      var stats = fs.statSync(outputDirpath);
+      var lstats = fs.lstatSync(outputDirpath);
+      var outputLink = fs.readlinkSync(outputDirpath);
+
+      expect(files.length).toEqual(1);
+      expect(files).toInclude(file);
+      expect(files[0].base).toEqual(outputBase, 'base should have changed');
+      expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
+      expect(outputLink).toEqual(path.normalize('../fixtures/foo'));
+      expect(stats.isDirectory()).toEqual(true);
+      expect(lstats.isDirectory()).toEqual(false);
+    }
+
+    pipe([
+      from.obj([file]),
+      vfs.symlink(outputBase, { useJunctions: false, relative: true }),
+      concat(assert),
+    ], done);
+  });
+
   it('uses different modes for files and directories', function(done) {
     // Changing the mode of a file is not supported by node.js in Windows.
     if (isWindows) {