Skip to content

Commit

Permalink
Breaking: Default to using Junctions on Windows (fixes #210) (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
phated authored May 1, 2017
1 parent 6b86c6e commit a5d03b2
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 6 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/dest/write-contents/write-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
24 changes: 22 additions & 2 deletions lib/symlink/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var path = require('path');
var os = require('os');

var fs = require('graceful-fs');
var through2 = require('through2');
Expand All @@ -12,14 +13,33 @@ var prepareWrite = require('../prepare-write');

var boolean = valueOrFunction.boolean;

var isWindows = (os.platform() === 'win32');

function symlink(outFolder, opt) {
if (!opt) {
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);
Expand All @@ -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);
}

Expand Down
202 changes: 200 additions & 2 deletions test/symlink.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit a5d03b2

Please sign in to comment.