diff --git a/README.md b/README.md index 78055317..9838bbeb 100644 --- a/README.md +++ b/README.md @@ -211,10 +211,12 @@ Default: `false` ##### `options.useJunctions` When creating a symlink, whether or not a directory symlink should be created as a `junction`. +This option is only relevant on Windows and ignored elsewhere. Please refer to +the caveats section below. Type: `Boolean` -Default: `true` on Windows, `false` on all other platforms +Default: `true` ### `symlink(folder[, options])` @@ -239,14 +241,6 @@ Type: `String` Default: `process.cwd()` -##### `options.mode` - -The mode the symlinks should be created with. - -Type: `Number` - -Default: The `mode` of the input file (`file.stat.mode`) if any, or the process mode if the input file has no `mode` property. - ##### `options.dirMode` The mode the directory should be created with. @@ -274,10 +268,37 @@ Default: `false` ##### `options.useJunctions` Whether or not a directory symlink should be created as a `junction`. +This option is only relevant on Windows and ignored elsewhere. Please refer to +the caveats section below. Type: `Boolean` -Default: `true` on Windows, `false` on all other platforms +Default: `true` + +#### Symbolic links on Windows + +When creating symbolic links on Windows, we pass a `type` argument to Node's +`fs` module which specifies the kind of target we link to (one of `'file'`, +`'dir'` or `'junction'`). Specifically, this will be `'file'` when the target +is a regular file, `'junction'` if the target is a directory, or `'dir'` if +the target is a directory and the user overrides the `useJunctions` option +default. + +However if the user tries to make a "dangling" link, pointing to a non-existent +target, we won't be able to determine automatically which type we should use. +In these cases, `vinyl-fs` will behave slightly differently depending on +whether the dangling link is being created via `symlink()` or via `dest()`. + +For dangling links created via `symlink()`, the incoming vinyl represents the +target and so we will look to its stats to guess the desired type. In +particular, if `isDirectory()` returns false then we'll create a `'file'` type +link, otherwise we will create a `'junction'` or a `'dir'` type link depending +on the value of the `useJunctions` option. + +For dangling links created via `dest()`, the incoming vinyl represents the link, +typically read off disk via `src()` with the `resolveSymlinks` option set to +false. In this case we won't be able to make any reasonable guess as to the +type of link and we default to using `'file'`. [glob-stream]: https://github.com/gulpjs/glob-stream [node-glob]: https://github.com/isaacs/node-glob diff --git a/lib/dest/options.js b/lib/dest/options.js index d89d7281..a5fcd452 100644 --- a/lib/dest/options.js +++ b/lib/dest/options.js @@ -1,9 +1,5 @@ 'use strict'; -var os = require('os'); - -var isWindows = (os.platform() === 'win32'); - var config = { cwd: { type: 'string', @@ -34,14 +30,15 @@ var config = { default: false, }, // Symlink options - useJunctions: { - type: 'boolean', - default: isWindows, - }, relativeSymlinks: { type: 'boolean', default: false, }, + // This option is ignored on non-Windows platforms + useJunctions: { + type: 'boolean', + default: true, + }, }; module.exports = config; diff --git a/lib/dest/prepare.js b/lib/dest/prepare.js index afa09c10..a8068e44 100644 --- a/lib/dest/prepare.js +++ b/lib/dest/prepare.js @@ -11,22 +11,23 @@ function prepareWrite(folderResolver, optResolver) { } function normalize(file, enc, cb) { - var mode = optResolver.resolve('mode', file); - var cwd = path.resolve(optResolver.resolve('cwd', file)); - var outFolderPath = folderResolver.resolve('outFolder', file); if (!outFolderPath) { return cb(new Error('Invalid output folder')); } + var cwd = path.resolve(optResolver.resolve('cwd', file)); var basePath = path.resolve(cwd, outFolderPath); var writePath = path.resolve(basePath, file.relative); // Wire up new properties - file.stat = (file.stat || new fs.Stats()); - file.stat.mode = mode; file.cwd = cwd; file.base = basePath; file.path = writePath; + if (!file.isSymbolic()) { + var mode = optResolver.resolve('mode', file); + file.stat = (file.stat || new fs.Stats()); + file.stat.mode = mode; + } cb(null, file); } diff --git a/lib/dest/write-contents/index.js b/lib/dest/write-contents/index.js index 439a47b3..c0c9eff3 100644 --- a/lib/dest/write-contents/index.js +++ b/lib/dest/write-contents/index.js @@ -13,7 +13,7 @@ function writeContents(optResolver) { function writeFile(file, enc, callback) { // Write it as a symlink - if (file.symlink) { + if (file.isSymbolic()) { return writeSymbolicLink(file, optResolver, onWritten); } diff --git a/lib/dest/write-contents/write-symbolic-link.js b/lib/dest/write-contents/write-symbolic-link.js index ddd4c92d..d8f3216d 100644 --- a/lib/dest/write-contents/write-symbolic-link.js +++ b/lib/dest/write-contents/write-symbolic-link.js @@ -1,42 +1,74 @@ 'use strict'; +var os = require('os'); var path = require('path'); var fo = require('../../file-operations'); +var isWindows = (os.platform() === 'win32'); + function writeSymbolicLink(file, optResolver, onWritten) { - 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, JetBrains 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 = optResolver.resolve('useJunctions', file); - - var symDirType = useJunctions ? 'junction' : 'dir'; - var symType = isDirectory ? symDirType : 'file'; + if (!file.symlink) { + return onWritten(new Error('Missing symlink property on symbolic vinyl')); + } + var isRelative = optResolver.resolve('relativeSymlinks', file); + var flag = optResolver.resolve('flag', file); - // This is done after prepare() to use the adjusted file.base property - if (isRelative && symType !== 'junction') { - file.symlink = path.relative(file.base, file.symlink); + if (!isWindows) { + // On non-Windows, just use 'file' + return createLinkWithType('file'); } - var flag = optResolver.resolve('flag', file); + fo.reflectStat(file.symlink, file, onReflect); + + function onReflect(statErr) { + if (statErr && statErr.code !== 'ENOENT') { + return onWritten(statErr); + } + + // 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, JetBrains 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 opts = { - flag: flag, - type: symType, - }; + // This function is Windows only, so we don't need to check again + var useJunctions = optResolver.resolve('useJunctions', file); - fo.symlink(file.symlink, file.path, opts, onWritten); + var dirType = useJunctions ? 'junction' : 'dir'; + // Dangling links are always 'file' + var type = !statErr && file.isDirectory() ? dirType : 'file'; + + createLinkWithType(type); + } + + function createLinkWithType(type) { + // This is done after prepare() to use the adjusted file.base property + if (isRelative && type !== 'junction') { + file.symlink = path.relative(file.base, file.symlink); + } + + var opts = { + flag: flag, + type: type, + }; + fo.symlink(file.symlink, file.path, opts, onSymlink); + + function onSymlink(symlinkErr) { + if (symlinkErr) { + return onWritten(symlinkErr); + } + + fo.reflectLinkStat(file.path, file, onWritten); + } + } } module.exports = writeSymbolicLink; diff --git a/lib/file-operations.js b/lib/file-operations.js index 0072e314..7dd25d7b 100644 --- a/lib/file-operations.js +++ b/lib/file-operations.js @@ -155,6 +155,34 @@ function isOwner(fsStat) { return true; } +function reflectStat(path, file, callback) { + // Set file.stat to the reflect current state on disk + fs.stat(path, onStat); + + function onStat(statErr, stat) { + if (statErr) { + return callback(statErr); + } + + file.stat = stat; + callback(); + } +} + +function reflectLinkStat(path, file, callback) { + // Set file.stat to the reflect current state on disk + fs.lstat(path, onLstat); + + function onLstat(lstatErr, stat) { + if (lstatErr) { + return callback(lstatErr); + } + + file.stat = stat; + callback(); + } +} + function updateMetadata(fd, file, callback) { fs.fstat(fd, onStat); @@ -413,6 +441,8 @@ module.exports = { getTimesDiff: getTimesDiff, getOwnerDiff: getOwnerDiff, isOwner: isOwner, + reflectStat: reflectStat, + reflectLinkStat: reflectLinkStat, updateMetadata: updateMetadata, symlink: symlink, writeFile: writeFile, diff --git a/lib/src/resolve-symlinks.js b/lib/src/resolve-symlinks.js index 5e322c2d..d77f912f 100644 --- a/lib/src/resolve-symlinks.js +++ b/lib/src/resolve-symlinks.js @@ -1,23 +1,21 @@ 'use strict'; var through = require('through2'); -var fs = require('graceful-fs'); +var fo = require('../file-operations'); function resolveSymlinks(optResolver) { // A stat property is exposed on file objects as a (wanted) side effect function resolveFile(file, enc, callback) { - fs.lstat(file.path, onStat); + fo.reflectLinkStat(file.path, file, onReflect); - function onStat(statErr, stat) { + function onReflect(statErr) { if (statErr) { return callback(statErr); } - file.stat = stat; - - if (!stat.isSymbolicLink()) { + if (!file.stat.isSymbolicLink()) { return callback(null, file); } @@ -27,8 +25,8 @@ function resolveSymlinks(optResolver) { return callback(null, file); } - // Recurse to get real file stat - fs.stat(file.path, onStat); + // Get target's stats + fo.reflectStat(file.path, file, onReflect); } } diff --git a/lib/symlink/link-file.js b/lib/symlink/link-file.js index 13ea2f58..8f832139 100644 --- a/lib/symlink/link-file.js +++ b/lib/symlink/link-file.js @@ -1,52 +1,81 @@ 'use strict'; +var os = require('os'); var path = require('path'); var through = require('through2'); var fo = require('../file-operations'); +var isWindows = (os.platform() === 'win32'); + function linkStream(optResolver) { function linkFile(file, enc, callback) { - 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, JetBrains 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 = optResolver.resolve('useJunctions', file); - - var symDirType = useJunctions ? 'junction' : 'dir'; - var symType = isDirectory ? symDirType : 'file'; var isRelative = optResolver.resolve('relativeSymlinks', file); + var flag = optResolver.resolve('flag', file); - // This is done after prepare() to use the adjusted file.base property - if (isRelative && symType !== 'junction') { - file.symlink = path.relative(file.base, file.symlink); + if (!isWindows) { + // On non-Windows, just use 'file' + return createLinkWithType('file'); } - var flag = optResolver.resolve('flag', file); + fo.reflectStat(file.symlink, file, onReflectTarget); + + function onReflectTarget(statErr) { + if (statErr && statErr.code !== 'ENOENT') { + return onWritten(statErr); + } + // If target doesn't exist, the vinyl will still carry the target stats. + // Let's use those to determine which kind of dangling link to create. + + // 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, JetBrains 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. + + // This function is Windows only, so we don't need to check again + var useJunctions = optResolver.resolve('useJunctions', file); + + var dirType = useJunctions ? 'junction' : 'dir'; + var type = !statErr && file.isDirectory() ? dirType : 'file'; - var opts = { - flag: flag, - type: symType, - }; + createLinkWithType(type); + } + + function createLinkWithType(type) { + // This is done after prepare() to use the adjusted file.base property + if (isRelative && type !== 'junction') { + file.symlink = path.relative(file.base, file.symlink); + } - fo.symlink(file.symlink, file.path, opts, onSymlink); + var opts = { + flag: flag, + type: type, + }; + fo.symlink(file.symlink, file.path, opts, onSymlink); + } function onSymlink(symlinkErr) { if (symlinkErr) { return callback(symlinkErr); } + fo.reflectLinkStat(file.path, file, onReflectLink); + } + + function onReflectLink(reflectErr) { + if (reflectErr) { + return callback(reflectErr); + } + callback(null, file); } } diff --git a/lib/symlink/options.js b/lib/symlink/options.js index eaa49982..102d88a4 100644 --- a/lib/symlink/options.js +++ b/lib/symlink/options.js @@ -1,13 +1,10 @@ 'use strict'; -var os = require('os'); - -var isWindows = (os.platform() === 'win32'); - var config = { + // This option is ignored on non-Windows platforms useJunctions: { type: 'boolean', - default: isWindows, + default: true, }, relativeSymlinks: { type: 'boolean', @@ -17,12 +14,6 @@ var config = { type: 'string', default: process.cwd, }, - mode: { - type: 'number', - default: function(file) { - return file.stat ? file.stat.mode : null; - }, - }, dirMode: { type: 'number', }, diff --git a/lib/symlink/prepare.js b/lib/symlink/prepare.js index 8c1e5e4e..0510d655 100644 --- a/lib/symlink/prepare.js +++ b/lib/symlink/prepare.js @@ -1,7 +1,5 @@ 'use strict'; -// TODO: currently a copy-paste of prepareWrite but should be customized - var path = require('path'); var fs = require('graceful-fs'); @@ -13,7 +11,6 @@ function prepareSymlink(folderResolver, optResolver) { } function normalize(file, enc, cb) { - var mode = optResolver.resolve('mode', file); var cwd = path.resolve(optResolver.resolve('cwd', file)); var outFolderPath = folderResolver.resolve('outFolder', file); @@ -24,12 +21,16 @@ function prepareSymlink(folderResolver, optResolver) { var writePath = path.resolve(basePath, file.relative); // Wire up new properties + // Note: keep the target stats for now, we may need them in link-file file.stat = (file.stat || new fs.Stats()); - file.stat.mode = mode; file.cwd = cwd; file.base = basePath; + // This is the path we are linking *TO* file.symlink = file.path; file.path = writePath; + // We have to set contents to null for a link + // Otherwise `isSymbolic()` returns false + file.contents = null; cb(null, file); } diff --git a/test/dest-modes.js b/test/dest-modes.js index dc7aa894..5f315a0e 100644 --- a/test/dest-modes.js +++ b/test/dest-modes.js @@ -12,7 +12,7 @@ var statMode = require('./utils/stat-mode'); var mockError = require('./utils/mock-error'); var isWindows = require('./utils/is-windows'); var applyUmask = require('./utils/apply-umask'); -var isDirectory = require('./utils/is-directory-mock'); +var always = require('./utils/always'); var testConstants = require('./utils/test-constants'); var from = miss.from; @@ -135,7 +135,7 @@ describe('.dest() with custom modes', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), mode: expectedMode, }, }); @@ -164,7 +164,7 @@ describe('.dest() with custom modes', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), mode: expectedMode, }, }); @@ -251,7 +251,7 @@ describe('.dest() with custom modes', function() { base: inputBase, path: outputDirpath, stat: { - isDirectory: isDirectory, + isDirectory: always(true), mode: startMode, }, }); @@ -259,7 +259,7 @@ describe('.dest() with custom modes', function() { base: inputBase, path: outputDirpath, stat: { - isDirectory: isDirectory, + isDirectory: always(true), mode: expectedMode, }, }); diff --git a/test/dest-symlinks.js b/test/dest-symlinks.js index 29d02804..b1ba7ea9 100644 --- a/test/dest-symlinks.js +++ b/test/dest-symlinks.js @@ -11,7 +11,7 @@ var vfs = require('../'); var cleanup = require('./utils/cleanup'); var isWindows = require('./utils/is-windows'); -var isDirectory = require('./utils/is-directory-mock'); +var always = require('./utils/always'); var testConstants = require('./utils/test-constants'); var from = miss.from; @@ -25,6 +25,11 @@ var outputPath = testConstants.outputPath; var inputDirpath = testConstants.inputDirpath; var outputDirpath = testConstants.outputDirpath; var contents = testConstants.contents; +// For not-exists tests +var neInputBase = testConstants.neInputBase; +var neOutputBase = testConstants.neOutputBase; +var neInputDirpath = testConstants.neInputDirpath; +var neOutputDirpath = testConstants.neOutputDirpath; var clean = cleanup(outputBase); @@ -33,11 +38,14 @@ describe('.dest() with symlinks', function() { beforeEach(clean); afterEach(clean); - it('creates symlinks when the `symlink` attribute is set on the file', function(done) { + it('creates symlinks when `file.isSymbolic()` is true', function(done) { var file = new File({ base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false @@ -49,6 +57,7 @@ describe('.dest() with symlinks', function() { expect(files.length).toEqual(1); expect(file.symlink).toEqual(symlink); expect(files[0].symlink).toEqual(symlink); + expect(files[0].isSymbolic()).toBe(true); expect(files[0].path).toEqual(outputPath); } @@ -59,11 +68,88 @@ describe('.dest() with symlinks', function() { ], done); }); + it('does not create symlinks when `file.isSymbolic()` is false', function(done) { + var file = new File({ + base: inputBase, + path: inputPath, + contents: null, + stat: { + isSymbolicLink: always(false), + }, + }); + + // `src()` adds this side-effect with `resolveSymlinks` option set to false + file.symlink = inputPath; + + function assert(files) { + var symlinkExists = fs.existsSync(outputPath); + + expect(files.length).toEqual(1); + expect(symlinkExists).toBe(false); + } + + pipe([ + from.obj([file]), + vfs.dest(outputBase), + concat(assert), + ], done); + }); + + it('errors if missing a `.symlink` property', function(done) { + var file = new File({ + base: inputBase, + path: inputPath, + contents: null, + stat: { + isSymbolicLink: always(true), + }, + }); + + function assert(err) { + expect(err).toExist(); + expect(err.message).toEqual('Missing symlink property on symbolic vinyl'); + done(); + } + + pipe([ + from.obj([file]), + vfs.dest(outputBase), + ], assert); + }); + + it('emits Vinyl files that are (still) symbolic', function(done) { + var file = new File({ + base: inputBase, + path: inputPath, + contents: null, + stat: { + isSymbolicLink: always(true), + }, + }); + + // `src()` adds this side-effect with `resolveSymlinks` option set to false + file.symlink = inputPath; + + function assert(files) { + expect(files.length).toEqual(1); + expect(files[0].isSymbolic()).toEqual(true); + } + + pipe([ + from.obj([file]), + vfs.dest(outputBase), + concat(assert), + ], done); + }); + it('can create relative links', function(done) { var file = new File({ base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false @@ -74,6 +160,7 @@ describe('.dest() with symlinks', function() { expect(files.length).toEqual(1); expect(outputLink).toEqual(path.normalize('../fixtures/test.txt')); + expect(files[0].isSymbolic()).toBe(true); } pipe([ @@ -94,7 +181,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -130,7 +217,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -167,7 +254,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -203,7 +290,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -245,7 +332,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -270,6 +357,83 @@ describe('.dest() with symlinks', function() { ], done); }); + it('(*nix) receives a virtual symbolic directory and creates a symlink', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: neInputBase, + path: neInputDirpath, + contents: null, + stat: { + isSymbolicLink: always(true), + }, + }); + + // `src()` adds this side-effect with `resolveSymlinks` option set to false + file.symlink = neInputDirpath; + + function assert(files) { + var lstats = fs.lstatSync(neOutputDirpath); + var outputLink = fs.readlinkSync(neOutputDirpath); + var linkTargetExists = fs.existsSync(outputLink); + + expect(files.length).toEqual(1); + expect(outputLink).toEqual(neInputDirpath); + expect(linkTargetExists).toEqual(false); + expect(lstats.isSymbolicLink()).toEqual(true); + } + + pipe([ + // This could also be from a different Vinyl adapter + from.obj([file]), + vfs.dest(neOutputBase), + concat(assert), + ], done); + }); + + // There's no way to determine the proper type of link to create with a dangling link + // So we just create a 'file' type symlink + // There's also no real way to test the type that was created + it('(windows) receives a virtual symbolic directory and creates a symlink', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: neInputBase, + path: neInputDirpath, + contents: null, + stat: { + isSymbolicLink: always(true), + }, + }); + + // `src()` adds this side-effect with `resolveSymlinks` option set to false + file.symlink = neInputDirpath; + + function assert(files) { + var lstats = fs.lstatSync(neOutputDirpath); + var outputLink = fs.readlinkSync(neOutputDirpath); + var linkTargetExists = fs.existsSync(outputLink); + + expect(files.length).toEqual(1); + expect(outputLink).toEqual(neInputDirpath); + expect(linkTargetExists).toEqual(false); + expect(lstats.isSymbolicLink()).toEqual(true); + } + + pipe([ + // This could also be from a different Vinyl adapter + from.obj([file]), + vfs.dest(neOutputBase), + concat(assert), + ], done); + }); + it('(windows) relativeSymlinks option is ignored when junctions are used', function(done) { if (!isWindows) { this.skip(); @@ -281,7 +445,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -317,6 +481,9 @@ describe('.dest() with symlinks', function() { base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false @@ -348,7 +515,7 @@ describe('.dest() with symlinks', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isSymbolicLink: always(true), }, }); @@ -383,6 +550,9 @@ describe('.dest() with symlinks', function() { base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false @@ -414,6 +584,9 @@ describe('.dest() with symlinks', function() { base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false @@ -444,6 +617,9 @@ describe('.dest() with symlinks', function() { base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false @@ -479,6 +655,9 @@ describe('.dest() with symlinks', function() { base: inputBase, path: inputPath, contents: null, + stat: { + isSymbolicLink: always(true), + }, }); // `src()` adds this side-effect with `resolveSymlinks` option set to false diff --git a/test/dest.js b/test/dest.js index e92dc569..58568414 100644 --- a/test/dest.js +++ b/test/dest.js @@ -14,7 +14,7 @@ var statMode = require('./utils/stat-mode'); var mockError = require('./utils/mock-error'); var applyUmask = require('./utils/apply-umask'); var testStreams = require('./utils/test-streams'); -var isDirectory = require('./utils/is-directory-mock'); +var always = require('./utils/always'); var testConstants = require('./utils/test-constants'); var from = miss.from; @@ -368,7 +368,7 @@ describe('.dest()', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -828,7 +828,7 @@ describe('.dest()', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -851,7 +851,7 @@ describe('.dest()', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), mode: applyUmask('000'), }, }); @@ -876,7 +876,7 @@ describe('.dest()', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); diff --git a/test/file-operations.js b/test/file-operations.js index aa80a767..4af7c68e 100644 --- a/test/file-operations.js +++ b/test/file-operations.js @@ -31,6 +31,8 @@ var getOwnerDiff = fo.getOwnerDiff; var isValidUnixId = fo.isValidUnixId; var isFatalOverwriteError = fo.isFatalOverwriteError; var isFatalUnlinkError = fo.isFatalUnlinkError; +var reflectStat = fo.reflectStat; +var reflectLinkStat = fo.reflectLinkStat; var updateMetadata = fo.updateMetadata; var createWriteStream = fo.createWriteStream; @@ -41,7 +43,9 @@ var string = testStreams.string; var outputBase = testConstants.outputBase; var inputPath = testConstants.inputPath; +var neInputDirpath = testConstants.neInputDirpath; var outputPath = testConstants.outputPath; +var symlinkPath = testConstants.symlinkDirpath; var contents = testConstants.contents; var clean = cleanup(outputBase); @@ -893,6 +897,84 @@ describe('writeFile', function() { }); }); +describe('reflectStat', function() { + + beforeEach(clean); + afterEach(clean); + + beforeEach(function(done) { + mkdirp(outputBase, done); + }); + + it('passes the error if stat fails', function(done) { + + var file = new File(); + + reflectStat(neInputDirpath, file, function(err) { + expect(err).toExist(); + + done(); + }); + }); + + it('updates the vinyl with filesystem stats', function(done) { + var file = new File(); + + fs.symlinkSync(inputPath, symlinkPath); + + reflectStat(symlinkPath, file, function() { + // There appears to be a bug in the Windows implementation which causes + // the sync versions of stat and lstat to return unsigned 32-bit ints + // whilst the async versions returns signed 32-bit ints... This affects + // dev but possibly others as well? + fs.stat(symlinkPath, function(err, stat) { + expect(file.stat).toEqual(stat); + + done(); + }); + }); + }); +}); + +describe('reflectLinkStat', function() { + + beforeEach(clean); + afterEach(clean); + + beforeEach(function(done) { + mkdirp(outputBase, done); + }); + + it('passes the error if lstat fails', function(done) { + + var file = new File(); + + reflectLinkStat(neInputDirpath, file, function(err) { + expect(err).toExist(); + + done(); + }); + }); + + it('updates the vinyl with filesystem symbolic stats', function(done) { + var file = new File(); + + fs.symlinkSync(inputPath, symlinkPath); + + reflectLinkStat(symlinkPath, file, function() { + // There appears to be a bug in the Windows implementation which causes + // the sync versions of stat and lstat to return unsigned 32-bit ints + // whilst the async versions returns signed 32-bit ints... This affects + // dev but possibly others as well? + fs.lstat(symlinkPath, function(err, stat) { + expect(file.stat).toEqual(stat); + + done(); + }); + }); + }); +}); + describe('updateMetadata', function() { beforeEach(clean); diff --git a/test/integration.js b/test/integration.js index 44405d32..2f355555 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1,24 +1,36 @@ 'use strict'; +var os = require('os'); var path = require('path'); var fs = require('graceful-fs'); var miss = require('mississippi'); +var expect = require('expect'); var vfs = require('../'); var cleanup = require('./utils/cleanup'); +var isWindows = require('./utils/is-windows'); var testStreams = require('./utils/test-streams'); var testConstants = require('./utils/test-constants'); var pipe = miss.pipe; +var concat = miss.concat; var count = testStreams.count; var base = testConstants.outputBase; +var inputDirpath = testConstants.inputDirpath; +var outputDirpath = testConstants.outputDirpath; +var symlinkDirpath = testConstants.symlinkDirpath; var inputBase = path.join(base, './in/'); +var inputDirpath = testConstants.inputDirpath; +var outputDirpath = testConstants.outputDirpath; +var symlinkDirpath = testConstants.symlinkDirpath; var inputGlob = path.join(inputBase, './*.txt'); var outputBase = path.join(base, './out/'); +var outputSymlink = path.join(symlinkDirpath, './foo'); +var outputDirpathSymlink = path.join(outputDirpath, './foo'); var content = testConstants.content; var clean = cleanup(base); @@ -49,4 +61,133 @@ describe('integrations', function() { vfs.dest(outputBase), ], done); }); + + it('(*nix) sources a directory, creates a symlink and copies it', function(done) { + if (isWindows) { + this.skip(); + return; + } + + function assert(files) { + var symlinkResult = fs.readlinkSync(outputSymlink); + var destResult = fs.readlinkSync(outputDirpathSymlink); + + expect(symlinkResult).toEqual(inputDirpath); + expect(destResult).toEqual(inputDirpath); + expect(files[0].isSymbolic()).toBe(true); + expect(files[0].symlink).toEqual(inputDirpath); + } + + pipe([ + vfs.src(inputDirpath), + vfs.symlink(symlinkDirpath), + vfs.dest(outputDirpath), + concat(assert), + ], done); + }); + + it('(windows) sources a directory, creates a junction and copies it', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + function assert(files) { + // Junctions add an ending separator + var expected = inputDirpath + path.sep; + var symlinkResult = fs.readlinkSync(outputSymlink); + var destResult = fs.readlinkSync(outputDirpathSymlink); + + expect(symlinkResult).toEqual(expected); + expect(destResult).toEqual(expected); + expect(files[0].isSymbolic()).toBe(true); + expect(files[0].symlink).toEqual(inputDirpath); + } + + pipe([ + vfs.src(inputDirpath), + vfs.symlink(symlinkDirpath), + vfs.dest(outputDirpath), + concat(assert), + ], done); + }); + + it('(*nix) sources a symlink and copies it', function(done) { + if (isWindows) { + this.skip(); + return; + } + + fs.mkdirSync(base); + fs.mkdirSync(symlinkDirpath); + fs.symlinkSync(inputDirpath, outputSymlink); + + function assert(files) { + var destResult = fs.readlinkSync(outputDirpathSymlink); + + expect(destResult).toEqual(inputDirpath); + expect(files[0].isSymbolic()).toEqual(true); + expect(files[0].symlink).toEqual(inputDirpath); + } + + pipe([ + vfs.src(outputSymlink, { resolveSymlinks: false }), + vfs.dest(outputDirpath), + concat(assert), + ], done); + }); + + it('(windows) sources a directory symlink and copies it', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + fs.mkdirSync(base); + fs.mkdirSync(symlinkDirpath); + fs.symlinkSync(inputDirpath, outputSymlink, 'dir'); + + function assert(files) { + // 'dir' symlinks add an ending separator + var expected = inputDirpath + path.sep; + var destResult = fs.readlinkSync(outputDirpathSymlink); + + expect(destResult).toEqual(expected); + expect(files[0].isSymbolic()).toEqual(true); + expect(files[0].symlink).toEqual(inputDirpath); + } + + pipe([ + vfs.src(outputSymlink, { resolveSymlinks: false }), + vfs.dest(outputDirpath), + concat(assert), + ], done); + }); + + it('(windows) sources a junction and copies it', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + fs.mkdirSync(base); + fs.mkdirSync(symlinkDirpath); + fs.symlinkSync(inputDirpath, outputSymlink, 'junction'); + + function assert(files) { + // Junctions add an ending separator + var expected = inputDirpath + path.sep; + var destResult = fs.readlinkSync(outputDirpathSymlink); + + expect(destResult).toEqual(expected); + expect(files[0].isSymbolic()).toEqual(true); + expect(files[0].symlink).toEqual(inputDirpath); + } + + pipe([ + vfs.src(outputSymlink, { resolveSymlinks: false }), + vfs.dest(outputDirpath), + concat(assert), + ], done); + }); }); diff --git a/test/src-symlinks.js b/test/src-symlinks.js index 32056258..a4f61d75 100644 --- a/test/src-symlinks.js +++ b/test/src-symlinks.js @@ -127,7 +127,7 @@ describe('.src() with symlinks', function() { ], done); }); - it('recieves a file with symbolic link stats when resolveSymlinks is a function', function(done) { + it('receives a file with symbolic link stats when resolveSymlinks is a function', function(done) { function resolveSymlinks(file) { expect(file).toExist(); diff --git a/test/symlink.js b/test/symlink.js index 6dea8456..815ceb1a 100644 --- a/test/symlink.js +++ b/test/symlink.js @@ -10,11 +10,9 @@ var miss = require('mississippi'); var vfs = require('../'); var cleanup = require('./utils/cleanup'); -var statMode = require('./utils/stat-mode'); var isWindows = require('./utils/is-windows'); -var applyUmask = require('./utils/apply-umask'); var testStreams = require('./utils/test-streams'); -var isDirectory = require('./utils/is-directory-mock'); +var always = require('./utils/always'); var testConstants = require('./utils/test-constants'); var from = miss.from; @@ -111,6 +109,7 @@ describe('symlink stream', function() { expect(files[0].base).toEqual(outputBase, 'base should have changed'); expect(files[0].path).toEqual(outputPath, 'path should have changed'); expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); + expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); expect(outputLink).toEqual(inputPath); } @@ -144,6 +143,7 @@ describe('symlink stream', function() { expect(files[0].base).toEqual(outputBase, 'base should have changed'); expect(files[0].path).toEqual(outputPath, 'path should have changed'); expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); + expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); expect(outputLink).toEqual(inputPath); } @@ -154,7 +154,6 @@ describe('symlink stream', function() { ], done); }); - // TODO: test for modes it('creates a link for a file with buffered contents', function(done) { var file = new File({ base: inputBase, @@ -170,6 +169,7 @@ describe('symlink stream', function() { expect(files[0].base).toEqual(outputBase, 'base should have changed'); expect(files[0].path).toEqual(outputPath, 'path should have changed'); expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); + expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); expect(outputLink).toEqual(inputPath); } @@ -195,6 +195,7 @@ describe('symlink stream', function() { expect(files[0].base).toEqual(outputBase, 'base should have changed'); expect(files[0].path).toEqual(outputPath, 'path should have changed'); expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); + expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); expect(outputLink).toEqual(path.normalize('../fixtures/test.txt')); } @@ -220,6 +221,7 @@ describe('symlink stream', function() { expect(files[0].base).toEqual(outputBase, 'base should have changed'); expect(files[0].path).toEqual(outputPath, 'path should have changed'); expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); + expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); expect(outputLink).toEqual(inputPath); } @@ -230,6 +232,25 @@ describe('symlink stream', function() { ], done); }); + it('emits Vinyl objects that are symbolic', function(done) { + var file = new File({ + base: inputBase, + path: inputPath, + contents: null, + }); + + function assert(files) { + expect(files.length).toEqual(1); + expect(files[0].isSymbolic()).toEqual(true); + } + + pipe([ + from.obj([file]), + vfs.symlink(outputBase), + concat(assert), + ], done); + }); + it('(*nix) creates a link for a directory', function(done) { if (isWindows) { this.skip(); @@ -241,7 +262,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -278,7 +299,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -316,7 +337,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -353,7 +374,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -396,7 +417,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -433,7 +454,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -501,7 +522,7 @@ describe('symlink stream', function() { path: inputDirpath, contents: null, stat: { - isDirectory: isDirectory, + isDirectory: always(true), }, }); @@ -527,35 +548,6 @@ describe('symlink stream', function() { ], 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) { - this.skip(); - return; - } - - var dirMode = applyUmask('722'); - var fileMode = applyUmask('700'); - - var file = new File({ - base: inputBase, - path: inputPath, - contents: null, - }); - - function assert(files) { - expect(statMode(outputDirpath)).toEqual(dirMode); - // TODO: the file doesn't actually get the mode updated - expect(files[0].stat.mode).toEqual(fileMode); - } - - pipe([ - from.obj([file]), - vfs.symlink(outputDirpath, { mode: fileMode, dirMode: dirMode }), - concat(assert), - ], done); - }); - it('reports IO errors', function(done) { // Changing the mode of a file is not supported by node.js in Windows. // This test is skipped on Windows because we have to chmod the file to 0. diff --git a/test/utils/always.js b/test/utils/always.js new file mode 100644 index 00000000..02b6b6c7 --- /dev/null +++ b/test/utils/always.js @@ -0,0 +1,9 @@ +'use strict'; + +function always(value) { + return function() { + return value; + }; +} + +module.exports = always; diff --git a/test/utils/is-directory-mock.js b/test/utils/is-directory-mock.js deleted file mode 100644 index a71241df..00000000 --- a/test/utils/is-directory-mock.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -function isDirectory() { - return true; -} - -module.exports = isDirectory; diff --git a/test/utils/test-constants.js b/test/utils/test-constants.js index b235210a..26abe9d3 100644 --- a/test/utils/test-constants.js +++ b/test/utils/test-constants.js @@ -34,6 +34,11 @@ var symlinkMultiDirpath = path.join(outputBase, './test-multi-layer-symlink-dir' var symlinkMultiDirpathSecond = path.join(outputBase, './test-multi-layer-symlink-dir2'); var symlinkNestedFirst = path.join(outputBase, './test-multi-layer-symlink'); var symlinkNestedSecond = path.join(outputBase, './foo/baz-link.txt'); +// Paths that don't exist +var neInputBase = path.join(inputBase, './not-exists/'); +var neOutputBase = path.join(outputBase, './not-exists/'); +var neInputDirpath = path.join(neInputBase, './foo'); +var neOutputDirpath = path.join(neOutputBase, './foo'); // Used for contents of files var contents = 'Hello World!\n'; var sourcemapContents = '//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9maXh0dXJlcyIsIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIiLCJzb3VyY2VzIjpbIi4vZml4dHVyZXMiXSwic291cmNlc0NvbnRlbnQiOlsiSGVsbG8gV29ybGQhXG4iXX0='; @@ -62,6 +67,10 @@ module.exports = { symlinkMultiDirpathSecond: symlinkMultiDirpathSecond, symlinkNestedFirst: symlinkNestedFirst, symlinkNestedSecond: symlinkNestedSecond, + neInputBase: neInputBase, + neOutputBase: neOutputBase, + neInputDirpath: neInputDirpath, + neOutputDirpath: neOutputDirpath, contents: contents, sourcemapContents: sourcemapContents, };