Skip to content

Commit 40ac6e9

Browse files
committed
Breaking: Default to using Junctions on Windows (fixes #210) (#231)
1 parent c11dcbd commit 40ac6e9

File tree

4 files changed

+234
-6
lines changed

4 files changed

+234
-6
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ Returns a stream that accepts [vinyl] `File` objects, create a symbolic link (i.
233233
__Note: The file will be modified after being written to this stream.__
234234
- `cwd`, `base`, and `path` will be overwritten to match the folder.
235235

236+
__Note: On Windows, directory links are created using Junctions by default. Use the `useJunctions` option to disable this behavior.__
237+
236238
#### Options
237239

238240
- 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.
260262

261263
Type: `Boolean`
262264

263-
Default: `false`.
265+
Default: `false`
266+
267+
##### `options.useJunctions`
268+
269+
Whether or not a directory symlink should be created as a `junction`.
270+
271+
Type: `Boolean`
272+
273+
Default: `true` on Windows, `false` on all other platforms
264274

265275
##### other
266276

lib/dest/write-contents/write-stream.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function writeStream(file, onWritten) {
4141
file.contents.removeListener('error', onComplete);
4242

4343
// TODO: this is doing sync stuff & the callback seems unnecessary
44-
// TODO: do we really want to replace the contents stream or should we use a clone
44+
// TODO: Replace the contents stream or use a clone?
4545
readStream(file, complete);
4646

4747
function complete() {

lib/symlink/index.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
var path = require('path');
4+
var os = require('os');
45

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

1314
var boolean = valueOrFunction.boolean;
1415

16+
var isWindows = (os.platform() === 'win32');
17+
1518
function symlink(outFolder, opt) {
1619
if (!opt) {
1720
opt = {};
1821
}
1922

2023
function linkFile(file, enc, callback) {
2124
var srcPath = file.path;
22-
var symType = (file.isDirectory() ? 'dir' : 'file');
25+
26+
var isDirectory = file.isDirectory();
27+
28+
// This option provides a way to create a Junction instead of a
29+
// Directory symlink on Windows. This comes with the following caveats:
30+
// * NTFS Junctions cannot be relative.
31+
// * NTFS Junctions MUST be directories.
32+
// * NTFS Junctions must be on the same file system.
33+
// * Most products CANNOT detect a directory is a Junction:
34+
// This has the side effect of possibly having a whole directory
35+
// deleted when a product is deleting the Junction directory.
36+
// For example, IntelliJ product lines will delete the entire
37+
// contents of the TARGET directory because the product does not
38+
// realize it's a symlink as the JVM and Node return false for isSymlink.
39+
var useJunctions = koalas(boolean(opt.useJunctions, file), (isWindows && isDirectory));
40+
41+
var symDirType = useJunctions ? 'junction' : 'dir';
42+
var symType = isDirectory ? symDirType : 'file';
2343
var isRelative = koalas(boolean(opt.relative, file), false);
2444

2545
prepareWrite(outFolder, file, opt, onPrepare);
@@ -30,7 +50,7 @@ function symlink(outFolder, opt) {
3050
}
3151

3252
// This is done inside prepareWrite to use the adjusted file.base property
33-
if (isRelative) {
53+
if (isRelative && !useJunctions) {
3454
srcPath = path.relative(file.base, srcPath);
3555
}
3656

test/symlink.js

Lines changed: 200 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,12 @@ describe('symlink stream', function() {
225225
], done);
226226
});
227227

228-
it('creates a link for a directory', function(done) {
228+
it('(*nix) creates a link for a directory', function(done) {
229+
if (isWindows) {
230+
this.skip();
231+
return;
232+
}
233+
229234
var file = new File({
230235
base: inputBase,
231236
path: inputDirpath,
@@ -256,7 +261,127 @@ describe('symlink stream', function() {
256261
], done);
257262
});
258263

259-
it('can create relative links for directories', function(done) {
264+
it('(windows) creates a junction for a directory', function(done) {
265+
if (!isWindows) {
266+
this.skip();
267+
return;
268+
}
269+
270+
var file = new File({
271+
base: inputBase,
272+
path: inputDirpath,
273+
contents: null,
274+
stat: {
275+
isDirectory: isDirectory,
276+
},
277+
});
278+
279+
function assert(files) {
280+
var stats = fs.statSync(outputDirpath);
281+
var lstats = fs.lstatSync(outputDirpath);
282+
var outputLink = fs.readlinkSync(outputDirpath);
283+
284+
expect(files.length).toEqual(1);
285+
expect(files).toInclude(file);
286+
expect(files[0].base).toEqual(outputBase, 'base should have changed');
287+
expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
288+
// When creating a junction, it seems Windows appends a separator
289+
expect(outputLink).toEqual(inputDirpath + path.sep);
290+
expect(stats.isDirectory()).toEqual(true);
291+
expect(lstats.isDirectory()).toEqual(false);
292+
}
293+
294+
pipe([
295+
from.obj([file]),
296+
vfs.symlink(outputBase),
297+
concat(assert),
298+
], done);
299+
});
300+
301+
it('(windows) options can disable junctions for a directory', function(done) {
302+
if (!isWindows) {
303+
this.skip();
304+
return;
305+
}
306+
307+
var file = new File({
308+
base: inputBase,
309+
path: inputDirpath,
310+
contents: null,
311+
stat: {
312+
isDirectory: isDirectory,
313+
},
314+
});
315+
316+
function assert(files) {
317+
var stats = fs.statSync(outputDirpath);
318+
var lstats = fs.lstatSync(outputDirpath);
319+
var outputLink = fs.readlinkSync(outputDirpath);
320+
321+
expect(files.length).toEqual(1);
322+
expect(files).toInclude(file);
323+
expect(files[0].base).toEqual(outputBase, 'base should have changed');
324+
expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
325+
expect(outputLink).toEqual(inputDirpath);
326+
expect(stats.isDirectory()).toEqual(true);
327+
expect(lstats.isDirectory()).toEqual(false);
328+
}
329+
330+
pipe([
331+
from.obj([file]),
332+
vfs.symlink(outputBase, { useJunctions: false }),
333+
concat(assert),
334+
], done);
335+
});
336+
337+
it('(windows) options can disable junctions for a directory (as a function)', function(done) {
338+
if (!isWindows) {
339+
this.skip();
340+
return;
341+
}
342+
343+
var file = new File({
344+
base: inputBase,
345+
path: inputDirpath,
346+
contents: null,
347+
stat: {
348+
isDirectory: isDirectory,
349+
},
350+
});
351+
352+
function useJunctions(f) {
353+
expect(f).toExist();
354+
expect(f).toBe(file);
355+
return false;
356+
}
357+
358+
function assert(files) {
359+
var stats = fs.statSync(outputDirpath);
360+
var lstats = fs.lstatSync(outputDirpath);
361+
var outputLink = fs.readlinkSync(outputDirpath);
362+
363+
expect(files.length).toEqual(1);
364+
expect(files).toInclude(file);
365+
expect(files[0].base).toEqual(outputBase, 'base should have changed');
366+
expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
367+
expect(outputLink).toEqual(inputDirpath);
368+
expect(stats.isDirectory()).toEqual(true);
369+
expect(lstats.isDirectory()).toEqual(false);
370+
}
371+
372+
pipe([
373+
from.obj([file]),
374+
vfs.symlink(outputBase, { useJunctions: useJunctions }),
375+
concat(assert),
376+
], done);
377+
});
378+
379+
it('(*nix) can create relative links for directories', function(done) {
380+
if (isWindows) {
381+
this.skip();
382+
return;
383+
}
384+
260385
var file = new File({
261386
base: inputBase,
262387
path: inputDirpath,
@@ -287,6 +412,79 @@ describe('symlink stream', function() {
287412
], done);
288413
});
289414

415+
it('(windows) relative option is ignored when junctions are used', function(done) {
416+
if (!isWindows) {
417+
this.skip();
418+
return;
419+
}
420+
421+
var file = new File({
422+
base: inputBase,
423+
path: inputDirpath,
424+
contents: null,
425+
stat: {
426+
isDirectory: isDirectory,
427+
},
428+
});
429+
430+
function assert(files) {
431+
var stats = fs.statSync(outputDirpath);
432+
var lstats = fs.lstatSync(outputDirpath);
433+
var outputLink = fs.readlinkSync(outputDirpath);
434+
435+
expect(files.length).toEqual(1);
436+
expect(files).toInclude(file);
437+
expect(files[0].base).toEqual(outputBase, 'base should have changed');
438+
expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
439+
// When creating a junction, it seems Windows appends a separator
440+
expect(outputLink).toEqual(inputDirpath + path.sep);
441+
expect(stats.isDirectory()).toEqual(true);
442+
expect(lstats.isDirectory()).toEqual(false);
443+
}
444+
445+
pipe([
446+
from.obj([file]),
447+
vfs.symlink(outputBase, { useJunctions: true, relative: true }),
448+
concat(assert),
449+
], done);
450+
});
451+
452+
it('(windows) can create relative links for directories when junctions are disabled', function(done) {
453+
if (!isWindows) {
454+
this.skip();
455+
return;
456+
}
457+
458+
var file = new File({
459+
base: inputBase,
460+
path: inputDirpath,
461+
contents: null,
462+
stat: {
463+
isDirectory: isDirectory,
464+
},
465+
});
466+
467+
function assert(files) {
468+
var stats = fs.statSync(outputDirpath);
469+
var lstats = fs.lstatSync(outputDirpath);
470+
var outputLink = fs.readlinkSync(outputDirpath);
471+
472+
expect(files.length).toEqual(1);
473+
expect(files).toInclude(file);
474+
expect(files[0].base).toEqual(outputBase, 'base should have changed');
475+
expect(files[0].path).toEqual(outputDirpath, 'path should have changed');
476+
expect(outputLink).toEqual(path.normalize('../fixtures/foo'));
477+
expect(stats.isDirectory()).toEqual(true);
478+
expect(lstats.isDirectory()).toEqual(false);
479+
}
480+
481+
pipe([
482+
from.obj([file]),
483+
vfs.symlink(outputBase, { useJunctions: false, relative: true }),
484+
concat(assert),
485+
], done);
486+
});
487+
290488
it('uses different modes for files and directories', function(done) {
291489
// Changing the mode of a file is not supported by node.js in Windows.
292490
if (isWindows) {

0 commit comments

Comments
 (0)