├── .npmrc ├── test ├── fixtures │ ├── test-symlink-dir │ ├── watch │ │ └── test.txt │ ├── generic │ │ ├── run.dmc │ │ └── test.dmc │ ├── pages │ │ ├── a.hbs │ │ ├── b.hbs │ │ └── c.hbs │ ├── test-symlink │ ├── test.coffee │ ├── wow │ │ └── suchempty │ ├── copy │ │ └── example.txt │ ├── templates │ │ ├── a.tmpl │ │ ├── b.tmpl │ │ └── c.tmpl │ ├── vinyl │ │ ├── test-symlink │ │ ├── test-symlink-dir │ │ ├── test.coffee │ │ ├── wow │ │ │ └── suchempty │ │ ├── bom-utf8.txt │ │ ├── bom-utf16be.txt │ │ └── bom-utf16le.txt │ ├── data │ │ ├── a.json │ │ ├── b.json │ │ ├── c.json │ │ ├── test.json │ │ ├── data.json │ │ └── alert.json │ ├── bom-utf8.txt │ ├── helpers │ │ ├── a.js │ │ ├── b.js │ │ ├── c.js │ │ └── obj.js │ ├── posts │ │ ├── a.txt │ │ ├── b.txt │ │ └── c.txt │ ├── front-matter │ │ ├── lang-yaml.md │ │ ├── autodetect-yaml.md │ │ └── autodetect-no-lang.md │ ├── bom-utf16be.txt │ ├── bom-utf16le.txt │ └── noext │ │ └── license ├── vfs │ ├── fixtures │ │ ├── test.txt │ │ ├── foo │ │ │ └── bar │ │ │ │ └── baz.txt │ │ ├── not-owned │ │ │ └── not-owned.txt │ │ ├── bom-utf8.txt │ │ ├── bom-utf16be.txt │ │ └── bom-utf16le.txt │ ├── utils │ │ ├── is-windows.js │ │ ├── always.js │ │ ├── mock-error.js │ │ ├── apply-umask.js │ │ ├── stat-mode.js │ │ ├── cleanup.js │ │ ├── break-prototype.js │ │ ├── test-streams.js │ │ └── test-constants.js │ ├── dest-owner.js │ ├── not-owned.js │ ├── src-symlinks.js │ ├── dest-times.js │ ├── integration.js │ ├── dest-modes.js │ ├── src.js │ ├── dest-symlinks.js │ ├── dest.js │ └── symlink.js ├── support │ ├── ignore.js │ ├── index.js │ └── spy.js ├── app.copy.js ├── handlers.js ├── app.src.js └── collection.src.js ├── .gitattributes ├── .travis.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── .verb.md ├── package.json ├── utils.js ├── .eslintrc.json ├── index.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/fixtures/test-symlink-dir: -------------------------------------------------------------------------------- 1 | wow -------------------------------------------------------------------------------- /test/fixtures/watch/test.txt: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/fixtures/generic/run.dmc: -------------------------------------------------------------------------------- 1 | # run.dmc -------------------------------------------------------------------------------- /test/fixtures/pages/a.hbs: -------------------------------------------------------------------------------- 1 |

-------------------------------------------------------------------------------- /test/fixtures/pages/b.hbs: -------------------------------------------------------------------------------- 1 |

-------------------------------------------------------------------------------- /test/fixtures/pages/c.hbs: -------------------------------------------------------------------------------- 1 |

-------------------------------------------------------------------------------- /test/fixtures/test-symlink: -------------------------------------------------------------------------------- 1 | test.coffee -------------------------------------------------------------------------------- /test/fixtures/test.coffee: -------------------------------------------------------------------------------- 1 | Hello world! -------------------------------------------------------------------------------- /test/fixtures/wow/suchempty: -------------------------------------------------------------------------------- 1 | suchempty -------------------------------------------------------------------------------- /test/fixtures/copy/example.txt: -------------------------------------------------------------------------------- 1 | Hello world! -------------------------------------------------------------------------------- /test/fixtures/generic/test.dmc: -------------------------------------------------------------------------------- 1 | # test.dmc -------------------------------------------------------------------------------- /test/fixtures/templates/a.tmpl: -------------------------------------------------------------------------------- 1 | <%= name %> -------------------------------------------------------------------------------- /test/fixtures/templates/b.tmpl: -------------------------------------------------------------------------------- 1 | <%= name %> -------------------------------------------------------------------------------- /test/fixtures/templates/c.tmpl: -------------------------------------------------------------------------------- 1 | <%= name %> -------------------------------------------------------------------------------- /test/fixtures/vinyl/test-symlink: -------------------------------------------------------------------------------- 1 | test.coffee -------------------------------------------------------------------------------- /test/fixtures/vinyl/test-symlink-dir: -------------------------------------------------------------------------------- 1 | wow -------------------------------------------------------------------------------- /test/fixtures/vinyl/test.coffee: -------------------------------------------------------------------------------- 1 | Hello world! -------------------------------------------------------------------------------- /test/fixtures/vinyl/wow/suchempty: -------------------------------------------------------------------------------- 1 | suchempty -------------------------------------------------------------------------------- /test/vfs/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/vfs/fixtures/foo/bar/baz.txt: -------------------------------------------------------------------------------- 1 | symlink works 2 | -------------------------------------------------------------------------------- /test/fixtures/data/a.json: -------------------------------------------------------------------------------- 1 | { 2 | "one": {"a": "aaa"} 3 | } -------------------------------------------------------------------------------- /test/fixtures/data/b.json: -------------------------------------------------------------------------------- 1 | { 2 | "two": {"b": "bbb"} 3 | } -------------------------------------------------------------------------------- /test/vfs/fixtures/not-owned/not-owned.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/fixtures/data/c.json: -------------------------------------------------------------------------------- 1 | { 2 | "three": {"c": "ccc"} 3 | } -------------------------------------------------------------------------------- /test/fixtures/bom-utf8.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-8 with BOM. 𝌆 2 | -------------------------------------------------------------------------------- /test/fixtures/helpers/a.js: -------------------------------------------------------------------------------- 1 | module.exports = function aaa() { 2 | 3 | }; -------------------------------------------------------------------------------- /test/fixtures/helpers/b.js: -------------------------------------------------------------------------------- 1 | module.exports = function bbb() { 2 | 3 | }; -------------------------------------------------------------------------------- /test/fixtures/helpers/c.js: -------------------------------------------------------------------------------- 1 | module.exports = function ccc() { 2 | 3 | }; -------------------------------------------------------------------------------- /test/fixtures/data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpha": "one", 3 | "beta": "two" 4 | } -------------------------------------------------------------------------------- /test/fixtures/posts/a.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: AAA 3 | --- 4 | This is <%= title %> -------------------------------------------------------------------------------- /test/fixtures/posts/b.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: BBB 3 | --- 4 | This is <%= title %> -------------------------------------------------------------------------------- /test/fixtures/posts/c.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: CCC 3 | --- 4 | This is <%= title %> -------------------------------------------------------------------------------- /test/fixtures/vinyl/bom-utf8.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-8 with BOM. 𝌆 2 | -------------------------------------------------------------------------------- /test/vfs/fixtures/bom-utf8.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-8 with BOM. 𝌆 2 | -------------------------------------------------------------------------------- /test/fixtures/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "Whoa, I should be at the root!" 3 | } -------------------------------------------------------------------------------- /test/fixtures/front-matter/lang-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: YAML 3 | --- 4 | 5 | # This page has YAML front matter! 6 | -------------------------------------------------------------------------------- /test/fixtures/front-matter/autodetect-yaml.md: -------------------------------------------------------------------------------- 1 | ---yaml 2 | title: autodetect-yaml 3 | user: jonschlinkert 4 | --- 5 | Content -------------------------------------------------------------------------------- /test/fixtures/front-matter/autodetect-no-lang.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: autodetect-no-lang 3 | user: jonschlinkert 4 | --- 5 | Content -------------------------------------------------------------------------------- /test/support/ignore.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'addEventListener', 3 | 'removeEventListener', 4 | 'removeAllListeners', 5 | 'removeListener' 6 | ]; -------------------------------------------------------------------------------- /test/fixtures/data/alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": { 3 | "test": true, 4 | "strong": "Heads up! This is a warning!", 5 | "text": "You forgot a field!" 6 | } 7 | } -------------------------------------------------------------------------------- /test/vfs/utils/is-windows.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var os = require('os'); 4 | 5 | var isWindows = (os.platform() === 'win32'); 6 | 7 | module.exports = isWindows; 8 | -------------------------------------------------------------------------------- /test/fixtures/helpers/obj.js: -------------------------------------------------------------------------------- 1 | exports.one = function one() { 2 | 3 | }; 4 | exports.two = function two() { 5 | 6 | }; 7 | exports.three = function three() { 8 | 9 | }; -------------------------------------------------------------------------------- /test/vfs/utils/always.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function always(value) { 4 | return function() { 5 | return value; 6 | }; 7 | } 8 | 9 | module.exports = always; 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text eol=lf 3 | 4 | # binaries 5 | *.ai binary 6 | *.psd binary 7 | *.jpg binary 8 | *.gif binary 9 | *.png binary 10 | *.jpeg binary 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - linux 4 | - osx 5 | - windows 6 | language: node_js 7 | node_js: 8 | - node 9 | - '11' 10 | - '10' 11 | - '9' 12 | - '8' 13 | - '7' 14 | - '6' 15 | -------------------------------------------------------------------------------- /test/vfs/utils/mock-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function mockError() { 4 | var callback = arguments[arguments.length - 1]; 5 | callback(new Error('mocked error')); 6 | } 7 | 8 | module.exports = mockError; 9 | -------------------------------------------------------------------------------- /test/vfs/utils/apply-umask.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function applyUmask(mode) { 4 | if (typeof mode !== 'number') { 5 | mode = parseInt(mode, 8); 6 | } 7 | 8 | return (mode & ~process.umask()); 9 | } 10 | 11 | module.exports = applyUmask; 12 | -------------------------------------------------------------------------------- /test/fixtures/bom-utf16be.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-16-BE. It contains some garbage at the start that looks like a UTF-8-encoded BOM (but isn t). 2 | -------------------------------------------------------------------------------- /test/fixtures/bom-utf16le.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-16-LE. It contains some garbage at the start that looks like a UTF-8-encoded BOM (but isn t). 2 | -------------------------------------------------------------------------------- /test/vfs/fixtures/bom-utf16be.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-16-BE. It contains some garbage at the start that looks like a UTF-8-encoded BOM (but isn t). 2 | -------------------------------------------------------------------------------- /test/vfs/fixtures/bom-utf16le.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-16-LE. It contains some garbage at the start that looks like a UTF-8-encoded BOM (but isn t). 2 | -------------------------------------------------------------------------------- /test/fixtures/vinyl/bom-utf16be.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-16-BE. It contains some garbage at the start that looks like a UTF-8-encoded BOM (but isn t). 2 | -------------------------------------------------------------------------------- /test/fixtures/vinyl/bom-utf16le.txt: -------------------------------------------------------------------------------- 1 | This file is saved as UTF-16-LE. It contains some garbage at the start that looks like a UTF-8-encoded BOM (but isn t). 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /test/vfs/utils/stat-mode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('graceful-fs'); 4 | 5 | var constants = require('vinyl-fs/lib/constants'); 6 | 7 | function masked(mode) { 8 | return mode & constants.MASK_MODE; 9 | } 10 | 11 | function statMode(outputPath) { 12 | return masked(fs.lstatSync(outputPath).mode); 13 | } 14 | 15 | module.exports = statMode; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | .idea 4 | .vscode 5 | *.sublime-* 6 | 7 | # test related, or directories generated by tests 8 | test/actual 9 | actual 10 | coverage 11 | .nyc* 12 | 13 | # npm 14 | node_modules 15 | npm-debug.log 16 | 17 | # yarn 18 | yarn.lock 19 | yarn-error.log 20 | 21 | # misc 22 | _gh_pages 23 | _draft 24 | _drafts 25 | bower_components 26 | vendor 27 | temp 28 | tmp 29 | TODO.md 30 | package-lock.json -------------------------------------------------------------------------------- /test/vfs/utils/cleanup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rimraf = require('rimraf'); 4 | var expect = require('expect'); 5 | 6 | function cleanup(glob) { 7 | return function(cb) { 8 | this.timeout(20000); 9 | 10 | expect.restoreSpies(); 11 | 12 | if (!glob) { 13 | return cb(); 14 | } 15 | 16 | // Async del to get sort-of-fix for https://github.com/isaacs/rimraf/issues/72 17 | rimraf(glob, cb); 18 | }; 19 | } 20 | 21 | module.exports = cleanup; 22 | -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var ignore = require('./ignore'); 5 | 6 | exports.containEql = function containEql(actual, expected) { 7 | if (Array.isArray(expected)) { 8 | var len = expected.length; 9 | while (len--) { 10 | exports.containEql(actual[len], expected[len]); 11 | } 12 | } else { 13 | for (var key in expected) { 14 | assert.deepEqual(actual[key], expected[key]); 15 | } 16 | } 17 | }; 18 | 19 | exports.keys = function keys(obj) { 20 | var arr = []; 21 | for (var key in obj) { 22 | if (ignore.indexOf(key) === -1) { 23 | arr.push(key); 24 | } 25 | } 26 | return arr; 27 | }; 28 | -------------------------------------------------------------------------------- /test/vfs/utils/break-prototype.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var File = require('vinyl'); 4 | 5 | function breakPrototype(file) { 6 | // Set up a broken prototype 7 | var oldProto = {}; 8 | Object.getOwnPropertyNames(File.prototype).forEach(function(key) { 9 | if (key !== 'isSymbolic') { 10 | var desc = Object.getOwnPropertyDescriptor(File.prototype, key); 11 | Object.defineProperty(oldProto, key, desc); 12 | } 13 | }); 14 | 15 | // Assign the broken prototype to our instance 16 | if (typeof Object.setPrototypeOf === 'function') { 17 | Object.setPrototypeOf(file, oldProto); 18 | } else { 19 | file.__proto__ = oldProto; 20 | } 21 | } 22 | 23 | module.exports = breakPrototype; 24 | -------------------------------------------------------------------------------- /test/support/spy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const sinon = require('sinon'); 5 | let errorfn = false; 6 | 7 | function maybeCallAsync(module, func) { 8 | let original = module[func]; 9 | return sinon.stub(module, func, function() { 10 | let args = Array.prototype.slice.call(arguments); 11 | args.unshift(module, func); 12 | let err = typeof errorfn === 'function' && errorfn.apply(this, args); 13 | if (!err) { 14 | original.apply(this, arguments); 15 | } else { 16 | arguments[arguments.length - 1](err); 17 | } 18 | }); 19 | } 20 | 21 | module.exports = { 22 | setError: function(fn) { 23 | errorfn = fn; 24 | }, 25 | chmodSpy: maybeCallAsync(fs, 'chmod'), 26 | fchmodSpy: maybeCallAsync(fs, 'fchmod'), 27 | futimesSpy: maybeCallAsync(fs, 'futimes'), 28 | statSpy: maybeCallAsync(fs, 'stat'), 29 | fstatSpy: maybeCallAsync(fs, 'fstat'), 30 | }; 31 | -------------------------------------------------------------------------------- /test/app.copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const path = require('path'); 5 | const assert = require('assert'); 6 | const rimraf = require('rimraf'); 7 | const App = require('templates'); 8 | const vfs = require('..'); 9 | let app; 10 | 11 | const fixtures = path.join(__dirname, 'fixtures/copy/*.txt'); 12 | const outpath = path.join(__dirname, 'out-fixtures'); 13 | 14 | describe('app.copy', function() { 15 | beforeEach(function(cb) { 16 | rimraf(outpath, cb); 17 | app = new App(); 18 | app.use(vfs()); 19 | }); 20 | 21 | afterEach(function(cb) { 22 | rimraf(outpath, cb); 23 | }); 24 | 25 | describe('streams', function() { 26 | it('should copy files', function(cb) { 27 | app.copy(fixtures, path.join(__dirname, 'actual')) 28 | .on('error', cb) 29 | .on('data', function(file) { 30 | assert.equal(typeof file, 'object'); 31 | }) 32 | .on('end', cb); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Jon Schlinkert. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/fixtures/noext/license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, Jon Schlinkert. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | ## Heads up! 2 | 3 | Major breaking changes in v2.0 of this plugin! [See the Release History](#release-history) for details. 4 | 5 | ## Usage 6 | 7 | ```js 8 | const Assemble = require('assemble'); 9 | 10 | // create your application and add the plugin 11 | const app = new Assemble(); 12 | app.use(require('{%= name %}')) 13 | 14 | // now you can use `src` and `dest` 15 | app.src(['foo/*.hbs']) 16 | .pipe(app.dest('site/')); 17 | ``` 18 | 19 | ## API 20 | 21 | Adds the following methods to your [assemble][] instance (works with any [Templates][] application): 22 | 23 | {%= apidocs("index.js") %} 24 | 25 | 26 | ## Release History 27 | 28 | **v2.0.0** 29 | 30 | - Major breaking changes based on v1.0 of Assemble! Requires Assemble v1.0 or above. 31 | 32 | **v0.6.0** 33 | 34 | - emit `end` on `app` when stream ends 35 | 36 | **v0.3.0** 37 | 38 | - breaking change! plugin is wrapped in a function that now be called when registered. e.g. `fs()`. This is to be consistent with assemble's plugin guidelines, and allows the plugin to be auto-loaded following the same format as other plugins. 39 | - rename `files` array to `streamFiles` 40 | - adds `onStream` middleware handler to `src` 41 | - adds `preWrite` middleware handler to `dest` 42 | -------------------------------------------------------------------------------- /test/vfs/utils/test-streams.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miss = require('mississippi'); 4 | var expect = require('expect'); 5 | 6 | var to = miss.to; 7 | var from = miss.from; 8 | var through = miss.through; 9 | 10 | function string(length) { 11 | return from(function(size, next) { 12 | if (length <= 0) { 13 | next(null, null); 14 | return; 15 | } 16 | 17 | var chunkSize = size <= length ? size : length; 18 | 19 | length -= size; 20 | 21 | var chunk = ''; 22 | for (var x = 0; x < chunkSize; x++) { 23 | chunk += 'a'; 24 | } 25 | 26 | next(null, chunk); 27 | }); 28 | } 29 | 30 | function rename(filepath) { 31 | return through.obj(function(file, enc, cb) { 32 | file.path = filepath; 33 | cb(null, file); 34 | }); 35 | } 36 | 37 | function includes(obj) { 38 | return through.obj(function(file, enc, cb) { 39 | expect(file).toInclude(obj); 40 | cb(null, file); 41 | }); 42 | } 43 | 44 | function count(value) { 45 | var count = 0; 46 | return through.obj(function(file, enc, cb) { 47 | count++; 48 | cb(null, file); 49 | }, function(cb) { 50 | expect(count).toEqual(value); 51 | cb(); 52 | }); 53 | } 54 | 55 | function slowCount(value) { 56 | var count = 0; 57 | return to.obj(function(file, enc, cb) { 58 | count++; 59 | 60 | setTimeout(function() { 61 | cb(null, file); 62 | }, 250); 63 | }, function(cb) { 64 | expect(count).toEqual(value); 65 | cb(); 66 | }); 67 | } 68 | 69 | module.exports = { 70 | string: string, 71 | rename: rename, 72 | includes: includes, 73 | count: count, 74 | slowCount: slowCount, 75 | }; 76 | -------------------------------------------------------------------------------- /test/handlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | const rimraf = require('rimraf'); 6 | const App = require('templates'); 7 | const File = require('vinyl'); 8 | const vfs = require('..'); 9 | let app; 10 | 11 | describe('handlers', () => { 12 | beforeEach(() => { 13 | app = new App(); 14 | app.use(vfs()); 15 | }); 16 | 17 | afterEach(cb => { 18 | rimraf(path.join(__dirname, './out-fixtures/'), cb); 19 | }); 20 | 21 | it('should handle onLoad', cb => { 22 | let count = 0; 23 | app.onLoad(/./, file => { 24 | count++; 25 | }); 26 | 27 | app.src(path.join(__dirname, './fixtures/vinyl/test.coffee')) 28 | .pipe(app.dest('./out-fixtures/', {cwd: __dirname})) 29 | .on('end', () => { 30 | assert.equal(count, 1); 31 | cb(); 32 | }); 33 | }); 34 | 35 | it('should handle preWrite', cb => { 36 | let count = 0; 37 | app.preWrite(/./, file => { 38 | count++; 39 | }); 40 | 41 | const srcPath = path.join(__dirname, './fixtures/vinyl/test.coffee'); 42 | const stream = app.dest('./out-fixtures/', { 43 | cwd: __dirname 44 | }); 45 | 46 | stream.once('finish', () => { 47 | assert.equal(count, 1); 48 | cb(); 49 | }); 50 | 51 | const file = new File({ 52 | path: srcPath, 53 | cwd: __dirname, 54 | contents: Buffer.from('1234567890') 55 | }); 56 | 57 | stream.write(file); 58 | stream.end(); 59 | }); 60 | 61 | it('should handle postWrite', cb => { 62 | let count = 0; 63 | app.postWrite(/./, file => { 64 | count++; 65 | }); 66 | 67 | const srcPath = path.join(__dirname, './fixtures/vinyl/test.coffee'); 68 | const stream = app.dest('./out-fixtures/', { 69 | cwd: __dirname 70 | }); 71 | 72 | stream.once('finish', () => { 73 | assert.equal(count, 1); 74 | cb(); 75 | }); 76 | 77 | const file = new File({ 78 | path: srcPath, 79 | cwd: __dirname, 80 | contents: Buffer.from('1234567890') 81 | }); 82 | 83 | stream.write(file); 84 | stream.end(); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/vfs/dest-owner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const File = require('vinyl'); 5 | const expect = require('expect'); 6 | const { concat, from, pipe } = require('mississippi'); 7 | const cleanup = require('./utils/cleanup'); 8 | const isWindows = require('./utils/is-windows'); 9 | const testConstants = require('./utils/test-constants'); 10 | const inputBase = testConstants.inputBase; 11 | const outputBase = testConstants.outputBase; 12 | const inputPath = testConstants.inputPath; 13 | const contents = testConstants.contents; 14 | const clean = cleanup(outputBase); 15 | const App = require('templates'); 16 | let plugin = require('../..'); 17 | let vfs; 18 | 19 | describe('.dest() with custom owner', function() { 20 | beforeEach(() => { 21 | vfs = new App(); 22 | vfs.use(plugin()); 23 | }); 24 | 25 | beforeEach(clean); 26 | afterEach(clean); 27 | 28 | it('calls fchown when the uid and/or gid are provided on the vinyl stat', function(done) { 29 | if (isWindows) { 30 | this.skip(); 31 | return; 32 | } 33 | 34 | var fchownSpy = expect.spyOn(fs, 'fchown').andCallThrough(); 35 | 36 | var file = new File({ 37 | base: inputBase, 38 | path: inputPath, 39 | contents: Buffer.from(contents), 40 | stat: { 41 | uid: 1001, 42 | gid: 1001, 43 | }, 44 | }); 45 | 46 | function assert() { 47 | expect(fchownSpy.calls.length).toEqual(1); 48 | expect(fchownSpy.calls[0].arguments[1]).toEqual(1001); 49 | expect(fchownSpy.calls[0].arguments[2]).toEqual(1001); 50 | } 51 | 52 | pipe([ 53 | from.obj([file]), 54 | vfs.dest(outputBase), 55 | concat(assert), 56 | ], done); 57 | }); 58 | 59 | it('does not call fchown when the uid and gid provided on the vinyl stat are invalid', function(done) { 60 | if (isWindows) { 61 | this.skip(); 62 | return; 63 | } 64 | 65 | var fchownSpy = expect.spyOn(fs, 'fchown').andCallThrough(); 66 | 67 | var file = new File({ 68 | base: inputBase, 69 | path: inputPath, 70 | contents: Buffer.from(contents), 71 | stat: { 72 | uid: -1, 73 | gid: -1, 74 | }, 75 | }); 76 | 77 | function assert() { 78 | expect(fchownSpy.calls.length).toEqual(0); 79 | } 80 | 81 | pipe([ 82 | from.obj([file]), 83 | vfs.dest(outputBase), 84 | concat(assert), 85 | ], done); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assemble-fs", 3 | "description": "Light wrapper for vinyl-fs to add streams support in a way that plays nice with Assemble middleware.", 4 | "version": "2.0.1", 5 | "homepage": "https://github.com/assemble/assemble-fs", 6 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", 7 | "contributors": [ 8 | "Brian Woodward (https://twitter.com/doowb)", 9 | "Jon Schlinkert (http://twitter.com/jonschlinkert)" 10 | ], 11 | "repository": "assemble/assemble-fs", 12 | "bugs": { 13 | "url": "https://github.com/assemble/assemble-fs/issues" 14 | }, 15 | "license": "MIT", 16 | "files": [ 17 | "index.js", 18 | "utils.js" 19 | ], 20 | "main": "index.js", 21 | "engines": { 22 | "node": ">=6" 23 | }, 24 | "scripts": { 25 | "test": "mocha test/{*.js,vfs/*.js}" 26 | }, 27 | "dependencies": { 28 | "lead": "^1.0.0", 29 | "pumpify": "^1.5.1", 30 | "readable-stream": "^3.0.6", 31 | "stream-combiner": "^0.2.2", 32 | "to-through": "^2.0.0", 33 | "vinyl-fs": "^3.0.3" 34 | }, 35 | "devDependencies": { 36 | "expect": "^1.19.0", 37 | "graceful-fs": "^4.1.15", 38 | "gulp-format-md": "^2.0.0", 39 | "mississippi": "^3.0.0", 40 | "mocha": "^5.2.0", 41 | "rimraf": "^2.6.2", 42 | "sinon": "^7.1.1", 43 | "templates": "file:../../../templates/templates-next", 44 | "vinyl": "^2.2.0" 45 | }, 46 | "keywords": [ 47 | "assemble", 48 | "boilerplate", 49 | "build", 50 | "cli", 51 | "cli-app", 52 | "command-line", 53 | "copy", 54 | "create", 55 | "dest", 56 | "dev", 57 | "development", 58 | "file", 59 | "file-system", 60 | "framework", 61 | "front", 62 | "frontend", 63 | "fs", 64 | "plugin", 65 | "project", 66 | "projects", 67 | "scaffold", 68 | "scaffolder", 69 | "scaffolding", 70 | "src", 71 | "stream", 72 | "streams", 73 | "template", 74 | "templates", 75 | "vinyl", 76 | "vinyl-fs", 77 | "webapp", 78 | "yeoman", 79 | "yo" 80 | ], 81 | "lintDeps": { 82 | "devDependencies": { 83 | "files": { 84 | "patterns": [ 85 | "test/vfs/**/*.js" 86 | ] 87 | } 88 | } 89 | }, 90 | "verb": { 91 | "run": true, 92 | "toc": false, 93 | "layout": "default", 94 | "tasks": [ 95 | "readme" 96 | ], 97 | "plugins": [ 98 | "gulp-format-md" 99 | ], 100 | "related": { 101 | "list": [ 102 | "generate", 103 | "update", 104 | "verb" 105 | ] 106 | }, 107 | "lint": { 108 | "reflinks": true 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/vfs/not-owned.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('graceful-fs'); 4 | var File = require('vinyl'); 5 | var expect = require('expect'); 6 | var miss = require('mississippi'); 7 | 8 | const App = require('templates'); 9 | let plugin = require('../..'); 10 | let vfs; 11 | 12 | var cleanup = require('./utils/cleanup'); 13 | var applyUmask = require('./utils/apply-umask'); 14 | var testConstants = require('./utils/test-constants'); 15 | 16 | var from = miss.from; 17 | var pipe = miss.pipe; 18 | var concat = miss.concat; 19 | 20 | var notOwnedBase = testConstants.notOwnedBase; 21 | var notOwnedPath = testConstants.notOwnedPath; 22 | var contents = testConstants.contents; 23 | 24 | var clean = cleanup(); 25 | 26 | describe('.dest() on not owned files', function() { 27 | beforeEach(() => { 28 | vfs = new App(); 29 | vfs.use(plugin()); 30 | }); 31 | 32 | var fileStats = fs.statSync(notOwnedPath); 33 | 34 | beforeEach(clean); 35 | afterEach(clean); 36 | 37 | var seenActions = false; 38 | 39 | function needsAction() { 40 | var problems = []; 41 | var actions = []; 42 | if (fileStats.uid !== 0) { 43 | problems.push('Test files not owned by root.'); 44 | actions.push(' sudo chown root ' + notOwnedPath); 45 | } 46 | if ((fileStats.mode & parseInt('022', 8)) !== parseInt('022', 8)) { 47 | problems.push('Test files not readable/writable by non-owners.'); 48 | actions.push(' sudo chmod 666 ' + notOwnedPath); 49 | } 50 | if (actions.length > 0) { 51 | if (!seenActions) { 52 | console.log(problems.join('\n')); 53 | console.log('Please run the following commands and try again:'); 54 | console.log(actions.join('\n')); 55 | seenActions = true; 56 | } 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | it('does not error if mtime is different', function(done) { 63 | if (needsAction()) { 64 | this.skip(); 65 | return; 66 | } 67 | 68 | var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); 69 | 70 | var earlier = Date.now() - 1000; 71 | 72 | var file = new File({ 73 | base: notOwnedBase, 74 | path: notOwnedPath, 75 | contents: Buffer.from(contents), 76 | stat: { 77 | mtime: new Date(earlier), 78 | }, 79 | }); 80 | 81 | function assert() { 82 | expect(futimesSpy.calls.length).toEqual(0); 83 | } 84 | 85 | pipe([ 86 | from.obj([file]), 87 | vfs.dest(notOwnedBase), 88 | concat(assert), 89 | ], done); 90 | }); 91 | 92 | it('does not error if mode is different', function(done) { 93 | if (needsAction()) { 94 | this.skip(); 95 | return; 96 | } 97 | 98 | var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); 99 | 100 | var file = new File({ 101 | base: notOwnedBase, 102 | path: notOwnedPath, 103 | contents: Buffer.from(contents), 104 | stat: { 105 | mode: applyUmask('777'), 106 | }, 107 | }); 108 | 109 | function assert() { 110 | expect(fchmodSpy.calls.length).toEqual(0); 111 | } 112 | 113 | pipe([ 114 | from.obj([file]), 115 | vfs.dest(notOwnedBase), 116 | concat(assert), 117 | ], done); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/vfs/utils/test-constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | // Input/output relative paths 6 | var inputRelative = './fixtures'; 7 | var outputRelative = './out-fixtures'; 8 | // Input/Output base directories 9 | var inputBase = path.join(__dirname, '..', inputRelative); 10 | var outputBase = path.join(__dirname, '..', outputRelative); 11 | // Used for file tests 12 | var inputPath = path.join(inputBase, './test.txt'); 13 | var outputPath = path.join(outputBase, './test.txt'); 14 | // Used for directory tests 15 | var inputDirpath = path.join(inputBase, './foo'); 16 | var outputDirpath = path.join(outputBase, './foo'); 17 | // Used for nested tests 18 | var inputNestedPath = path.join(inputDirpath, './test.txt'); 19 | var outputNestedPath = path.join(outputDirpath, './test.txt'); 20 | // Used for rename tests 21 | var outputRenamePath = path.join(outputBase, './foo2.txt'); 22 | // Used for not-owned tests 23 | var notOwnedBase = path.join(inputBase, './not-owned/'); 24 | var notOwnedPath = path.join(notOwnedBase, 'not-owned.txt'); 25 | // Used for BOM tests 26 | var bomInputPath = path.join(inputBase, './bom-utf8.txt'); 27 | var beEncodedInputPath = path.join(inputBase, './bom-utf16be.txt'); 28 | var leEncodedInputPath = path.join(inputBase, './bom-utf16le.txt'); 29 | // Used for symlink tests 30 | var symlinkNestedTarget = path.join(inputBase, './foo/bar/baz.txt'); 31 | var symlinkPath = path.join(outputBase, './test-symlink'); 32 | var symlinkDirpath = path.join(outputBase, './test-symlink-dir'); 33 | var symlinkMultiDirpath = path.join(outputBase, './test-multi-layer-symlink-dir'); 34 | var symlinkMultiDirpathSecond = path.join(outputBase, './test-multi-layer-symlink-dir2'); 35 | var symlinkNestedFirst = path.join(outputBase, './test-multi-layer-symlink'); 36 | var symlinkNestedSecond = path.join(outputBase, './foo/baz-link.txt'); 37 | // Paths that don't exist 38 | var neInputBase = path.join(inputBase, './not-exists/'); 39 | var neOutputBase = path.join(outputBase, './not-exists/'); 40 | var neInputDirpath = path.join(neInputBase, './foo'); 41 | var neOutputDirpath = path.join(neOutputBase, './foo'); 42 | // Used for contents of files 43 | var contents = 'Hello World!\n'; 44 | var sourcemapContents = '//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9maXh0dXJlcyIsIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIiLCJzb3VyY2VzIjpbIi4vZml4dHVyZXMiXSwic291cmNlc0NvbnRlbnQiOlsiSGVsbG8gV29ybGQhXG4iXX0='; 45 | 46 | module.exports = { 47 | inputRelative: inputRelative, 48 | outputRelative: outputRelative, 49 | inputBase: inputBase, 50 | outputBase: outputBase, 51 | inputPath: inputPath, 52 | outputPath: outputPath, 53 | inputDirpath: inputDirpath, 54 | outputDirpath: outputDirpath, 55 | inputNestedPath: inputNestedPath, 56 | outputNestedPath: outputNestedPath, 57 | outputRenamePath: outputRenamePath, 58 | notOwnedBase: notOwnedBase, 59 | notOwnedPath: notOwnedPath, 60 | bomInputPath: bomInputPath, 61 | beEncodedInputPath: beEncodedInputPath, 62 | leEncodedInputPath: leEncodedInputPath, 63 | symlinkNestedTarget: symlinkNestedTarget, 64 | symlinkPath: symlinkPath, 65 | symlinkDirpath: symlinkDirpath, 66 | symlinkMultiDirpath: symlinkMultiDirpath, 67 | symlinkMultiDirpathSecond: symlinkMultiDirpathSecond, 68 | symlinkNestedFirst: symlinkNestedFirst, 69 | symlinkNestedSecond: symlinkNestedSecond, 70 | neInputBase: neInputBase, 71 | neOutputBase: neOutputBase, 72 | neInputDirpath: neInputDirpath, 73 | neOutputDirpath: neOutputDirpath, 74 | contents: contents, 75 | sourcemapContents: sourcemapContents, 76 | }; 77 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const assign = Object.assign; 5 | const { Transform } = require('readable-stream'); 6 | const noop = (data, enc, next) => next(null, data); 7 | const define = (obj, key, fn) => Reflect.defineProperty(obj, key, { get: fn }); 8 | 9 | define(exports, 'combine', () => require('stream-combiner')); 10 | define(exports, 'vfs', () => require('vinyl-fs')); 11 | define(exports, 'src', () => require('vinyl-fs/lib/src')); 12 | define(exports, 'dest', () => require('vinyl-fs/lib/dest')); 13 | define(exports, 'symlink', () => require('vinyl-fs/lib/symlink')); 14 | 15 | /** 16 | * This function does all of the path-specific operations that 17 | * `prepareWrite` does in http://github.com/wearefractal/vinyl-fs, 18 | * but on a **cloned file**, which accomplishes two things: 19 | * 20 | * 1. We can merge the dest path information onto the context so 21 | * that it can be used to calculate relative paths for navigation, 22 | * pagination, etc. 23 | * 2. Since we use a cloned file, we're not risking any double-processing 24 | * on the actual view when it's finally written to the file system 25 | * by the `.dest()` method. 26 | * 27 | * @param {Object} view 28 | * @param {String|Function} dest 29 | * @param {Object} options 30 | */ 31 | 32 | exports.prepare = function(app, view, dest, options = {}) { 33 | if (view.preparedDest === true) return; 34 | let file = new view.constructor(view); 35 | let cwd = app.paths.templates; 36 | 37 | let destDir = typeof dest === 'function' ? dest(file) : dest; 38 | if (typeof destDir !== 'string') { 39 | throw new TypeError('expected destination directory to be a string'); 40 | } 41 | 42 | let baseDir = typeof options.base === 'function' 43 | ? options.base(file) 44 | : path.resolve(cwd, destDir); 45 | 46 | if (typeof baseDir !== 'string') { 47 | throw new TypeError('expected base directory to be a string'); 48 | } 49 | 50 | let writePath = path.join(destDir, view.basename); 51 | let data = {}; 52 | data.cwd = cwd; 53 | data.base = baseDir; 54 | data.dest = destDir; 55 | data.path = writePath; 56 | view.preparedDest = true; 57 | return data; 58 | }; 59 | 60 | /** 61 | * This sets up an event listener that will eventually 62 | * be called by `app.renderFile()`, ensuring that `dest` 63 | * information is loaded onto the context before rendering, 64 | * so that views can render relative paths. 65 | */ 66 | 67 | exports.prepareDest = function fn(app, dest, options) { 68 | app.emit('dest', dest, options); 69 | 70 | let appOpts = assign({}, this.options); 71 | delete appOpts.engine; 72 | delete appOpts.tasks; 73 | 74 | let opts = assign({}, appOpts, options); 75 | 76 | if (fn.prepare) { 77 | app.off('prepareDest', fn.prepare); 78 | } 79 | 80 | fn.prepare = view => { 81 | let data = exports.prepare(app, view, dest, opts); 82 | view.data = assign({}, view.data, data); 83 | }; 84 | 85 | app.on('prepareDest', fn.prepare); 86 | }; 87 | 88 | exports.through = (options, transform, flush) => { 89 | if (typeof options === 'function') { 90 | flush = transform; 91 | transform = options; 92 | options = null; 93 | } 94 | 95 | if (!transform) { 96 | transform = noop; 97 | } 98 | 99 | if (transform.length === 2) { 100 | let fn = transform; 101 | transform = (data, enc, cb) => fn(data, cb); 102 | } 103 | 104 | let stream = new Transform({ transform, flush, ...options }); 105 | stream.setMaxListeners(0); 106 | return stream; 107 | }; 108 | 109 | exports.through.obj = (options, transform, flush) => { 110 | if (typeof options === 'function') { 111 | flush = transform; 112 | transform = options; 113 | options = null; 114 | } 115 | 116 | let opts = Object.assign({ objectMode: true, highWaterMark: 16 }, options); 117 | return exports.through(opts, transform, flush); 118 | }; 119 | 120 | exports.define = function(obj, key, value) { 121 | Reflect.defineProperty(obj, key, { 122 | configurable: true, 123 | enumerable: false, 124 | writable: true, 125 | value 126 | }); 127 | }; 128 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "browser": false, 8 | "es6": true, 9 | "node": true, 10 | "mocha": true 11 | }, 12 | 13 | "parserOptions":{ 14 | "ecmaVersion": 9, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "modules": true, 18 | "experimentalObjectRestSpread": true 19 | } 20 | }, 21 | 22 | "globals": { 23 | "document": false, 24 | "navigator": false, 25 | "window": false 26 | }, 27 | 28 | "rules": { 29 | "accessor-pairs": 2, 30 | "arrow-spacing": [2, { "before": true, "after": true }], 31 | "block-spacing": [2, "always"], 32 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 33 | "comma-dangle": [2, "never"], 34 | "comma-spacing": [2, { "before": false, "after": true }], 35 | "comma-style": [2, "last"], 36 | "constructor-super": 2, 37 | "curly": [2, "multi-line"], 38 | "dot-location": [2, "property"], 39 | "eol-last": 2, 40 | "eqeqeq": [2, "allow-null"], 41 | "generator-star-spacing": [2, { "before": true, "after": true }], 42 | "handle-callback-err": [2, "^(err|error)$" ], 43 | "indent": [2, 2, { "SwitchCase": 1 }], 44 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 45 | "keyword-spacing": [2, { "before": true, "after": true }], 46 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 47 | "new-parens": 2, 48 | "no-array-constructor": 2, 49 | "no-caller": 2, 50 | "no-class-assign": 2, 51 | "no-cond-assign": 2, 52 | "no-const-assign": 2, 53 | "no-control-regex": 2, 54 | "no-debugger": 2, 55 | "no-delete-var": 2, 56 | "no-dupe-args": 2, 57 | "no-dupe-class-members": 2, 58 | "no-dupe-keys": 2, 59 | "no-duplicate-case": 2, 60 | "no-empty-character-class": 2, 61 | "no-eval": 2, 62 | "no-ex-assign": 2, 63 | "no-extend-native": 2, 64 | "no-extra-bind": 2, 65 | "no-extra-boolean-cast": 2, 66 | "no-extra-parens": [2, "functions"], 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 2, 69 | "no-func-assign": 2, 70 | "no-implied-eval": 2, 71 | "no-inner-declarations": [2, "functions"], 72 | "no-invalid-regexp": 2, 73 | "no-irregular-whitespace": 2, 74 | "no-iterator": 2, 75 | "no-label-var": 2, 76 | "no-labels": 2, 77 | "no-lone-blocks": 2, 78 | "no-mixed-spaces-and-tabs": 2, 79 | "no-multi-spaces": 2, 80 | "no-multi-str": 2, 81 | "no-multiple-empty-lines": [2, { "max": 1 }], 82 | "no-native-reassign": 0, 83 | "no-negated-in-lhs": 2, 84 | "no-new": 2, 85 | "no-new-func": 2, 86 | "no-new-object": 2, 87 | "no-new-require": 2, 88 | "no-new-wrappers": 2, 89 | "no-obj-calls": 2, 90 | "no-octal": 2, 91 | "no-octal-escape": 2, 92 | "no-proto": 0, 93 | "no-redeclare": 2, 94 | "no-regex-spaces": 2, 95 | "no-return-assign": 2, 96 | "no-self-compare": 2, 97 | "no-sequences": 2, 98 | "no-shadow-restricted-names": 2, 99 | "no-spaced-func": 2, 100 | "no-sparse-arrays": 2, 101 | "no-this-before-super": 2, 102 | "no-throw-literal": 2, 103 | "no-trailing-spaces": 0, 104 | "no-undef": 2, 105 | "no-undef-init": 2, 106 | "no-unexpected-multiline": 2, 107 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 108 | "no-unreachable": 2, 109 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 110 | "no-useless-call": 0, 111 | "no-with": 2, 112 | "one-var": [0, { "initialized": "never" }], 113 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 114 | "padded-blocks": [0, "never"], 115 | "quotes": [2, "single", "avoid-escape"], 116 | "radix": 2, 117 | "semi": [2, "always"], 118 | "semi-spacing": [2, { "before": false, "after": true }], 119 | "space-before-blocks": [2, "always"], 120 | "space-before-function-paren": [2, "never"], 121 | "space-in-parens": [2, "never"], 122 | "space-infix-ops": 2, 123 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 124 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 125 | "use-isnan": 2, 126 | "valid-typeof": 2, 127 | "wrap-iife": [2, "any"], 128 | "yoda": [2, "never"] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * assemble-fs 3 | * 4 | * Copyright (c) 2015-2018, Jon Schlinkert. 5 | * Released under the MIT License. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const lead = require('lead'); 11 | const pumpify = require('pumpify'); 12 | const toThrough = require('to-through'); 13 | const utils = require('./utils'); 14 | 15 | module.exports = options => { 16 | return function plugin(app) { 17 | if (app.src || (!app.collections && !app.isCollection)) return; 18 | 19 | /** 20 | * Setup middleware handlers. Assume none of the handlers exist if `onStream` 21 | * does not exist. 22 | */ 23 | 24 | addHandlers(app, ['onLoad', 'onStream', 'preWrite', 'postWrite']); 25 | 26 | /** 27 | * Copy files with the given glob `patterns` to the specified `dest`. 28 | * 29 | * ```js 30 | * app.task('assets', function(cb) { 31 | * app.copy('assets/**', 'dist/') 32 | * .on('error', cb) 33 | * .on('finish', cb) 34 | * }); 35 | * ``` 36 | * @name .copy 37 | * @param {String|Array} `patterns` Glob patterns of files to copy. 38 | * @param {String|Function} `dest` Desination directory. 39 | * @return {Stream} Stream, to continue processing if necessary. 40 | * @api public 41 | */ 42 | 43 | utils.define(app, 'copy', (patterns, dest, options) => { 44 | return utils.vfs.src(patterns, { allowEmpty: true, ...options }) 45 | .pipe(utils.vfs.dest(dest, options)); 46 | }); 47 | 48 | /** 49 | * Glob patterns or filepaths to source files. 50 | * 51 | * ```js 52 | * app.src('src/*.hbs', {layout: 'default'}); 53 | * ``` 54 | * @name .src 55 | * @param {String|Array} `glob` Glob patterns or file paths to source files. 56 | * @param {Object} `options` Options or locals to merge into the context and/or pass to `src` plugins 57 | * @api public 58 | */ 59 | 60 | utils.define(app, 'src', (patterns, options) => { 61 | let streams = pumpify.obj([ 62 | utils.src(patterns, { allowEmpty: true, ...options }), 63 | handle(app, 'onStream'), 64 | toFiles(app, options) 65 | ]); 66 | return toThrough(streams); 67 | }); 68 | 69 | /** 70 | * Glob patterns or paths for symlinks. 71 | * 72 | * ```js 73 | * app.symlink('src/**'); 74 | * ``` 75 | * @name .symlink 76 | * @param {String|Array} `glob` 77 | * @api public 78 | */ 79 | 80 | utils.define(app, 'symlink', (...args) => utils.vfs.symlink(...args)); 81 | 82 | /** 83 | * Specify a destination for processed files. Runs `.preWrite` and 84 | * `.postWrite` middleware handlers on all files. 85 | * 86 | * ```js 87 | * app.dest('dist/'); 88 | * ``` 89 | * @name .dest 90 | * @param {String|Function} `dest` File path or rename function. 91 | * @param {Object} `options` Options and locals to pass to `dest` plugins 92 | * @api public 93 | */ 94 | 95 | utils.define(app, 'dest', (dest, options = {}) => { 96 | if (!dest) { 97 | throw new TypeError('expected dest to be a string or function'); 98 | } 99 | 100 | // ensure "dest" is added to the context before rendering 101 | utils.prepareDest(app, dest, options); 102 | 103 | let output = pumpify.obj([ 104 | handle(app, 'preWrite'), 105 | utils.dest(dest, options), 106 | handle(app, 'postWrite') 107 | ]); 108 | 109 | return lead(output); 110 | }); 111 | 112 | return plugin; 113 | }; 114 | }; 115 | 116 | function addHandlers(app, handlers) { 117 | for (let name of handlers) { 118 | if (typeof app[name] !== 'function') { 119 | app.handler(name); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * Make sure vinyl files are assemble files, and add 126 | * them to a collection, if specified. 127 | */ 128 | 129 | function toFiles(app, options) { 130 | let opts = Object.assign({ collection: null }, options); 131 | let name = opts.collection; 132 | let collection = app.collections ? name && app[name] : app; 133 | let view; 134 | 135 | if (!collection && name) { 136 | collection = app.create(name, opts); 137 | } 138 | 139 | return utils.through.obj(async (file, enc, next) => { 140 | if (!app.File.isFile(file)) { 141 | file = app.file(file.path, file); 142 | } 143 | 144 | if (file.isNull()) { 145 | next(null, file); 146 | return; 147 | } 148 | 149 | if (collection && isCollection(collection)) { 150 | try { 151 | view = await collection.set(file.path, file); 152 | } catch (err) { 153 | next(err); 154 | return; 155 | } 156 | next(null, view); 157 | return; 158 | } 159 | 160 | view = !app.File.isFile(file) ? app.file(file.path, file) : file; 161 | 162 | app.handle('onLoad', view) 163 | .then(() => next(null, view)) 164 | .catch(next); 165 | }); 166 | } 167 | 168 | function isCollection(collection) { 169 | return collection.files && !collection.collections; 170 | } 171 | 172 | function handle(app, method) { 173 | return utils.through.obj(async (file, enc, next) => { 174 | if (!file || (!file.path && !file.isNull && !file.contents)) { 175 | next(null, file); 176 | return; 177 | } 178 | 179 | if (file.isNull() || !app.handle) { 180 | next(null, file); 181 | return; 182 | } 183 | 184 | if (typeof app[method] !== 'function') { 185 | next(new Error(`middleware handler "${method}" is not registered`)); 186 | return; 187 | } 188 | 189 | app.handle(method, file) 190 | .then(() => next(null, file)) 191 | .catch(next); 192 | }); 193 | } 194 | -------------------------------------------------------------------------------- /test/vfs/src-symlinks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('graceful-fs'); 4 | var expect = require('expect'); 5 | var miss = require('mississippi'); 6 | 7 | const App = require('templates'); 8 | let plugin = require('../..'); 9 | let vfs; 10 | 11 | var cleanup = require('./utils/cleanup'); 12 | var testConstants = require('./utils/test-constants'); 13 | 14 | var pipe = miss.pipe; 15 | var concat = miss.concat; 16 | 17 | var outputBase = testConstants.outputBase; 18 | var inputPath = testConstants.inputPath; 19 | var inputDirpath = testConstants.inputDirpath; 20 | var outputDirpath = testConstants.outputDirpath; 21 | var symlinkNestedTarget = testConstants.symlinkNestedTarget; 22 | var symlinkPath = testConstants.symlinkPath; 23 | var symlinkDirpath = testConstants.symlinkDirpath; 24 | var symlinkMultiDirpath = testConstants.symlinkMultiDirpath; 25 | var symlinkMultiDirpathSecond = testConstants.symlinkMultiDirpathSecond; 26 | var symlinkNestedFirst = testConstants.symlinkNestedFirst; 27 | var symlinkNestedSecond = testConstants.symlinkNestedSecond; 28 | 29 | var clean = cleanup(outputBase); 30 | 31 | describe('.src() with symlinks', function() { 32 | beforeEach(() => { 33 | vfs = new App(); 34 | vfs.use(plugin()); 35 | }); 36 | 37 | beforeEach(clean); 38 | afterEach(clean); 39 | 40 | beforeEach(function(done) { 41 | fs.mkdirSync(outputBase); 42 | fs.mkdirSync(outputDirpath); 43 | fs.symlinkSync(inputDirpath, symlinkDirpath); 44 | fs.symlinkSync(symlinkDirpath, symlinkMultiDirpath); 45 | fs.symlinkSync(symlinkMultiDirpath, symlinkMultiDirpathSecond); 46 | fs.symlinkSync(inputPath, symlinkPath); 47 | fs.symlinkSync(symlinkNestedTarget, symlinkNestedSecond); 48 | fs.symlinkSync(symlinkNestedSecond, symlinkNestedFirst); 49 | done(); 50 | }); 51 | 52 | it('resolves symlinks correctly', function(done) { 53 | function assert(files) { 54 | expect(files.length).toEqual(1); 55 | // The path should be the symlink itself 56 | expect(files[0].path).toEqual(symlinkNestedFirst); 57 | // But the content should be what's in the actual file 58 | expect(files[0].contents.toString()).toEqual('symlink works\n'); 59 | // And the stats should have been updated 60 | expect(files[0].stat.isSymbolicLink()).toEqual(false); 61 | expect(files[0].stat.isFile()).toEqual(true); 62 | } 63 | 64 | pipe([ 65 | vfs.src(symlinkNestedFirst), 66 | concat(assert), 67 | ], done); 68 | }); 69 | 70 | it('resolves directory symlinks correctly', function(done) { 71 | function assert(files) { 72 | expect(files.length).toEqual(1); 73 | // The path should be the symlink itself 74 | expect(files[0].path).toEqual(symlinkDirpath); 75 | // But the contents should be null 76 | expect(files[0].contents).toEqual(null); 77 | // And the stats should have been updated 78 | expect(files[0].stat.isSymbolicLink()).toEqual(false); 79 | expect(files[0].stat.isDirectory()).toEqual(true); 80 | } 81 | 82 | pipe([ 83 | vfs.src(symlinkDirpath), 84 | concat(assert), 85 | ], done); 86 | }); 87 | 88 | it('resolves nested symlinks to directories correctly', function(done) { 89 | function assert(files) { 90 | expect(files.length).toEqual(1); 91 | // The path should be the symlink itself 92 | expect(files[0].path).toEqual(symlinkMultiDirpathSecond); 93 | // But the contents should be null 94 | expect(files[0].contents).toEqual(null); 95 | // And the stats should have been updated 96 | expect(files[0].stat.isSymbolicLink()).toEqual(false); 97 | expect(files[0].stat.isDirectory()).toEqual(true); 98 | } 99 | 100 | pipe([ 101 | vfs.src(symlinkMultiDirpathSecond), 102 | concat(assert), 103 | ], done); 104 | }); 105 | 106 | it('preserves file symlinks with resolveSymlinks option set to false', function(done) { 107 | var expectedRelativeSymlinkPath = fs.readlinkSync(symlinkPath); 108 | 109 | function assert(files) { 110 | expect(files.length).toEqual(1); 111 | expect(files[0].path).toEqual(symlinkPath); 112 | expect(files[0].symlink).toEqual(expectedRelativeSymlinkPath); 113 | } 114 | 115 | pipe([ 116 | vfs.src(symlinkPath, { resolveSymlinks: false }), 117 | concat(assert), 118 | ], done); 119 | }); 120 | 121 | it('preserves directory symlinks with resolveSymlinks option set to false', function(done) { 122 | var expectedRelativeSymlinkPath = fs.readlinkSync(symlinkDirpath); 123 | 124 | function assert(files) { 125 | expect(files.length).toEqual(1); 126 | expect(files[0].path).toEqual(symlinkDirpath); 127 | expect(files[0].symlink).toEqual(expectedRelativeSymlinkPath); 128 | } 129 | 130 | pipe([ 131 | vfs.src(symlinkDirpath, { resolveSymlinks: false }), 132 | concat(assert), 133 | ], done); 134 | }); 135 | 136 | it('receives a file with symbolic link stats when resolveSymlinks is a function', function(done) { 137 | 138 | function resolveSymlinks(file) { 139 | expect(file).toExist(); 140 | expect(file.stat).toExist(); 141 | expect(file.stat.isSymbolicLink()).toEqual(true); 142 | 143 | return true; 144 | } 145 | 146 | function assert(files) { 147 | expect(files.length).toEqual(1); 148 | // And the stats should have been updated 149 | expect(files[0].stat.isSymbolicLink()).toEqual(false); 150 | expect(files[0].stat.isFile()).toEqual(true); 151 | } 152 | 153 | pipe([ 154 | vfs.src(symlinkNestedFirst, { resolveSymlinks: resolveSymlinks }), 155 | concat(assert), 156 | ], done); 157 | }); 158 | 159 | it('only calls resolveSymlinks once-per-file if it is a function', function(done) { 160 | 161 | var spy = expect.createSpy().andReturn(true); 162 | 163 | function assert() { 164 | expect(spy.calls.length).toEqual(1); 165 | } 166 | 167 | pipe([ 168 | vfs.src(symlinkNestedFirst, { resolveSymlinks: spy }), 169 | concat(assert), 170 | ], done); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/vfs/dest-times.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const File = require('vinyl'); 5 | const expect = require('expect'); 6 | const cleanup = require('./utils/cleanup'); 7 | const isWindows = require('./utils/is-windows'); 8 | const testConstants = require('./utils/test-constants'); 9 | const { concat, from, pipe } = require('mississippi'); 10 | 11 | const inputBase = testConstants.inputBase; 12 | const outputBase = testConstants.outputBase; 13 | const inputPath = testConstants.inputPath; 14 | const outputPath = testConstants.outputPath; 15 | const contents = testConstants.contents; 16 | const clean = cleanup(outputBase); 17 | const App = require('templates'); 18 | let plugin = require('../..'); 19 | let vfs; 20 | 21 | describe('.dest() with custom times', function() { 22 | beforeEach(() => { 23 | vfs = new App(); 24 | vfs.use(plugin()); 25 | }); 26 | 27 | beforeEach(clean); 28 | afterEach(clean); 29 | 30 | it('does not call futimes when no mtime is provided on the vinyl stat', function(done) { 31 | // Changing the time of a directory errors in Windows. 32 | // Windows is treated as though it does not have permission to make this operation. 33 | if (isWindows) { 34 | this.skip(); 35 | return; 36 | } 37 | 38 | var earlier = Date.now() - 1001; 39 | 40 | var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); 41 | 42 | var file = new File({ 43 | base: inputBase, 44 | path: inputPath, 45 | contents: Buffer.from(contents), 46 | stat: {}, 47 | }); 48 | 49 | function assert() { 50 | var stats = fs.lstatSync(outputPath); 51 | 52 | expect(futimesSpy.calls.length).toEqual(0); 53 | expect(stats.atime.getTime()).toBeGreaterThan(earlier); 54 | expect(stats.mtime.getTime()).toBeGreaterThan(earlier); 55 | } 56 | 57 | pipe([ 58 | from.obj([file]), 59 | vfs.dest(outputBase, { cwd: __dirname }), 60 | concat(assert), 61 | ], done); 62 | }); 63 | 64 | it('calls futimes when an mtime is provided on the vinyl stat', function(done) { 65 | if (isWindows) { 66 | this.skip(); 67 | return; 68 | } 69 | 70 | // Use new mtime 71 | var mtime = new Date(Date.now() - 2048); 72 | 73 | var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); 74 | 75 | var file = new File({ 76 | base: inputBase, 77 | path: inputPath, 78 | contents: Buffer.from(contents), 79 | stat: { 80 | mtime: mtime, 81 | }, 82 | }); 83 | 84 | function assert() { 85 | expect(futimesSpy.calls.length).toEqual(1); 86 | 87 | // Compare args instead of fs.lstats(), since mtime may be drifted in x86 Node.js 88 | var mtimeSpy = futimesSpy.calls[0].arguments[2]; 89 | 90 | expect(mtimeSpy.getTime()) 91 | .toEqual(mtime.getTime()); 92 | 93 | expect(file.stat.mtime).toEqual(mtime); 94 | } 95 | 96 | pipe([ 97 | from.obj([file]), 98 | vfs.dest(outputBase, { cwd: __dirname }), 99 | concat(assert), 100 | ], done); 101 | }); 102 | 103 | it('does not call futimes when provided mtime on the vinyl stat is invalid', function(done) { 104 | if (isWindows) { 105 | this.skip(); 106 | return; 107 | } 108 | 109 | var earlier = Date.now() - 1001; 110 | 111 | var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); 112 | 113 | var file = new File({ 114 | base: inputBase, 115 | path: inputPath, 116 | contents: Buffer.from(contents), 117 | stat: { 118 | mtime: new Date(undefined), 119 | }, 120 | }); 121 | 122 | function assert() { 123 | var stats = fs.lstatSync(outputPath); 124 | 125 | expect(futimesSpy.calls.length).toEqual(0); 126 | expect(stats.mtime.getTime()).toBeGreaterThan(earlier); 127 | } 128 | 129 | pipe([ 130 | from.obj([file]), 131 | vfs.dest(outputBase, { cwd: __dirname }), 132 | concat(assert), 133 | ], done); 134 | }); 135 | 136 | it('calls futimes when provided mtime on the vinyl stat is valid but provided atime is invalid', function(done) { 137 | if (isWindows) { 138 | this.skip(); 139 | return; 140 | } 141 | 142 | // Use new mtime 143 | var mtime = new Date(Date.now() - 2048); 144 | var invalidAtime = new Date(undefined); 145 | 146 | var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); 147 | 148 | var file = new File({ 149 | base: inputBase, 150 | path: inputPath, 151 | contents: Buffer.from(contents), 152 | stat: { 153 | atime: invalidAtime, 154 | mtime: mtime, 155 | }, 156 | }); 157 | 158 | function assert() { 159 | expect(futimesSpy.calls.length).toEqual(1); 160 | 161 | var mtimeSpy = futimesSpy.calls[0].arguments[2]; 162 | 163 | expect(mtimeSpy.getTime()).toEqual(mtime.getTime()); 164 | } 165 | 166 | pipe([ 167 | from.obj([file]), 168 | vfs.dest(outputBase, { cwd: __dirname }), 169 | concat(assert), 170 | ], done); 171 | }); 172 | 173 | it('writes file atime and mtime using the vinyl stat', function(done) { 174 | if (isWindows) { 175 | this.skip(); 176 | return; 177 | } 178 | 179 | // Use new atime/mtime 180 | var atime = new Date(Date.now() - 2048); 181 | var mtime = new Date(Date.now() - 1024); 182 | 183 | var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); 184 | 185 | var file = new File({ 186 | base: inputBase, 187 | path: inputPath, 188 | contents: Buffer.from(contents), 189 | stat: { 190 | atime: atime, 191 | mtime: mtime, 192 | }, 193 | }); 194 | 195 | function assert() { 196 | expect(futimesSpy.calls.length).toEqual(1); 197 | 198 | var atimeSpy = futimesSpy.calls[0].arguments[1]; 199 | var mtimeSpy = futimesSpy.calls[0].arguments[2]; 200 | 201 | expect(atimeSpy.getTime()).toEqual(atime.getTime()); 202 | expect(mtimeSpy.getTime()).toEqual(mtime.getTime()); 203 | expect(file.stat.mtime).toEqual(mtime); 204 | expect(file.stat.atime).toEqual(atime); 205 | }; 206 | 207 | pipe([ 208 | from.obj([file]), 209 | vfs.dest(outputBase, { cwd: __dirname }), 210 | concat(assert), 211 | ], done); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /test/vfs/integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var fs = require('graceful-fs'); 6 | var miss = require('mississippi'); 7 | var expect = require('expect'); 8 | 9 | const App = require('templates'); 10 | let plugin = require('../..'); 11 | let vfs; 12 | 13 | var cleanup = require('./utils/cleanup'); 14 | var isWindows = require('./utils/is-windows'); 15 | var testStreams = require('./utils/test-streams'); 16 | var testConstants = require('./utils/test-constants'); 17 | 18 | var pipe = miss.pipe; 19 | var concat = miss.concat; 20 | 21 | var count = testStreams.count; 22 | 23 | var base = testConstants.outputBase; 24 | var inputDirpath = testConstants.inputDirpath; 25 | var outputDirpath = testConstants.outputDirpath; 26 | var symlinkDirpath = testConstants.symlinkDirpath; 27 | var inputBase = path.join(base, './in/'); 28 | var inputDirpath = testConstants.inputDirpath; 29 | var outputDirpath = testConstants.outputDirpath; 30 | var symlinkDirpath = testConstants.symlinkDirpath; 31 | var inputGlob = path.join(inputBase, './*.txt'); 32 | var outputBase = path.join(base, './out/'); 33 | var outputSymlink = path.join(symlinkDirpath, './foo'); 34 | var outputDirpathSymlink = path.join(outputDirpath, './foo'); 35 | var content = testConstants.content; 36 | 37 | var clean = cleanup(base); 38 | 39 | describe('integrations', function() { 40 | beforeEach(() => { 41 | vfs = new App(); 42 | vfs.use(plugin()); 43 | }); 44 | 45 | beforeEach(clean); 46 | afterEach(clean); 47 | 48 | it('does not exhaust available file descriptors when streaming thousands of files', function(done) { 49 | // This can be a very slow test on boxes with slow disk i/o 50 | this.timeout(0); 51 | 52 | // Make a ton of files. Changed from hard links due to Windows failures 53 | var expectedCount = 6000; 54 | 55 | fs.mkdirSync(base); 56 | fs.mkdirSync(inputBase); 57 | 58 | for (var idx = 0; idx < expectedCount; idx++) { 59 | var filepath = path.join(inputBase, './test' + idx + '.txt'); 60 | fs.writeFileSync(filepath, content); 61 | } 62 | 63 | pipe([ 64 | vfs.src(inputGlob, { buffer: false }), 65 | count(expectedCount), 66 | vfs.dest(outputBase), 67 | ], done); 68 | }); 69 | 70 | it('(*nix) sources a directory, creates a symlink and copies it', function(done) { 71 | if (isWindows) { 72 | this.skip(); 73 | return; 74 | } 75 | 76 | function assert(files) { 77 | var symlinkResult = fs.readlinkSync(outputSymlink); 78 | var destResult = fs.readlinkSync(outputDirpathSymlink); 79 | 80 | expect(symlinkResult).toEqual(inputDirpath); 81 | expect(destResult).toEqual(inputDirpath); 82 | expect(files[0].isSymbolic()).toBe(true); 83 | expect(files[0].symlink).toEqual(inputDirpath); 84 | } 85 | 86 | pipe([ 87 | vfs.src(inputDirpath), 88 | vfs.symlink(symlinkDirpath), 89 | vfs.dest(outputDirpath), 90 | concat(assert), 91 | ], done); 92 | }); 93 | 94 | it('(windows) sources a directory, creates a junction and copies it', function(done) { 95 | if (!isWindows) { 96 | this.skip(); 97 | return; 98 | } 99 | 100 | function assert(files) { 101 | // Junctions add an ending separator 102 | var expected = inputDirpath + path.sep; 103 | var symlinkResult = fs.readlinkSync(outputSymlink); 104 | var destResult = fs.readlinkSync(outputDirpathSymlink); 105 | 106 | expect(symlinkResult).toEqual(expected); 107 | expect(destResult).toEqual(expected); 108 | expect(files[0].isSymbolic()).toBe(true); 109 | expect(files[0].symlink).toEqual(inputDirpath); 110 | } 111 | 112 | pipe([ 113 | vfs.src(inputDirpath), 114 | vfs.symlink(symlinkDirpath), 115 | vfs.dest(outputDirpath), 116 | concat(assert), 117 | ], done); 118 | }); 119 | 120 | it('(*nix) sources a symlink and copies it', function(done) { 121 | if (isWindows) { 122 | this.skip(); 123 | return; 124 | } 125 | 126 | fs.mkdirSync(base); 127 | fs.mkdirSync(symlinkDirpath); 128 | fs.symlinkSync(inputDirpath, outputSymlink); 129 | 130 | function assert(files) { 131 | var destResult = fs.readlinkSync(outputDirpathSymlink); 132 | 133 | expect(destResult).toEqual(inputDirpath); 134 | expect(files[0].isSymbolic()).toEqual(true); 135 | expect(files[0].symlink).toEqual(inputDirpath); 136 | } 137 | 138 | pipe([ 139 | vfs.src(outputSymlink, { resolveSymlinks: false }), 140 | vfs.dest(outputDirpath), 141 | concat(assert), 142 | ], done); 143 | }); 144 | 145 | it('(windows) sources a directory symlink and copies it', function(done) { 146 | if (!isWindows) { 147 | this.skip(); 148 | return; 149 | } 150 | 151 | fs.mkdirSync(base); 152 | fs.mkdirSync(symlinkDirpath); 153 | fs.symlinkSync(inputDirpath, outputSymlink, 'dir'); 154 | 155 | function assert(files) { 156 | // 'dir' symlinks add an ending separator 157 | var expected = inputDirpath + path.sep; 158 | var destResult = fs.readlinkSync(outputDirpathSymlink); 159 | 160 | expect(destResult).toEqual(expected); 161 | expect(files[0].isSymbolic()).toEqual(true); 162 | expect(files[0].symlink).toEqual(inputDirpath); 163 | } 164 | 165 | pipe([ 166 | vfs.src(outputSymlink, { resolveSymlinks: false }), 167 | vfs.dest(outputDirpath), 168 | concat(assert), 169 | ], done); 170 | }); 171 | 172 | it('(windows) sources a junction and copies it', function(done) { 173 | if (!isWindows) { 174 | this.skip(); 175 | return; 176 | } 177 | 178 | fs.mkdirSync(base); 179 | fs.mkdirSync(symlinkDirpath); 180 | fs.symlinkSync(inputDirpath, outputSymlink, 'junction'); 181 | 182 | function assert(files) { 183 | // Junctions add an ending separator 184 | var expected = inputDirpath + path.sep; 185 | var destResult = fs.readlinkSync(outputDirpathSymlink); 186 | 187 | expect(destResult).toEqual(expected); 188 | expect(files[0].isSymbolic()).toEqual(true); 189 | expect(files[0].symlink).toEqual(inputDirpath); 190 | } 191 | 192 | pipe([ 193 | vfs.src(outputSymlink, { resolveSymlinks: false }), 194 | vfs.dest(outputDirpath), 195 | concat(assert), 196 | ], done); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # assemble-fs [![NPM version](https://img.shields.io/npm/v/assemble-fs.svg?style=flat)](https://www.npmjs.com/package/assemble-fs) [![NPM monthly downloads](https://img.shields.io/npm/dm/assemble-fs.svg?style=flat)](https://npmjs.org/package/assemble-fs) [![NPM total downloads](https://img.shields.io/npm/dt/assemble-fs.svg?style=flat)](https://npmjs.org/package/assemble-fs) [![Linux Build Status](https://img.shields.io/travis/assemble/assemble-fs.svg?style=flat&label=Travis)](https://travis-ci.org/assemble/assemble-fs) 2 | 3 | > Light wrapper for vinyl-fs to add streams support in a way that plays nice with Assemble middleware. 4 | 5 | Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. 6 | 7 | ## Install 8 | 9 | Install with [npm](https://www.npmjs.com/): 10 | 11 | ```sh 12 | $ npm install --save assemble-fs 13 | ``` 14 | 15 | ## Heads up! 16 | 17 | Major breaking changes in v2.0 of this plugin! [See the Release History](#release-history) for details. 18 | 19 | ## Usage 20 | 21 | ```js 22 | const Assemble = require('assemble'); 23 | 24 | // create your application and add the plugin 25 | const app = new Assemble(); 26 | app.use(require('assemble-fs')) 27 | 28 | // now you can use `src` and `dest` 29 | app.src(['foo/*.hbs']) 30 | .pipe(app.dest('site/')); 31 | ``` 32 | 33 | ## API 34 | 35 | Adds the following methods to your [assemble](https://github.com/assemble/assemble) instance (works with any [Templates][] application): 36 | 37 | ### [.copy](index.js#L43) 38 | 39 | Copy files with the given glob `patterns` to the specified `dest`. 40 | 41 | **Params** 42 | 43 | * `patterns` **{String|Array}**: Glob patterns of files to copy. 44 | * `dest` **{String|Function}**: Desination directory. 45 | * `returns` **{Stream}**: Stream, to continue processing if necessary. 46 | 47 | **Example** 48 | 49 | ```js 50 | app.task('assets', function(cb) { 51 | app.copy('assets/**', 'dist/') 52 | .on('error', cb) 53 | .on('finish', cb) 54 | }); 55 | ``` 56 | 57 | ### [.src](index.js#L60) 58 | 59 | Glob patterns or filepaths to source files. 60 | 61 | **Params** 62 | 63 | * `glob` **{String|Array}**: Glob patterns or file paths to source files. 64 | * `options` **{Object}**: Options or locals to merge into the context and/or pass to `src` plugins 65 | 66 | **Example** 67 | 68 | ```js 69 | app.src('src/*.hbs', {layout: 'default'}); 70 | ``` 71 | 72 | ### [.symlink](index.js#L80) 73 | 74 | Glob patterns or paths for symlinks. 75 | 76 | **Params** 77 | 78 | * `glob` **{String|Array}** 79 | 80 | **Example** 81 | 82 | ```js 83 | app.symlink('src/**'); 84 | ``` 85 | 86 | ### [.dest](index.js#L95) 87 | 88 | Specify a destination for processed files. Runs `.preWrite` and `.postWrite` middleware handlers on all files. 89 | 90 | **Params** 91 | 92 | * `dest` **{String|Function}**: File path or rename function. 93 | * `options` **{Object}**: Options and locals to pass to `dest` plugins 94 | 95 | **Example** 96 | 97 | ```js 98 | app.dest('dist/'); 99 | ``` 100 | 101 | ## Release History 102 | 103 | **v2.0.0** 104 | 105 | * Major breaking changes based on v1.0 of Assemble! Requires Assemble v1.0 or above. 106 | 107 | **v0.6.0** 108 | 109 | * emit `end` on `app` when stream ends 110 | 111 | **v0.3.0** 112 | 113 | * breaking change! plugin is wrapped in a function that now be called when registered. e.g. `fs()`. This is to be consistent with assemble's plugin guidelines, and allows the plugin to be auto-loaded following the same format as other plugins. 114 | * rename `files` array to `streamFiles` 115 | * adds `onStream` middleware handler to `src` 116 | * adds `preWrite` middleware handler to `dest` 117 | 118 | ## About 119 | 120 |
121 | Contributing 122 | 123 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 124 | 125 |
126 | 127 |
128 | Running Tests 129 | 130 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: 131 | 132 | ```sh 133 | $ npm install && npm test 134 | ``` 135 | 136 |
137 | 138 |
139 | Building docs 140 | 141 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ 142 | 143 | To generate the readme, run the following command: 144 | 145 | ```sh 146 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 147 | ``` 148 | 149 |
150 | 151 | ### Related projects 152 | 153 | You might also be interested in these projects: 154 | 155 | * [generate](https://www.npmjs.com/package/generate): Command line tool and developer framework for scaffolding out new GitHub projects. Generate offers the… [more](https://github.com/generate/generate) | [homepage](https://github.com/generate/generate "Command line tool and developer framework for scaffolding out new GitHub projects. Generate offers the robustness and configurability of Yeoman, the expressiveness and simplicity of Slush, and more powerful flow control and composability than either.") 156 | * [update](https://www.npmjs.com/package/update): Be scalable! Update is a new, open source developer framework and CLI for automating updates… [more](https://github.com/update/update) | [homepage](https://github.com/update/update "Be scalable! Update is a new, open source developer framework and CLI for automating updates of any kind in code projects.") 157 | * [verb](https://www.npmjs.com/package/verb): Documentation generator for GitHub projects. Verb is extremely powerful, easy to use, and is used… [more](https://github.com/verbose/verb) | [homepage](https://github.com/verbose/verb "Documentation generator for GitHub projects. Verb is extremely powerful, easy to use, and is used on hundreds of projects of all sizes to generate everything from API docs to readmes.") 158 | 159 | ### Contributors 160 | 161 | | **Commits** | **Contributor** | 162 | | --- | --- | 163 | | 100 | [jonschlinkert](https://github.com/jonschlinkert) | 164 | | 11 | [doowb](https://github.com/doowb) | 165 | 166 | ### Author 167 | 168 | **Jon Schlinkert** 169 | 170 | * [GitHub Profile](https://github.com/jonschlinkert) 171 | * [Twitter Profile](https://twitter.com/jonschlinkert) 172 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) 173 | 174 | ### License 175 | 176 | Copyright © 2018, [Jon Schlinkert](https://github.com/jonschlinkert). 177 | Released under the [MIT License](LICENSE). 178 | 179 | *** 180 | 181 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on December 11, 2018._ -------------------------------------------------------------------------------- /test/app.src.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | const App = require('templates'); 6 | const vfs = require('..'); 7 | let app; 8 | 9 | describe('src()', () => { 10 | beforeEach(() => { 11 | app = new App(); 12 | app.create('pages'); 13 | app.use(vfs()); 14 | }); 15 | 16 | it('should return a stream', cb => { 17 | const stream = app.src(path.join(__dirname, 'fixtures/*.coffee')); 18 | assert(stream); 19 | assert.equal(typeof stream.on, 'function'); 20 | assert.equal(typeof stream.pipe, 'function'); 21 | cb(); 22 | }); 23 | 24 | it('should return an input stream from a flat glob', cb => { 25 | const stream = app.src(path.join(__dirname, 'fixtures/*.coffee')); 26 | stream.on('error', cb); 27 | stream.on('data', file => { 28 | assert(file); 29 | assert(file.path); 30 | assert(file.contents); 31 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 32 | assert.equal(String(file.contents), 'Hello world!'); 33 | }); 34 | stream.on('end', cb); 35 | }); 36 | 37 | it('should return an input stream for multiple globs', cb => { 38 | const globs = [ 39 | path.join(__dirname, 'fixtures/generic/run.dmc'), 40 | path.join(__dirname, 'fixtures/generic/test.dmc') 41 | ]; 42 | 43 | const stream = app.src(globs); 44 | const files = []; 45 | 46 | stream.on('error', cb); 47 | stream.on('data', file => { 48 | assert(file); 49 | assert(file.path); 50 | files.push(file); 51 | }); 52 | 53 | stream.on('end', () => { 54 | assert.equal(files.length, 2); 55 | assert.equal(files[0].path, globs[0]); 56 | assert.equal(files[1].path, globs[1]); 57 | cb(); 58 | }); 59 | }); 60 | 61 | it('should return an input stream for multiple globs with negation', cb => { 62 | let expectedPath = path.join(__dirname, 'fixtures/generic/run.dmc'); 63 | let globArray = [ 64 | path.join(__dirname, 'fixtures/generic/*.dmc'), 65 | '!' + path.join(__dirname, 'fixtures/generic/test.dmc') 66 | ]; 67 | let stream = app.src(globArray); 68 | 69 | let files = []; 70 | stream.on('error', cb); 71 | stream.on('data', file => { 72 | assert(file); 73 | assert(file.path); 74 | files.push(file); 75 | }); 76 | stream.on('end', () => { 77 | assert.equal(files.length, 1); 78 | assert.equal(files[0].path, expectedPath); 79 | cb(); 80 | }); 81 | }); 82 | 83 | it('should return an input stream with no contents when read is false', cb => { 84 | app.src(path.join(__dirname, 'fixtures/*.coffee'), { read: false }) 85 | .on('error', cb) 86 | .on('data', file => { 87 | assert(file); 88 | assert(file.path); 89 | assert(!file.contents); 90 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 91 | }) 92 | .on('end', cb); 93 | }); 94 | 95 | it('should not blow up when no files are matched', cb => { 96 | app 97 | .src(['test.js', 'foo/*.js']) 98 | .on('error', cb) 99 | .on('data', () => {}) 100 | .on('end', cb); 101 | }); 102 | 103 | it('should return an input stream with contents as stream when buffer is false', cb => { 104 | let stream = app.src(path.join(__dirname, 'fixtures/*.coffee'), { buffer: false }); 105 | stream.on('error', cb); 106 | stream.on('data', file => { 107 | assert(file); 108 | assert(file.path); 109 | assert(file.contents); 110 | let buf = ''; 111 | file.contents.on('data', function(d) { 112 | buf += d; 113 | }); 114 | file.contents.on('end', () => { 115 | assert.equal(buf, 'Hello world!'); 116 | cb(); 117 | }); 118 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 119 | }); 120 | }); 121 | 122 | it('should return an input stream from a deep glob', cb => { 123 | let stream = app.src(path.join(__dirname, 'fixtures/**/*.jade')); 124 | stream.on('error', cb); 125 | stream.on('data', file => { 126 | assert(file); 127 | assert(file.path); 128 | assert(file.contents); 129 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test/run.jade')); 130 | assert.equal(String(file.contents), 'test template'); 131 | }); 132 | stream.on('end', cb); 133 | }); 134 | 135 | it('should return an input stream from a deeper glob', cb => { 136 | let stream = app.src(path.join(__dirname, 'fixtures/**/*.dmc')); 137 | let a = 0; 138 | stream.on('error', cb); 139 | stream.on('data', () => { 140 | ++a; 141 | }); 142 | stream.on('end', () => { 143 | assert.equal(a, 2); 144 | cb(); 145 | }); 146 | }); 147 | 148 | it('should return a file stream from a flat path', cb => { 149 | let a = 0; 150 | let stream = app.src(path.join(__dirname, 'fixtures/test.coffee')); 151 | stream.on('error', cb); 152 | stream.on('data', file => { 153 | ++a; 154 | assert(file); 155 | assert(file.path); 156 | assert(file.contents); 157 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 158 | assert.equal(String(file.contents), 'Hello world!'); 159 | }); 160 | stream.on('end', () => { 161 | assert.equal(a, 1); 162 | cb(); 163 | }); 164 | }); 165 | 166 | it('should return a stream', cb => { 167 | let stream = app.src(path.join(__dirname, 'fixtures/*.coffee')); 168 | assert(stream); 169 | assert(stream.on); 170 | cb(); 171 | }); 172 | 173 | it('should return an input stream from a flat glob', cb => { 174 | let stream = app.src(path.join(__dirname, 'fixtures/*.coffee')); 175 | stream.on('error', cb); 176 | stream.on('data', file => { 177 | assert(file); 178 | assert(file.path); 179 | assert(file.contents); 180 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 181 | assert.equal(String(file.contents), 'Hello world!'); 182 | }); 183 | stream.on('end', cb); 184 | }); 185 | 186 | it('should return an input stream for multiple globs, with negation', cb => { 187 | let expectedPath = path.join(__dirname, 'fixtures/generic/run.dmc'); 188 | let globArray = [ 189 | path.join(__dirname, 'fixtures/generic/*.dmc'), 190 | '!' + path.join(__dirname, 'fixtures/generic/test.dmc') 191 | ]; 192 | let stream = app.src(globArray); 193 | 194 | let files = []; 195 | stream.on('error', cb); 196 | stream.on('data', file => { 197 | assert(file); 198 | assert(file.path); 199 | files.push(file); 200 | }); 201 | stream.on('end', () => { 202 | assert.equal(files.length, 1); 203 | assert.equal(files[0].path, expectedPath); 204 | cb(); 205 | }); 206 | }); 207 | 208 | it('should return an input stream with no contents when read is false', cb => { 209 | let stream = app.src(path.join(__dirname, 'fixtures/*.coffee'), { read: false }); 210 | stream.on('error', cb); 211 | stream.on('data', file => { 212 | assert(file); 213 | assert(file.path); 214 | assert(!file.contents); 215 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 216 | }); 217 | stream.on('end', cb); 218 | }); 219 | 220 | it('should return an input stream from a deep glob', cb => { 221 | app 222 | .src(path.join(__dirname, 'fixtures/**/*.jade')) 223 | .on('error', cb) 224 | .on('data', file => { 225 | assert(file); 226 | assert(file.path); 227 | assert(file.contents); 228 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test/run.jade')); 229 | assert.equal(String(file.contents), 'test template'); 230 | }) 231 | .on('end', () => { 232 | cb(); 233 | }); 234 | }); 235 | 236 | it('should return an input stream from a deeper glob', cb => { 237 | let stream = app.src(path.join(__dirname, 'fixtures/**/*.dmc')); 238 | let a = 0; 239 | stream.on('error', cb); 240 | stream.on('data', () => { 241 | ++a; 242 | }); 243 | stream.on('end', () => { 244 | assert.equal(a, 2); 245 | cb(); 246 | }); 247 | }); 248 | 249 | it('should return a file stream from a flat path', cb => { 250 | let a = 0; 251 | let stream = app.src(path.join(__dirname, 'fixtures/test.coffee')); 252 | stream.on('error', cb); 253 | stream.on('data', file => { 254 | ++a; 255 | assert(file); 256 | assert(file.path); 257 | assert(file.contents); 258 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 259 | assert.equal(String(file.contents), 'Hello world!'); 260 | }); 261 | stream.on('end', () => { 262 | assert.equal(a, 1); 263 | cb(); 264 | }); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /test/collection.src.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | const App = require('templates'); 6 | const fs = require('..'); 7 | let app, pages; 8 | 9 | describe('collection.src()', () => { 10 | beforeEach(() => { 11 | app = new App(); 12 | app.use(fs()); 13 | 14 | pages = app.create('pages'); 15 | }); 16 | 17 | it('should return a stream', cb => { 18 | let stream = pages.src(path.join(__dirname, 'fixtures/*.coffee')); 19 | assert(stream); 20 | assert.equal(typeof stream.on, 'function'); 21 | assert.equal(typeof stream.pipe, 'function'); 22 | cb(); 23 | }); 24 | 25 | it('should convert vinyl files to Templates files', cb => { 26 | let patterns = path.join(__dirname, 'fixtures/*.coffee'); 27 | pages.src(patterns) 28 | .on('error', cb) 29 | .on('data', file => { 30 | assert(file.constructor.isVinyl); 31 | assert(file.constructor.isFile); 32 | }) 33 | .on('end', cb); 34 | }); 35 | 36 | it('should add src files to the collection', cb => { 37 | let patterns = path.join(__dirname, 'fixtures/*.coffee'); 38 | 39 | pages.src(patterns) 40 | .on('data', file => { 41 | assert(pages.files); 42 | assert.equal(pages.files.size, 1); 43 | }) 44 | .on('error', cb) 45 | .on('end', cb); 46 | }); 47 | 48 | it('should work with files added with other methods', cb => { 49 | pages.set('a', {content: '...'}); 50 | pages.set('b', {content: '...'}); 51 | pages.set('c', {content: '...'}); 52 | 53 | let patterns = path.join(__dirname, 'fixtures/*.coffee'); 54 | pages.src(patterns) 55 | .on('error', cb) 56 | .on('data', file => { 57 | assert(pages.files); 58 | assert.equal(pages.files.size, 4); 59 | }) 60 | .on('end', cb); 61 | }); 62 | 63 | it('should return an input stream from a flat glob', cb => { 64 | let stream = pages.src(path.join(__dirname, 'fixtures/*.coffee')); 65 | stream.on('error', cb); 66 | stream.on('data', file => { 67 | assert(file); 68 | assert(file.path); 69 | assert(file.contents); 70 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 71 | assert.equal(String(file.contents), 'Hello world!'); 72 | }); 73 | 74 | stream.on('end', cb); 75 | }); 76 | 77 | it('should return an input stream for multiple globs', cb => { 78 | let globArray = [ 79 | path.join(__dirname, 'fixtures/generic/run.dmc'), 80 | path.join(__dirname, 'fixtures/generic/test.dmc') 81 | ]; 82 | 83 | let stream = pages.src(globArray); 84 | let files = []; 85 | 86 | stream.on('error', cb); 87 | stream.on('data', file => { 88 | assert(file); 89 | assert(file.path); 90 | files.push(file); 91 | }); 92 | 93 | stream.on('end', () => { 94 | assert.equal(files.length, 2); 95 | assert.equal(files[0].path, globArray[0]); 96 | assert.equal(files[1].path, globArray[1]); 97 | cb(); 98 | }); 99 | }); 100 | 101 | it('should return an input stream for multiple globs with negation', cb => { 102 | let expectedPath = path.join(__dirname, 'fixtures/generic/run.dmc'); 103 | let globArray = [ 104 | path.join(__dirname, 'fixtures/generic/*.dmc'), 105 | '!' + path.join(__dirname, 'fixtures/generic/test.dmc') 106 | ]; 107 | 108 | let stream = pages.src(globArray); 109 | let files = []; 110 | 111 | stream.on('error', cb); 112 | stream.on('data', file => { 113 | assert(file); 114 | assert(file.path); 115 | files.push(file); 116 | }); 117 | 118 | stream.on('end', () => { 119 | assert.equal(files.length, 1); 120 | assert.equal(files[0].path, expectedPath); 121 | cb(); 122 | }); 123 | }); 124 | 125 | it('should return an input stream with no contents when read is false', cb => { 126 | let stream = pages.src(path.join(__dirname, 'fixtures/*.coffee'), {read: false}); 127 | stream.on('error', cb); 128 | stream.on('data', file => { 129 | assert(file); 130 | assert(file.path); 131 | assert(!file.contents); 132 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 133 | }); 134 | 135 | stream.on('end', cb); 136 | }); 137 | 138 | it('should return an input stream with contents as stream when buffer is false', cb => { 139 | let stream = pages.src(path.join(__dirname, 'fixtures/*.coffee'), {buffer: false}); 140 | stream.on('error', cb); 141 | stream.on('data', file => { 142 | assert(file); 143 | assert(file.path); 144 | assert(file.contents); 145 | let buf = ''; 146 | file.contents.on('data', (d) => { 147 | buf += d; 148 | }); 149 | file.contents.on('end', () => { 150 | assert.equal(buf, 'Hello world!'); 151 | cb(); 152 | }); 153 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 154 | }); 155 | }); 156 | 157 | it('should return an input stream from a deep glob', cb => { 158 | let stream = pages.src(path.join(__dirname, 'fixtures/**/*.jade')); 159 | stream.on('error', cb); 160 | stream.on('data', file => { 161 | assert(file); 162 | assert(file.path); 163 | assert(file.contents); 164 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test/run.jade')); 165 | assert.equal(String(file.contents), 'test template'); 166 | }); 167 | 168 | stream.on('end', cb); 169 | }); 170 | 171 | it('should return an input stream from a deeper glob', cb => { 172 | let stream = pages.src(path.join(__dirname, 'fixtures/**/*.dmc')); 173 | let count = 0; 174 | stream.on('error', cb); 175 | stream.on('data', () => { 176 | ++count; 177 | }); 178 | stream.on('end', () => { 179 | assert.equal(count, 2); 180 | cb(); 181 | }); 182 | }); 183 | 184 | it('should return a file stream from a flat path', cb => { 185 | let stream = pages.src(path.join(__dirname, 'fixtures/test.coffee')); 186 | let count = 0; 187 | 188 | stream.on('error', cb); 189 | stream.on('data', file => { 190 | ++count; 191 | assert(file); 192 | assert(file.path); 193 | assert(file.contents); 194 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 195 | assert.equal(String(file.contents), 'Hello world!'); 196 | }); 197 | 198 | stream.on('end', () => { 199 | assert.equal(count, 1); 200 | cb(); 201 | }); 202 | }); 203 | 204 | it('should return an input stream from a flat glob', cb => { 205 | let stream = pages.src(path.join(__dirname, 'fixtures/*.coffee')); 206 | stream.on('error', cb); 207 | stream.on('data', file => { 208 | assert(file); 209 | assert(file.path); 210 | assert(file.contents); 211 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 212 | assert.equal(String(file.contents), 'Hello world!'); 213 | }); 214 | 215 | stream.on('end', cb); 216 | }); 217 | 218 | it('should return an input stream for multiple globs', cb => { 219 | let globArray = [ 220 | path.join(__dirname, 'fixtures/generic/run.dmc'), 221 | path.join(__dirname, 'fixtures/generic/test.dmc') 222 | ]; 223 | 224 | let stream = pages.src(globArray); 225 | let files = []; 226 | 227 | stream.on('error', cb); 228 | stream.on('data', file => { 229 | assert(file); 230 | assert(file.path); 231 | files.push(file); 232 | }); 233 | stream.on('end', () => { 234 | assert.equal(files.length, 2); 235 | assert.equal(files[0].path, globArray[0]); 236 | assert.equal(files[1].path, globArray[1]); 237 | cb(); 238 | }); 239 | }); 240 | 241 | it('should return an input stream for multiple globs, with negation', cb => { 242 | let expectedPath = path.join(__dirname, 'fixtures/generic/run.dmc'); 243 | let globArray = [ 244 | path.join(__dirname, 'fixtures/generic/*.dmc'), 245 | '!' + path.join(__dirname, 'fixtures/generic/test.dmc') 246 | ]; 247 | let stream = pages.src(globArray); 248 | let files = []; 249 | 250 | stream.on('error', cb); 251 | stream.on('data', file => { 252 | assert(file); 253 | assert(file.path); 254 | files.push(file); 255 | }); 256 | 257 | stream.on('end', () => { 258 | assert.equal(files.length, 1); 259 | assert.equal(files[0].path, expectedPath); 260 | cb(); 261 | }); 262 | }); 263 | 264 | it('should return an input stream with no contents when read is false', cb => { 265 | let stream = pages.src(path.join(__dirname, 'fixtures/*.coffee'), {read: false}); 266 | stream.on('data', file => { 267 | assert(file); 268 | assert(file.path); 269 | assert(!file.contents); 270 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 271 | }); 272 | stream.on('error', cb); 273 | stream.on('end', cb); 274 | }); 275 | 276 | it('should return an input stream from a deep glob', cb => { 277 | let piped = false; 278 | pages.src(path.join(__dirname, 'fixtures/*.txt')) 279 | .on('data', file => { 280 | piped = true; 281 | assert(file); 282 | assert(file.path); 283 | assert(file.contents); 284 | }) 285 | .on('error', cb) 286 | .on('end', () => { 287 | assert(piped); 288 | cb(); 289 | }); 290 | }); 291 | 292 | it('should return an input stream from a deeper glob', cb => { 293 | let stream = pages.src(path.join(__dirname, 'fixtures/**/*.dmc')); 294 | let count = 0; 295 | stream.on('error', cb); 296 | stream.on('data', () => { 297 | ++count; 298 | }); 299 | stream.on('end', () => { 300 | assert.equal(count, 2); 301 | cb(); 302 | }); 303 | }); 304 | 305 | it('should return a file stream from a flat path', cb => { 306 | let count = 0; 307 | let stream = pages.src(path.join(__dirname, 'fixtures/test.coffee')); 308 | stream.on('error', cb); 309 | stream.on('data', file => { 310 | ++count; 311 | assert(file); 312 | assert(file.path); 313 | assert(file.contents); 314 | assert.equal(path.join(file.path, ''), path.join(__dirname, 'fixtures/test.coffee')); 315 | assert.equal(String(file.contents), 'Hello world!'); 316 | }); 317 | stream.on('end', () => { 318 | assert.equal(count, 1); 319 | cb(); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /test/vfs/dest-modes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const File = require('vinyl'); 5 | const expect = require('expect'); 6 | 7 | const cleanup = require('./utils/cleanup'); 8 | const statMode = require('./utils/stat-mode'); 9 | const mockError = require('./utils/mock-error'); 10 | const isWindows = require('./utils/is-windows'); 11 | const applyUmask = require('./utils/apply-umask'); 12 | const always = require('./utils/always'); 13 | const testConstants = require('./utils/test-constants'); 14 | 15 | const { concat, from, pipe } = require('mississippi'); 16 | const inputBase = testConstants.inputBase; 17 | const outputBase = testConstants.outputBase; 18 | const inputPath = testConstants.inputPath; 19 | const outputPath = testConstants.outputPath; 20 | const inputDirpath = testConstants.inputDirpath; 21 | const outputDirpath = testConstants.outputDirpath; 22 | const inputNestedPath = testConstants.inputNestedPath; 23 | const outputNestedPath = testConstants.outputNestedPath; 24 | const contents = testConstants.contents; 25 | const clean = cleanup(outputBase); 26 | const App = require('templates'); 27 | let plugin = require('../..'); 28 | let vfs; 29 | 30 | describe('.dest() with custom modes', function() { 31 | beforeEach(() => { 32 | vfs = new App(); 33 | vfs.use(plugin()); 34 | }); 35 | 36 | beforeEach(clean); 37 | afterEach(clean); 38 | 39 | it('sets the mode of a written buffer file if set on the vinyl object', function(done) { 40 | // Changing the mode of a file is not supported by node.js in Windows. 41 | // Windows is treated as though it does not have permission to make this operation. 42 | if (isWindows) { 43 | this.skip(); 44 | return; 45 | } 46 | 47 | let expectedMode = applyUmask('677'); 48 | let file = new File({ 49 | base: inputBase, 50 | path: inputPath, 51 | contents: Buffer.from(contents), 52 | stat: { 53 | mode: expectedMode, 54 | }, 55 | }); 56 | 57 | function assert() { 58 | expect(statMode(outputPath)).toEqual(expectedMode); 59 | } 60 | 61 | pipe([ 62 | from.obj([file]), 63 | vfs.dest(outputBase, { cwd: __dirname }), 64 | concat(assert), 65 | ], done); 66 | }); 67 | 68 | it('sets the sticky bit on the mode of a written stream file if set on the vinyl object', function(done) { 69 | if (isWindows) { 70 | this.skip(); 71 | return; 72 | } 73 | 74 | let expectedMode = applyUmask('1677'); 75 | 76 | let file = new File({ 77 | base: inputBase, 78 | path: inputPath, 79 | contents: from([contents]), 80 | stat: { 81 | mode: expectedMode, 82 | }, 83 | }); 84 | 85 | function assert() { 86 | expect(statMode(outputPath)).toEqual(expectedMode); 87 | } 88 | 89 | pipe([ 90 | from.obj([file]), 91 | vfs.dest(outputBase, { cwd: __dirname }), 92 | concat(assert), 93 | ], done); 94 | }); 95 | 96 | it('sets the mode of a written stream file if set on the vinyl object', function(done) { 97 | if (isWindows) { 98 | this.skip(); 99 | return; 100 | } 101 | 102 | let expectedMode = applyUmask('677'); 103 | 104 | let file = new File({ 105 | base: inputBase, 106 | path: inputPath, 107 | contents: from([contents]), 108 | stat: { 109 | mode: expectedMode, 110 | }, 111 | }); 112 | 113 | function assert() { 114 | expect(statMode(outputPath)).toEqual(expectedMode); 115 | } 116 | 117 | pipe([ 118 | from.obj([file]), 119 | vfs.dest(outputBase, { cwd: __dirname }), 120 | concat(assert), 121 | ], done); 122 | }); 123 | 124 | it('sets the mode of a written directory if set on the vinyl object', function(done) { 125 | if (isWindows) { 126 | this.skip(); 127 | return; 128 | } 129 | 130 | let expectedMode = applyUmask('677'); 131 | 132 | let file = new File({ 133 | base: inputBase, 134 | path: inputDirpath, 135 | contents: null, 136 | stat: { 137 | isDirectory: always(true), 138 | mode: expectedMode, 139 | }, 140 | }); 141 | 142 | function assert() { 143 | expect(statMode(outputDirpath)).toEqual(expectedMode); 144 | } 145 | 146 | pipe([ 147 | from.obj([file]), 148 | vfs.dest(outputBase, { cwd: __dirname }), 149 | concat(assert), 150 | ], done); 151 | }); 152 | 153 | it('sets sticky bit on the mode of a written directory if set on the vinyl object', function(done) { 154 | if (isWindows) { 155 | this.skip(); 156 | return; 157 | } 158 | 159 | let expectedMode = applyUmask('1677'); 160 | 161 | let file = new File({ 162 | base: inputBase, 163 | path: inputDirpath, 164 | contents: null, 165 | stat: { 166 | isDirectory: always(true), 167 | mode: expectedMode, 168 | }, 169 | }); 170 | 171 | function assert() { 172 | expect(statMode(outputDirpath)).toEqual(expectedMode); 173 | } 174 | 175 | pipe([ 176 | from.obj([file]), 177 | vfs.dest(outputBase, { cwd: __dirname }), 178 | concat(assert), 179 | ], done); 180 | }); 181 | 182 | it('writes new files with the mode specified in options', function(done) { 183 | if (isWindows) { 184 | this.skip(); 185 | return; 186 | } 187 | 188 | let expectedMode = applyUmask('777'); 189 | 190 | let file = new File({ 191 | base: inputBase, 192 | path: inputPath, 193 | contents: Buffer.from(contents), 194 | }); 195 | 196 | function assert() { 197 | expect(statMode(outputPath)).toEqual(expectedMode); 198 | } 199 | 200 | pipe([ 201 | from.obj([file]), 202 | vfs.dest(outputBase, { cwd: __dirname, mode: expectedMode }), 203 | concat(assert), 204 | ], done); 205 | }); 206 | 207 | it('updates the file mode to match the vinyl mode', function(done) { 208 | if (isWindows) { 209 | this.skip(); 210 | return; 211 | } 212 | 213 | let startMode = applyUmask('655'); 214 | let expectedMode = applyUmask('722'); 215 | 216 | let file = new File({ 217 | base: inputBase, 218 | path: inputPath, 219 | contents: Buffer.from(contents), 220 | stat: { 221 | mode: expectedMode, 222 | }, 223 | }); 224 | 225 | function assert() { 226 | expect(statMode(outputPath)).toEqual(expectedMode); 227 | } 228 | 229 | fs.mkdirSync(outputBase); 230 | fs.closeSync(fs.openSync(outputPath, 'w')); 231 | fs.chmodSync(outputPath, startMode); 232 | 233 | pipe([ 234 | from.obj([file]), 235 | vfs.dest(outputBase, { cwd: __dirname }), 236 | concat(assert), 237 | ], done); 238 | }); 239 | 240 | it('updates the directory mode to match the vinyl mode', function(done) { 241 | if (isWindows) { 242 | this.skip(); 243 | return; 244 | } 245 | 246 | let startMode = applyUmask('2777'); 247 | let expectedMode = applyUmask('727'); 248 | 249 | let file1 = new File({ 250 | base: inputBase, 251 | path: outputDirpath, 252 | stat: { 253 | isDirectory: always(true), 254 | mode: startMode, 255 | }, 256 | }); 257 | let file2 = new File({ 258 | base: inputBase, 259 | path: outputDirpath, 260 | stat: { 261 | isDirectory: always(true), 262 | mode: expectedMode, 263 | }, 264 | }); 265 | 266 | function assert() { 267 | expect(statMode(outputDirpath)).toEqual(expectedMode); 268 | } 269 | 270 | pipe([ 271 | from.obj([file1, file2]), 272 | vfs.dest(outputBase, { cwd: __dirname }), 273 | concat(assert), 274 | ], done); 275 | }); 276 | 277 | it('uses different modes for files and directories', function(done) { 278 | if (isWindows) { 279 | this.skip(); 280 | return; 281 | } 282 | 283 | let expectedDirMode = applyUmask('2777'); 284 | let expectedFileMode = applyUmask('755'); 285 | 286 | let file = new File({ 287 | base: inputBase, 288 | path: inputNestedPath, 289 | contents: Buffer.from(contents), 290 | }); 291 | 292 | function assert() { 293 | expect(statMode(outputDirpath)).toEqual(expectedDirMode); 294 | expect(statMode(outputNestedPath)).toEqual(expectedFileMode); 295 | } 296 | 297 | pipe([ 298 | from.obj([file]), 299 | vfs.dest(outputBase, { 300 | cwd: __dirname, 301 | mode: expectedFileMode, 302 | dirMode: expectedDirMode, 303 | }), 304 | concat(assert), 305 | ], done); 306 | }); 307 | 308 | it('does not fchmod a matching file', function(done) { 309 | if (isWindows) { 310 | this.skip(); 311 | return; 312 | } 313 | 314 | let fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); 315 | 316 | let expectedMode = applyUmask('777'); 317 | 318 | let file = new File({ 319 | base: inputBase, 320 | path: inputPath, 321 | contents: Buffer.from(contents), 322 | stat: { 323 | mode: expectedMode, 324 | }, 325 | }); 326 | 327 | function assert() { 328 | expect(fchmodSpy.calls.length).toEqual(0); 329 | expect(statMode(outputPath)).toEqual(expectedMode); 330 | } 331 | 332 | pipe([ 333 | from.obj([file]), 334 | vfs.dest(outputBase, { cwd: __dirname }), 335 | concat(assert), 336 | ], done); 337 | }); 338 | 339 | it('sees a file with special chmod (setuid/setgid/sticky) as distinct', function(done) { 340 | if (isWindows) { 341 | this.skip(); 342 | return; 343 | } 344 | 345 | let fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); 346 | 347 | let startMode = applyUmask('3722'); 348 | let expectedMode = applyUmask('722'); 349 | 350 | let file = new File({ 351 | base: inputBase, 352 | path: inputPath, 353 | contents: Buffer.from(contents), 354 | stat: { 355 | mode: expectedMode, 356 | }, 357 | }); 358 | 359 | function assert() { 360 | expect(fchmodSpy.calls.length).toEqual(1); 361 | } 362 | 363 | fs.mkdirSync(outputBase); 364 | fs.closeSync(fs.openSync(outputPath, 'w')); 365 | fs.chmodSync(outputPath, startMode); 366 | 367 | pipe([ 368 | from.obj([file]), 369 | vfs.dest(outputBase, { cwd: __dirname }), 370 | concat(assert), 371 | ], done); 372 | }); 373 | 374 | it('reports fchmod errors', function(done) { 375 | if (isWindows) { 376 | this.skip(); 377 | return; 378 | } 379 | 380 | let expectedMode = applyUmask('722'); 381 | let fchmodSpy = expect.spyOn(fs, 'fchmod').andCall(mockError); 382 | 383 | let file = new File({ 384 | base: inputBase, 385 | path: inputPath, 386 | contents: Buffer.from(contents), 387 | stat: { 388 | mode: expectedMode, 389 | }, 390 | }); 391 | 392 | function assert(err) { 393 | expect(err).toExist(); 394 | expect(fchmodSpy.calls.length).toEqual(1); 395 | done(); 396 | } 397 | 398 | fs.mkdirSync(outputBase); 399 | fs.closeSync(fs.openSync(outputPath, 'w')); 400 | 401 | pipe([ 402 | from.obj([file]), 403 | vfs.dest(outputBase, { cwd: __dirname }), 404 | ], assert); 405 | }); 406 | }); 407 | -------------------------------------------------------------------------------- /test/vfs/src.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var fs = require('graceful-fs'); 6 | var assert = require('assert'); 7 | var File = require('vinyl'); 8 | var expect = require('expect'); 9 | var miss = require('mississippi'); 10 | 11 | const App = require('templates'); 12 | let plugin = require('../..'); 13 | let vfs; 14 | 15 | var testConstants = require('./utils/test-constants'); 16 | 17 | var pipe = miss.pipe; 18 | var from = miss.from; 19 | var concat = miss.concat; 20 | var through = miss.through; 21 | 22 | var inputBase = testConstants.inputBase; 23 | var inputPath = testConstants.inputPath; 24 | var inputDirpath = testConstants.inputDirpath; 25 | var bomInputPath = testConstants.bomInputPath; 26 | var beEncodedInputPath = testConstants.beEncodedInputPath; 27 | var leEncodedInputPath = testConstants.leEncodedInputPath; 28 | var contents = testConstants.contents; 29 | 30 | describe('.src()', function() { 31 | beforeEach(() => { 32 | vfs = new App(); 33 | vfs.use(plugin()); 34 | }); 35 | 36 | it('throws on invalid glob (empty)', function(done) { 37 | var stream; 38 | try { 39 | stream = vfs.src(); 40 | } catch (err) { 41 | assert(err, 'expected an error'); 42 | expect(stream).toNotExist(); 43 | done(); 44 | } 45 | }); 46 | 47 | it('throws on invalid glob (empty string)', function(done) { 48 | var stream; 49 | try { 50 | stream = vfs.src(''); 51 | } catch (err) { 52 | assert(err, 'expected an error'); 53 | expect(stream).toNotExist(); 54 | done(); 55 | } 56 | }); 57 | 58 | it('throws on invalid glob (number)', function(done) { 59 | var stream; 60 | try { 61 | stream = vfs.src(123); 62 | } catch (err) { 63 | assert(err, 'expected an error'); 64 | expect(stream).toNotExist(); 65 | done(); 66 | } 67 | }); 68 | 69 | it('throws on invalid glob (nested array)', function(done) { 70 | var stream; 71 | try { 72 | stream = vfs.src([['./fixtures/*.coffee']]); 73 | } catch (err) { 74 | assert(err, 'expected an error'); 75 | expect(stream).toNotExist(); 76 | expect(err.message).toInclude('Invalid glob argument'); 77 | done(); 78 | } 79 | }); 80 | 81 | it('throws on invalid glob (empty string in array)', function(done) { 82 | var stream; 83 | try { 84 | stream = vfs.src(['']); 85 | } catch (err) { 86 | assert(err, 'expected an error'); 87 | expect(stream).toNotExist(); 88 | done(); 89 | } 90 | }); 91 | 92 | it('throws on invalid glob (empty array)', function(done) { 93 | var stream; 94 | try { 95 | stream = vfs.src([]); 96 | } catch (err) { 97 | assert(err, 'expected an error'); 98 | expect(stream).toNotExist(); 99 | done(); 100 | } 101 | }); 102 | 103 | it('emits an error on file not existing', function(done) { 104 | function compare(err) { 105 | done(); 106 | } 107 | 108 | pipe([ 109 | vfs.src('./fixtures/noexist.coffee', { allowEmpty: false }), 110 | concat(), 111 | ], compare); 112 | }); 113 | 114 | it('passes through writes', function(done) { 115 | var file = new File({ 116 | base: inputBase, 117 | path: inputPath, 118 | contents: Buffer.from(contents), 119 | stat: fs.statSync(inputPath), 120 | }); 121 | 122 | var srcStream = vfs.src(inputPath); 123 | 124 | function compare(files) { 125 | expect(files.length).toEqual(2); 126 | expect(files[0]).toEqual(file); 127 | } 128 | 129 | srcStream.write(file); 130 | 131 | pipe([ 132 | srcStream, 133 | concat(compare), 134 | ], done); 135 | }); 136 | 137 | it('removes BOM from utf8-encoded files by default', function(done) { 138 | // U+FEFF takes up 3 bytes in UTF-8: http://mothereff.in/utf-8#%EF%BB%BF 139 | var expectedContent = fs.readFileSync(bomInputPath).slice(3); 140 | 141 | function compare(files) { 142 | expect(files.length).toEqual(1); 143 | expect(files[0].contents).toMatch(expectedContent); 144 | } 145 | 146 | pipe([ 147 | vfs.src(bomInputPath), 148 | concat(compare), 149 | ], done); 150 | }); 151 | 152 | it('does not remove BOM from utf8-encoded files if option is false', function(done) { 153 | var expectedContent = fs.readFileSync(bomInputPath); 154 | 155 | function compare(files) { 156 | expect(files.length).toEqual(1); 157 | expect(files[0].contents).toMatch(expectedContent); 158 | } 159 | 160 | pipe([ 161 | vfs.src(bomInputPath, { removeBOM: false }), 162 | concat(compare), 163 | ], done); 164 | }); 165 | 166 | // This goes for any non-UTF-8 encoding. 167 | // UTF-16-BE is enough to demonstrate this is done properly. 168 | it('does not remove anything that looks like a utf8-encoded BOM from utf16be-encoded files', function(done) { 169 | var expectedContent = fs.readFileSync(beEncodedInputPath); 170 | 171 | function compare(files) { 172 | expect(files.length).toEqual(1); 173 | expect(files[0].contents).toMatch(expectedContent); 174 | }; 175 | 176 | pipe([ 177 | vfs.src(beEncodedInputPath), 178 | concat(compare), 179 | ], done); 180 | }); 181 | 182 | it('does not remove anything that looks like a utf8-encoded BOM from utf16be-encoded files with streaming contents', function(done) { 183 | var expectedContent = fs.readFileSync(beEncodedInputPath); 184 | 185 | function compareContent(contents) { 186 | expect(contents).toMatch(expectedContent); 187 | } 188 | 189 | function compareContents(file, enc, cb) { 190 | pipe([ 191 | file.contents, 192 | concat(compareContent), 193 | ], function(err) { 194 | cb(err, file); 195 | }); 196 | } 197 | 198 | function compare(files) { 199 | expect(files.length).toEqual(1); 200 | expect(files[0].isStream()).toEqual(true); 201 | } 202 | 203 | pipe([ 204 | vfs.src(beEncodedInputPath, { buffer: false }), 205 | through.obj(compareContents), 206 | concat(compare), 207 | ], done); 208 | }); 209 | 210 | // This goes for any non-UTF-8 encoding. 211 | // UTF-16-LE is enough to demonstrate this is done properly. 212 | it('does not remove anything that looks like a utf8-encoded BOM from utf16le-encoded files', function(done) { 213 | var expectedContent = fs.readFileSync(leEncodedInputPath); 214 | 215 | function compare(files) { 216 | expect(files.length).toEqual(1); 217 | expect(files[0].contents).toMatch(expectedContent); 218 | } 219 | 220 | pipe([ 221 | vfs.src(leEncodedInputPath), 222 | concat(compare), 223 | ], done); 224 | }); 225 | 226 | it('does not remove anything that looks like a utf8-encoded BOM from utf16le-encoded files with streaming contents', function(done) { 227 | var expectedContent = fs.readFileSync(leEncodedInputPath); 228 | 229 | function compareContent(contents) { 230 | expect(contents).toMatch(expectedContent); 231 | } 232 | 233 | function compareContents(file, enc, cb) { 234 | pipe([ 235 | file.contents, 236 | concat(compareContent), 237 | ], function(err) { 238 | cb(err, file); 239 | }); 240 | } 241 | 242 | function compare(files) { 243 | expect(files.length).toEqual(1); 244 | expect(files[0].isStream()).toEqual(true); 245 | } 246 | 247 | pipe([ 248 | vfs.src(leEncodedInputPath, { buffer: false }), 249 | through.obj(compareContents), 250 | concat(compare), 251 | ], done); 252 | }); 253 | 254 | it('globs files with default settings', function(done) { 255 | function compare(files) { 256 | expect(files.length).toEqual(4); 257 | } 258 | 259 | pipe([ 260 | vfs.src('./fixtures/*.txt', { cwd: __dirname }), 261 | concat(compare), 262 | ], done); 263 | }); 264 | 265 | it('globs files with default settings and relative cwd', function(done) { 266 | var cwd = path.relative(process.cwd(), __dirname); 267 | 268 | function compare(files) { 269 | expect(files.length).toEqual(4); 270 | } 271 | 272 | pipe([ 273 | vfs.src('./fixtures/*.txt', { cwd: cwd }), 274 | concat(compare), 275 | ], done); 276 | }); 277 | 278 | // TODO: need to normalize the path of a directory vinyl object 279 | it('globs a directory with default settings', function(done) { 280 | var inputDirGlob = path.join(inputBase, './f*/'); 281 | 282 | function compare(files) { 283 | expect(files.length).toEqual(1); 284 | expect(files[0].isNull()).toEqual(true); 285 | expect(files[0].isDirectory()).toEqual(true); 286 | } 287 | 288 | pipe([ 289 | vfs.src(inputDirGlob), 290 | concat(compare), 291 | ], done); 292 | }); 293 | 294 | it('globs a directory with default settings and relative cwd', function(done) { 295 | var cwd = path.relative(process.cwd(), __dirname); 296 | 297 | function compare(files) { 298 | expect(files.length).toEqual(1); 299 | expect(files[0].isNull()).toEqual(true); 300 | expect(files[0].isDirectory()).toEqual(true); 301 | } 302 | 303 | pipe([ 304 | vfs.src('./fixtures/f*/', { cwd: cwd }), 305 | concat(compare), 306 | ], done); 307 | }); 308 | 309 | it('streams a directory with default settings', function(done) { 310 | function compare(files) { 311 | expect(files.length).toEqual(1); 312 | expect(files[0].path).toEqual(inputDirpath); 313 | expect(files[0].isNull()).toEqual(true); 314 | expect(files[0].isDirectory()).toEqual(true); 315 | } 316 | 317 | pipe([ 318 | vfs.src(inputDirpath), 319 | concat(compare), 320 | ], done); 321 | }); 322 | 323 | it('streams file with with no contents using read: false option', function(done) { 324 | function compare(files) { 325 | expect(files.length).toEqual(1); 326 | expect(files[0].path).toEqual(inputPath); 327 | expect(files[0].isNull()).toEqual(true); 328 | expect(files[0].contents).toNotExist(); 329 | } 330 | 331 | pipe([ 332 | vfs.src(inputPath, { read: false }), 333 | concat(compare), 334 | ], done); 335 | }); 336 | 337 | it('streams a file changed after since', function(done) { 338 | var lastUpdateDate = new Date(+fs.statSync(inputPath).mtime - 1000); 339 | 340 | function compare(files) { 341 | expect(files.length).toEqual(1); 342 | expect(files[0].path).toEqual(inputPath); 343 | } 344 | 345 | pipe([ 346 | vfs.src(inputPath, { since: lastUpdateDate }), 347 | concat(compare), 348 | ], done); 349 | }); 350 | 351 | it('does not stream a file changed before since', function(done) { 352 | var lastUpdateDate = new Date(+fs.statSync(inputPath).mtime + 1000); 353 | 354 | function compare(files) { 355 | expect(files.length).toEqual(0); 356 | } 357 | 358 | pipe([ 359 | vfs.src(inputPath, { since: lastUpdateDate }), 360 | concat(compare), 361 | ], done); 362 | }); 363 | 364 | it('streams a file with streaming contents', function(done) { 365 | var expectedContent = fs.readFileSync(inputPath); 366 | 367 | function compareContent(contents) { 368 | expect(contents).toMatch(expectedContent); 369 | } 370 | 371 | function compareContents(file, enc, cb) { 372 | pipe([ 373 | file.contents, 374 | concat(compareContent), 375 | ], function(err) { 376 | cb(err, file); 377 | }); 378 | } 379 | 380 | function compare(files) { 381 | expect(files.length).toEqual(1); 382 | expect(files[0].path).toEqual(inputPath); 383 | expect(files[0].isStream()).toEqual(true); 384 | } 385 | 386 | pipe([ 387 | vfs.src(inputPath, { buffer: false }), 388 | through.obj(compareContents), 389 | concat(compare), 390 | ], done); 391 | }); 392 | 393 | it('can be used as a through stream and adds new files to the end', function(done) { 394 | var file = new File({ 395 | base: inputBase, 396 | path: inputPath, 397 | contents: fs.readFileSync(inputPath), 398 | stat: fs.statSync(inputPath), 399 | }); 400 | 401 | function compare(files) { 402 | expect(files.length).toEqual(2); 403 | expect(files[0]).toEqual(file); 404 | } 405 | 406 | pipe([ 407 | from.obj([file]), 408 | vfs.src(inputPath), 409 | concat(compare), 410 | ], done); 411 | }); 412 | 413 | it('can be used at beginning and in the middle', function(done) { 414 | function compare(files) { 415 | assert.equal(files.length, 2); 416 | } 417 | 418 | pipe([ 419 | vfs.src(inputPath), 420 | vfs.src(inputPath), 421 | concat(compare), 422 | ], done); 423 | }); 424 | 425 | it('does not pass options on to through2', function(done) { 426 | // Reference: https://github.com/gulpjs/vinyl-fs/issues/153 427 | var read = expect.createSpy().andReturn(false); 428 | 429 | function compare() { 430 | // Called once to resolve the option 431 | expect(read.calls.length).toEqual(1); 432 | } 433 | 434 | pipe([ 435 | vfs.src(inputPath, { read: read }), 436 | concat(compare), 437 | ], done); 438 | }); 439 | }); 440 | -------------------------------------------------------------------------------- /test/vfs/dest-symlinks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const fs = require('graceful-fs'); 6 | const File = require('vinyl'); 7 | const expect = require('expect'); 8 | 9 | const cleanup = require('./utils/cleanup'); 10 | const isWindows = require('./utils/is-windows'); 11 | const always = require('./utils/always'); 12 | const testConstants = require('./utils/test-constants'); 13 | 14 | const { concat, from, pipe } = require('mississippi'); 15 | 16 | const inputBase = testConstants.inputBase; 17 | const outputBase = testConstants.outputBase; 18 | const inputPath = testConstants.inputPath; 19 | const outputPath = testConstants.outputPath; 20 | const inputDirpath = testConstants.inputDirpath; 21 | const outputDirpath = testConstants.outputDirpath; 22 | const contents = testConstants.contents; 23 | // For not-exists tests 24 | const neInputBase = testConstants.neInputBase; 25 | const neOutputBase = testConstants.neOutputBase; 26 | const neInputDirpath = testConstants.neInputDirpath; 27 | const neOutputDirpath = testConstants.neOutputDirpath; 28 | const clean = cleanup(outputBase); 29 | const App = require('templates'); 30 | let plugin = require('../..'); 31 | let vfs; 32 | 33 | describe('.dest() with symlinks', function() { 34 | beforeEach(() => { 35 | vfs = new App(); 36 | vfs.use(plugin()); 37 | }); 38 | 39 | beforeEach(clean); 40 | afterEach(clean); 41 | 42 | it('creates symlinks when `file.isSymbolic()` is true', function(done) { 43 | let file = new File({ 44 | base: inputBase, 45 | path: inputPath, 46 | contents: null, 47 | stat: { 48 | isSymbolicLink: always(true), 49 | }, 50 | }); 51 | 52 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 53 | file.symlink = inputPath; 54 | 55 | function assert(files) { 56 | var symlink = fs.readlinkSync(outputPath); 57 | 58 | expect(files.length).toEqual(1); 59 | expect(file.symlink).toEqual(symlink); 60 | expect(files[0].symlink).toEqual(symlink); 61 | expect(files[0].isSymbolic()).toBe(true); 62 | expect(files[0].path).toEqual(outputPath); 63 | } 64 | 65 | pipe([ 66 | from.obj([file]), 67 | vfs.dest(outputBase), 68 | concat(assert), 69 | ], done); 70 | }); 71 | 72 | it('does not create symlinks when `file.isSymbolic()` is false', function(done) { 73 | var file = new File({ 74 | base: inputBase, 75 | path: inputPath, 76 | contents: null, 77 | stat: { 78 | isSymbolicLink: always(false), 79 | }, 80 | }); 81 | 82 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 83 | file.symlink = inputPath; 84 | 85 | function assert(files) { 86 | var symlinkExists = fs.existsSync(outputPath); 87 | 88 | expect(files.length).toEqual(1); 89 | expect(symlinkExists).toBe(false); 90 | } 91 | 92 | pipe([ 93 | from.obj([file]), 94 | vfs.dest(outputBase), 95 | concat(assert), 96 | ], done); 97 | }); 98 | 99 | it('errors if missing a `.symlink` property', function(done) { 100 | var file = new File({ 101 | base: inputBase, 102 | path: inputPath, 103 | contents: null, 104 | stat: { 105 | isSymbolicLink: always(true), 106 | }, 107 | }); 108 | 109 | function assert(err) { 110 | expect(err).toExist(); 111 | expect(err.message).toEqual('Missing symlink property on symbolic vinyl'); 112 | done(); 113 | } 114 | 115 | pipe([ 116 | from.obj([file]), 117 | vfs.dest(outputBase), 118 | ], assert); 119 | }); 120 | 121 | it('emits Vinyl files that are (still) symbolic', function(done) { 122 | var file = new File({ 123 | base: inputBase, 124 | path: inputPath, 125 | contents: null, 126 | stat: { 127 | isSymbolicLink: always(true), 128 | }, 129 | }); 130 | 131 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 132 | file.symlink = inputPath; 133 | 134 | function assert(files) { 135 | expect(files.length).toEqual(1); 136 | expect(files[0].isSymbolic()).toEqual(true); 137 | } 138 | 139 | pipe([ 140 | from.obj([file]), 141 | vfs.dest(outputBase), 142 | concat(assert), 143 | ], done); 144 | }); 145 | 146 | it('can create relative links', function(done) { 147 | var file = new File({ 148 | base: inputBase, 149 | path: inputPath, 150 | contents: null, 151 | stat: { 152 | isSymbolicLink: always(true), 153 | }, 154 | }); 155 | 156 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 157 | file.symlink = inputPath; 158 | 159 | function assert(files) { 160 | var outputLink = fs.readlinkSync(outputPath); 161 | 162 | expect(files.length).toEqual(1); 163 | expect(outputLink).toEqual(path.normalize('../fixtures/test.txt')); 164 | expect(files[0].isSymbolic()).toBe(true); 165 | } 166 | 167 | pipe([ 168 | from.obj([file]), 169 | vfs.dest(outputBase, { relativeSymlinks: true }), 170 | concat(assert), 171 | ], done); 172 | }); 173 | 174 | it('(*nix) creates a link for a directory', function(done) { 175 | if (isWindows) { 176 | this.skip(); 177 | return; 178 | } 179 | 180 | var file = new File({ 181 | base: inputBase, 182 | path: inputDirpath, 183 | contents: null, 184 | stat: { 185 | isSymbolicLink: always(true), 186 | }, 187 | }); 188 | 189 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 190 | file.symlink = inputDirpath; 191 | 192 | function assert(files) { 193 | var stats = fs.statSync(outputDirpath); 194 | var lstats = fs.lstatSync(outputDirpath); 195 | var outputLink = fs.readlinkSync(outputDirpath); 196 | 197 | expect(files.length).toEqual(1); 198 | expect(outputLink).toEqual(inputDirpath); 199 | expect(stats.isDirectory()).toEqual(true); 200 | expect(lstats.isDirectory()).toEqual(false); 201 | } 202 | 203 | pipe([ 204 | from.obj([file]), 205 | vfs.dest(outputBase), 206 | concat(assert), 207 | ], done); 208 | }); 209 | 210 | it('(windows) creates a junction for a directory', function(done) { 211 | if (!isWindows) { 212 | this.skip(); 213 | return; 214 | } 215 | 216 | var file = new File({ 217 | base: inputBase, 218 | path: inputDirpath, 219 | contents: null, 220 | stat: { 221 | isSymbolicLink: always(true), 222 | }, 223 | }); 224 | 225 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 226 | file.symlink = inputDirpath; 227 | 228 | function assert(files) { 229 | var stats = fs.statSync(outputDirpath); 230 | var lstats = fs.lstatSync(outputDirpath); 231 | var outputLink = fs.readlinkSync(outputDirpath); 232 | 233 | expect(files.length).toEqual(1); 234 | // When creating a junction, it seems Windows appends a separator 235 | expect(outputLink).toEqual(inputDirpath + path.sep); 236 | expect(stats.isDirectory()).toEqual(true); 237 | expect(lstats.isDirectory()).toEqual(false); 238 | } 239 | 240 | pipe([ 241 | from.obj([file]), 242 | vfs.dest(outputBase), 243 | concat(assert), 244 | ], done); 245 | }); 246 | 247 | it('(windows) options can disable junctions for a directory', function(done) { 248 | if (!isWindows) { 249 | this.skip(); 250 | return; 251 | } 252 | 253 | var file = new File({ 254 | base: inputBase, 255 | path: inputDirpath, 256 | contents: null, 257 | stat: { 258 | isSymbolicLink: always(true), 259 | }, 260 | }); 261 | 262 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 263 | file.symlink = inputDirpath; 264 | 265 | function assert(files) { 266 | var stats = fs.statSync(outputDirpath); 267 | var lstats = fs.lstatSync(outputDirpath); 268 | var outputLink = fs.readlinkSync(outputDirpath); 269 | 270 | expect(files.length).toEqual(1); 271 | expect(outputLink).toEqual(inputDirpath); 272 | expect(stats.isDirectory()).toEqual(true); 273 | expect(lstats.isDirectory()).toEqual(false); 274 | } 275 | 276 | pipe([ 277 | from.obj([file]), 278 | vfs.dest(outputBase, { useJunctions: false }), 279 | concat(assert), 280 | ], done); 281 | }); 282 | 283 | it('(windows) options can disable junctions for a directory (as a function)', function(done) { 284 | if (!isWindows) { 285 | this.skip(); 286 | return; 287 | } 288 | 289 | var file = new File({ 290 | base: inputBase, 291 | path: inputDirpath, 292 | contents: null, 293 | stat: { 294 | isSymbolicLink: always(true), 295 | }, 296 | }); 297 | 298 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 299 | file.symlink = inputDirpath; 300 | 301 | function useJunctions(f) { 302 | expect(f).toExist(); 303 | expect(f).toBe(file); 304 | return false; 305 | } 306 | 307 | function assert(files) { 308 | var stats = fs.statSync(outputDirpath); 309 | var lstats = fs.lstatSync(outputDirpath); 310 | var outputLink = fs.readlinkSync(outputDirpath); 311 | 312 | expect(files.length).toEqual(1); 313 | expect(outputLink).toEqual(inputDirpath); 314 | expect(stats.isDirectory()).toEqual(true); 315 | expect(lstats.isDirectory()).toEqual(false); 316 | } 317 | 318 | pipe([ 319 | from.obj([file]), 320 | vfs.dest(outputBase, { useJunctions: useJunctions }), 321 | concat(assert), 322 | ], done); 323 | }); 324 | 325 | it('(*nix) can create relative links for directories', function(done) { 326 | if (isWindows) { 327 | this.skip(); 328 | return; 329 | } 330 | 331 | var file = new File({ 332 | base: inputBase, 333 | path: inputDirpath, 334 | contents: null, 335 | stat: { 336 | isSymbolicLink: always(true), 337 | }, 338 | }); 339 | 340 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 341 | file.symlink = inputDirpath; 342 | 343 | function assert(files) { 344 | var stats = fs.statSync(outputDirpath); 345 | var lstats = fs.lstatSync(outputDirpath); 346 | var outputLink = fs.readlinkSync(outputDirpath); 347 | 348 | expect(files.length).toEqual(1); 349 | expect(outputLink).toEqual(path.normalize('../fixtures/foo')); 350 | expect(stats.isDirectory()).toEqual(true); 351 | expect(lstats.isDirectory()).toEqual(false); 352 | } 353 | 354 | pipe([ 355 | from.obj([file]), 356 | vfs.dest(outputBase, { relativeSymlinks: true }), 357 | concat(assert), 358 | ], done); 359 | }); 360 | 361 | it('(*nix) receives a virtual symbolic directory and creates a symlink', function(done) { 362 | if (isWindows) { 363 | this.skip(); 364 | return; 365 | } 366 | 367 | var file = new File({ 368 | base: neInputBase, 369 | path: neInputDirpath, 370 | contents: null, 371 | stat: { 372 | isSymbolicLink: always(true), 373 | }, 374 | }); 375 | 376 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 377 | file.symlink = neInputDirpath; 378 | 379 | function assert(files) { 380 | var lstats = fs.lstatSync(neOutputDirpath); 381 | var outputLink = fs.readlinkSync(neOutputDirpath); 382 | var linkTargetExists = fs.existsSync(outputLink); 383 | 384 | expect(files.length).toEqual(1); 385 | expect(outputLink).toEqual(neInputDirpath); 386 | expect(linkTargetExists).toEqual(false); 387 | expect(lstats.isSymbolicLink()).toEqual(true); 388 | } 389 | 390 | pipe([ 391 | // This could also be from a different Vinyl adapter 392 | from.obj([file]), 393 | vfs.dest(neOutputBase), 394 | concat(assert), 395 | ], done); 396 | }); 397 | 398 | // There's no way to determine the proper type of link to create with a dangling link 399 | // So we just create a 'file' type symlink 400 | // There's also no real way to test the type that was created 401 | it('(windows) receives a virtual symbolic directory and creates a symlink', function(done) { 402 | if (!isWindows) { 403 | this.skip(); 404 | return; 405 | } 406 | 407 | var file = new File({ 408 | base: neInputBase, 409 | path: neInputDirpath, 410 | contents: null, 411 | stat: { 412 | isSymbolicLink: always(true), 413 | }, 414 | }); 415 | 416 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 417 | file.symlink = neInputDirpath; 418 | 419 | function assert(files) { 420 | var lstats = fs.lstatSync(neOutputDirpath); 421 | var outputLink = fs.readlinkSync(neOutputDirpath); 422 | var linkTargetExists = fs.existsSync(outputLink); 423 | 424 | expect(files.length).toEqual(1); 425 | expect(outputLink).toEqual(neInputDirpath); 426 | expect(linkTargetExists).toEqual(false); 427 | expect(lstats.isSymbolicLink()).toEqual(true); 428 | } 429 | 430 | pipe([ 431 | // This could also be from a different Vinyl adapter 432 | from.obj([file]), 433 | vfs.dest(neOutputBase), 434 | concat(assert), 435 | ], done); 436 | }); 437 | 438 | it('(windows) relativeSymlinks option is ignored when junctions are used', function(done) { 439 | if (!isWindows) { 440 | this.skip(); 441 | return; 442 | } 443 | 444 | var file = new File({ 445 | base: inputBase, 446 | path: inputDirpath, 447 | contents: null, 448 | stat: { 449 | isSymbolicLink: always(true), 450 | }, 451 | }); 452 | 453 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 454 | file.symlink = inputDirpath; 455 | 456 | function assert(files) { 457 | var stats = fs.statSync(outputDirpath); 458 | var lstats = fs.lstatSync(outputDirpath); 459 | var outputLink = fs.readlinkSync(outputDirpath); 460 | 461 | expect(files.length).toEqual(1); 462 | // When creating a junction, it seems Windows appends a separator 463 | expect(outputLink).toEqual(inputDirpath + path.sep); 464 | expect(stats.isDirectory()).toEqual(true); 465 | expect(lstats.isDirectory()).toEqual(false); 466 | } 467 | 468 | pipe([ 469 | from.obj([file]), 470 | vfs.dest(outputBase, { useJunctions: true, relativeSymlinks: true }), 471 | concat(assert), 472 | ], done); 473 | }); 474 | 475 | it('(windows) supports relativeSymlinks option when link is not for a directory', function(done) { 476 | if (!isWindows) { 477 | this.skip(); 478 | return; 479 | } 480 | 481 | var file = new File({ 482 | base: inputBase, 483 | path: inputPath, 484 | contents: null, 485 | stat: { 486 | isSymbolicLink: always(true), 487 | }, 488 | }); 489 | 490 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 491 | file.symlink = inputPath; 492 | 493 | function assert(files) { 494 | var outputLink = fs.readlinkSync(outputPath); 495 | 496 | expect(files.length).toEqual(1); 497 | expect(outputLink).toEqual(path.normalize('../fixtures/test.txt')); 498 | } 499 | 500 | pipe([ 501 | from.obj([file]), 502 | // The useJunctions option is ignored when file is not a directory 503 | vfs.dest(outputBase, { useJunctions: true, relativeSymlinks: true }), 504 | concat(assert), 505 | ], done); 506 | }); 507 | 508 | it('(windows) can create relative links for directories when junctions are disabled', function(done) { 509 | if (!isWindows) { 510 | this.skip(); 511 | return; 512 | } 513 | 514 | var file = new File({ 515 | base: inputBase, 516 | path: inputDirpath, 517 | contents: null, 518 | stat: { 519 | isSymbolicLink: always(true), 520 | }, 521 | }); 522 | 523 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 524 | file.symlink = inputDirpath; 525 | 526 | function assert(files) { 527 | var stats = fs.statSync(outputDirpath); 528 | var lstats = fs.lstatSync(outputDirpath); 529 | var outputLink = fs.readlinkSync(outputDirpath); 530 | 531 | expect(files.length).toEqual(1); 532 | expect(files).toInclude(file); 533 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 534 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 535 | expect(outputLink).toEqual(path.normalize('../fixtures/foo')); 536 | expect(stats.isDirectory()).toEqual(true); 537 | expect(lstats.isDirectory()).toEqual(false); 538 | } 539 | 540 | pipe([ 541 | from.obj([file]), 542 | vfs.dest(outputBase, { useJunctions: false, relativeSymlinks: true }), 543 | concat(assert), 544 | ], done); 545 | }); 546 | 547 | it('does not overwrite links with overwrite option set to false', function(done) { 548 | var existingContents = 'Lorem Ipsum'; 549 | 550 | var file = new File({ 551 | base: inputBase, 552 | path: inputPath, 553 | contents: null, 554 | stat: { 555 | isSymbolicLink: always(true), 556 | }, 557 | }); 558 | 559 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 560 | file.symlink = inputPath; 561 | 562 | function assert(files) { 563 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 564 | 565 | expect(files.length).toEqual(1); 566 | expect(outputContents).toEqual(existingContents); 567 | } 568 | 569 | // Write expected file which should not be overwritten 570 | fs.mkdirSync(outputBase); 571 | fs.writeFileSync(outputPath, existingContents); 572 | 573 | pipe([ 574 | from.obj([file]), 575 | vfs.dest(outputBase, { overwrite: false }), 576 | concat(assert), 577 | ], done); 578 | }); 579 | 580 | 581 | it('overwrites links with overwrite option set to true', function(done) { 582 | var existingContents = 'Lorem Ipsum'; 583 | 584 | var file = new File({ 585 | base: inputBase, 586 | path: inputPath, 587 | contents: null, 588 | stat: { 589 | isSymbolicLink: always(true), 590 | }, 591 | }); 592 | 593 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 594 | file.symlink = inputPath; 595 | 596 | function assert(files) { 597 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 598 | 599 | expect(files.length).toEqual(1); 600 | expect(outputContents).toEqual(contents); 601 | } 602 | 603 | // This should be overwritten 604 | fs.mkdirSync(outputBase); 605 | fs.writeFileSync(outputPath, existingContents); 606 | 607 | pipe([ 608 | from.obj([file]), 609 | vfs.dest(outputBase, { overwrite: true }), 610 | concat(assert), 611 | ], done); 612 | }); 613 | 614 | it('does not overwrite links with overwrite option set to a function that returns false', function(done) { 615 | var existingContents = 'Lorem Ipsum'; 616 | 617 | var file = new File({ 618 | base: inputBase, 619 | path: inputPath, 620 | contents: null, 621 | stat: { 622 | isSymbolicLink: always(true), 623 | }, 624 | }); 625 | 626 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 627 | file.symlink = inputPath; 628 | 629 | function overwrite(f) { 630 | expect(f).toEqual(file); 631 | return false; 632 | } 633 | 634 | function assert(files) { 635 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 636 | 637 | expect(files.length).toEqual(1); 638 | expect(outputContents).toEqual(existingContents); 639 | } 640 | 641 | // Write expected file which should not be overwritten 642 | fs.mkdirSync(outputBase); 643 | fs.writeFileSync(outputPath, existingContents); 644 | 645 | pipe([ 646 | from.obj([file]), 647 | vfs.dest(outputBase, { overwrite: overwrite }), 648 | concat(assert), 649 | ], done); 650 | }); 651 | 652 | it('overwrites links with overwrite option set to a function that returns true', function(done) { 653 | var existingContents = 'Lorem Ipsum'; 654 | 655 | var file = new File({ 656 | base: inputBase, 657 | path: inputPath, 658 | contents: null, 659 | stat: { 660 | isSymbolicLink: always(true), 661 | }, 662 | }); 663 | 664 | // `src()` adds this side-effect with `resolveSymlinks` option set to false 665 | file.symlink = inputPath; 666 | 667 | function overwrite(f) { 668 | expect(f).toEqual(file); 669 | return true; 670 | } 671 | 672 | function assert(files) { 673 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 674 | 675 | expect(files.length).toEqual(1); 676 | expect(outputContents).toEqual(contents); 677 | } 678 | 679 | // This should be overwritten 680 | fs.mkdirSync(outputBase); 681 | fs.writeFileSync(outputPath, existingContents); 682 | 683 | pipe([ 684 | from.obj([file]), 685 | vfs.dest(outputBase, { overwrite: overwrite }), 686 | concat(assert), 687 | ], done); 688 | }); 689 | }); 690 | -------------------------------------------------------------------------------- /test/vfs/dest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var assert = require('assert'); 6 | var fs = require('graceful-fs'); 7 | var File = require('vinyl'); 8 | var expect = require('expect'); 9 | var miss = require('mississippi'); 10 | 11 | var cleanup = require('./utils/cleanup'); 12 | var statMode = require('./utils/stat-mode'); 13 | var mockError = require('./utils/mock-error'); 14 | var applyUmask = require('./utils/apply-umask'); 15 | var testStreams = require('./utils/test-streams'); 16 | var always = require('./utils/always'); 17 | var testConstants = require('./utils/test-constants'); 18 | var breakPrototype = require('./utils/break-prototype'); 19 | 20 | var from = miss.from; 21 | var pipe = miss.pipe; 22 | var concat = miss.concat; 23 | 24 | var count = testStreams.count; 25 | var rename = testStreams.rename; 26 | var includes = testStreams.includes; 27 | var slowCount = testStreams.slowCount; 28 | var string = testStreams.string; 29 | 30 | function noop() {} 31 | 32 | var inputRelative = testConstants.inputRelative; 33 | var outputRelative = testConstants.outputRelative; 34 | var inputBase = testConstants.inputBase; 35 | var outputBase = testConstants.outputBase; 36 | var inputPath = testConstants.inputPath; 37 | var outputPath = testConstants.outputPath; 38 | var outputRenamePath = testConstants.outputRenamePath; 39 | var inputDirpath = testConstants.inputDirpath; 40 | var outputDirpath = testConstants.outputDirpath; 41 | var contents = testConstants.contents; 42 | var sourcemapContents = testConstants.sourcemapContents; 43 | 44 | function makeSourceMap() { 45 | return { 46 | version: 3, 47 | file: inputRelative, 48 | names: [], 49 | mappings: '', 50 | sources: [inputRelative], 51 | sourcesContent: [contents] 52 | }; 53 | } 54 | 55 | var clean = cleanup(outputBase); 56 | const App = require('templates'); 57 | let plugin = require('../..'); 58 | let vfs; 59 | 60 | describe('.dest()', function() { 61 | beforeEach(clean); 62 | afterEach(clean); 63 | beforeEach(() => { 64 | vfs = new App(); 65 | vfs.use(plugin()); 66 | }); 67 | 68 | it('throws on no folder argument', cb => { 69 | function noFolder() { 70 | vfs.dest(); 71 | } 72 | 73 | expect(noFolder).toThrow(); 74 | cb(); 75 | }); 76 | 77 | it('throws on empty string folder argument', cb => { 78 | function emptyFolder() { 79 | vfs.dest(''); 80 | } 81 | expect(emptyFolder).toThrow(); 82 | cb(); 83 | }); 84 | 85 | it('accepts the sourcemap option as true', cb => { 86 | var file = new File({ 87 | base: inputBase, 88 | path: inputPath, 89 | contents: Buffer.from(contents), 90 | sourceMap: makeSourceMap() 91 | }); 92 | 93 | function compare(files) { 94 | assert.equal(files.length, 1); 95 | expect(files).toInclude(file); 96 | } 97 | 98 | pipe( 99 | [from.obj([file]), vfs.dest(outputBase, { sourcemaps: true }), concat(compare)], 100 | cb 101 | ); 102 | }); 103 | 104 | it('accepts the sourcemap option as a string', cb => { 105 | var file = new File({ 106 | base: inputBase, 107 | path: inputPath, 108 | contents: Buffer.from(contents), 109 | sourceMap: makeSourceMap() 110 | }); 111 | 112 | function compare(files) { 113 | assert.equal(files.length, 2); 114 | expect(files).toInclude(file); 115 | } 116 | 117 | pipe( 118 | [from.obj([file]), vfs.dest(outputBase, { sourcemaps: '.' }), concat(compare)], 119 | cb 120 | ); 121 | }); 122 | 123 | it('inlines sourcemaps when option is true', cb => { 124 | var file = new File({ 125 | base: inputBase, 126 | path: inputPath, 127 | contents: Buffer.from(contents), 128 | sourceMap: makeSourceMap() 129 | }); 130 | 131 | function compare(files) { 132 | assert.equal(files.length, 1); 133 | expect(files).toInclude(file); 134 | expect(files[0].contents.toString()).toMatch(new RegExp(sourcemapContents)); 135 | } 136 | 137 | pipe( 138 | [from.obj([file]), vfs.dest(outputBase, { sourcemaps: true }), concat(compare)], 139 | cb 140 | ); 141 | }); 142 | 143 | it('generates an extra File when option is a string', cb => { 144 | var file = new File({ 145 | base: inputBase, 146 | path: inputPath, 147 | contents: Buffer.from(contents), 148 | sourceMap: makeSourceMap() 149 | }); 150 | 151 | function compare(files) { 152 | assert.equal(files.length, 2); 153 | expect(files).toInclude(file); 154 | expect(files[0].contents.toString()).toMatch(new RegExp('//# sourceMappingURL=test.txt.map')); 155 | assert.equal(files[1].contents, JSON.stringify(makeSourceMap())); 156 | } 157 | 158 | pipe( 159 | [from.obj([file]), vfs.dest(outputBase, { sourcemaps: '.' }), concat(compare)], 160 | cb 161 | ); 162 | }); 163 | 164 | it('passes through writes with cwd', cb => { 165 | var file = new File({ 166 | base: inputBase, 167 | path: inputPath, 168 | contents: null 169 | }); 170 | 171 | function compare(files) { 172 | assert.equal(files.length, 1); 173 | expect(files).toInclude(file); 174 | assert.equal(files[0].cwd, __dirname, 'cwd should have changed'); 175 | } 176 | 177 | pipe( 178 | [from.obj([file]), vfs.dest(outputRelative, { cwd: __dirname }), concat(compare)], 179 | cb 180 | ); 181 | }); 182 | 183 | it('passes through writes with default cwd', cb => { 184 | var file = new File({ 185 | base: inputBase, 186 | path: inputPath, 187 | contents: null 188 | }); 189 | 190 | function compare(files) { 191 | assert.equal(files.length, 1); 192 | expect(files).toInclude(file); 193 | assert.equal(files[0].cwd, process.cwd(), 'cwd should not have changed'); 194 | } 195 | 196 | pipe( 197 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 198 | cb 199 | ); 200 | }); 201 | 202 | it('does not write null files', cb => { 203 | var file = new File({ 204 | base: inputBase, 205 | path: inputPath, 206 | contents: null 207 | }); 208 | 209 | function compare(files) { 210 | var exists = fs.existsSync(outputPath); 211 | 212 | assert.equal(files.length, 1); 213 | expect(files).toInclude(file); 214 | assert.equal(files[0].base, outputBase, 'base should have changed'); 215 | assert.equal(files[0].path, outputPath, 'path should have changed'); 216 | assert.equal(exists, false); 217 | } 218 | 219 | pipe( 220 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 221 | cb 222 | ); 223 | }); 224 | 225 | it('writes buffer files to the right folder with relative cwd', cb => { 226 | var cwd = path.relative(process.cwd(), __dirname); 227 | 228 | var file = new File({ 229 | base: inputBase, 230 | path: inputPath, 231 | contents: Buffer.from(contents) 232 | }); 233 | 234 | function compare(files) { 235 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 236 | 237 | assert.equal(files.length, 1); 238 | expect(files).toInclude(file); 239 | assert.equal(files[0].cwd, __dirname, 'cwd should have changed'); 240 | assert.equal(files[0].base, outputBase, 'base should have changed'); 241 | assert.equal(files[0].path, outputPath, 'path should have changed'); 242 | assert.equal(outputContents, contents); 243 | } 244 | 245 | pipe( 246 | [from.obj([file]), vfs.dest(outputRelative, { cwd: cwd }), concat(compare)], 247 | cb 248 | ); 249 | }); 250 | 251 | it('writes buffer files to the right folder with function and relative cwd', cb => { 252 | var cwd = path.relative(process.cwd(), __dirname); 253 | 254 | var file = new File({ 255 | base: inputBase, 256 | path: inputPath, 257 | contents: Buffer.from(contents) 258 | }); 259 | 260 | function outputFn(f) { 261 | assert(f); 262 | assert(file); 263 | return outputRelative; 264 | } 265 | 266 | function compare(files) { 267 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 268 | 269 | assert.equal(files.length, 1); 270 | expect(files).toInclude(file); 271 | assert.equal(files[0].cwd, __dirname, 'cwd should have changed'); 272 | assert.equal(files[0].base, outputBase, 'base should have changed'); 273 | assert.equal(files[0].path, outputPath, 'path should have changed'); 274 | assert.equal(outputContents, contents); 275 | } 276 | 277 | pipe( 278 | [from.obj([file]), vfs.dest(outputFn, { cwd: cwd }), concat(compare)], 279 | cb 280 | ); 281 | }); 282 | 283 | it('writes buffer files to the right folder', cb => { 284 | var file = new File({ 285 | base: inputBase, 286 | path: inputPath, 287 | contents: Buffer.from(contents) 288 | }); 289 | 290 | function compare(files) { 291 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 292 | 293 | assert.equal(files.length, 1); 294 | expect(files).toInclude(file); 295 | assert.equal(files[0].base, outputBase, 'base should have changed'); 296 | assert.equal(files[0].path, outputPath, 'path should have changed'); 297 | assert.equal(outputContents, contents); 298 | } 299 | 300 | pipe( 301 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 302 | cb 303 | ); 304 | }); 305 | 306 | it('writes streaming files to the right folder', cb => { 307 | var file = new File({ 308 | base: inputBase, 309 | path: inputPath, 310 | contents: from([contents]) 311 | }); 312 | 313 | function compare(files) { 314 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 315 | 316 | assert.equal(files.length, 1); 317 | expect(files).toInclude(file); 318 | assert.equal(files[0].base, outputBase, 'base should have changed'); 319 | assert.equal(files[0].path, outputPath, 'path should have changed'); 320 | assert.equal(outputContents, contents); 321 | } 322 | 323 | pipe( 324 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 325 | cb 326 | ); 327 | }); 328 | 329 | it('writes large streaming files to the right folder', cb => { 330 | var size = 40000; 331 | 332 | var file = new File({ 333 | base: inputBase, 334 | path: inputPath, 335 | contents: string(size) 336 | }); 337 | 338 | function compare(files) { 339 | var stats = fs.lstatSync(outputPath); 340 | 341 | assert.equal(files.length, 1); 342 | expect(files).toInclude(file); 343 | assert.equal(stats.size, size); 344 | } 345 | 346 | pipe( 347 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 348 | cb 349 | ); 350 | }); 351 | 352 | it('writes directories to the right folder', cb => { 353 | var file = new File({ 354 | base: inputBase, 355 | path: inputDirpath, 356 | contents: null, 357 | stat: { 358 | isDirectory: always(true) 359 | } 360 | }); 361 | 362 | function compare(files) { 363 | var stats = fs.lstatSync(outputDirpath); 364 | 365 | assert.equal(files.length, 1); 366 | expect(files).toInclude(file); 367 | assert.equal(files[0].base, outputBase, 'base should have changed'); 368 | // TODO: normalize this path 369 | assert.equal(files[0].path, outputDirpath, 'path should have changed'); 370 | assert.equal(stats.isDirectory(), true); 371 | } 372 | 373 | pipe( 374 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 375 | cb 376 | ); 377 | }); 378 | 379 | it('allows piping multiple dests in streaming mode', cb => { 380 | var file = new File({ 381 | base: inputBase, 382 | path: inputPath, 383 | contents: Buffer.from(contents) 384 | }); 385 | 386 | function compare() { 387 | var outputContents1 = fs.readFileSync(outputPath, 'utf8'); 388 | var outputContents2 = fs.readFileSync(outputRenamePath, 'utf8'); 389 | assert.equal(outputContents1, contents); 390 | assert.equal(outputContents2, contents); 391 | } 392 | 393 | pipe( 394 | [ 395 | from.obj([file]), 396 | includes({ path: inputPath }), 397 | vfs.dest(outputBase), 398 | rename(outputRenamePath), 399 | includes({ path: outputRenamePath }), 400 | vfs.dest(outputBase), 401 | concat(compare) 402 | ], 403 | cb 404 | ); 405 | }); 406 | 407 | it('writes new files with the default user mode', cb => { 408 | var expectedMode = applyUmask('666'); 409 | 410 | var file = new File({ 411 | base: inputBase, 412 | path: inputPath, 413 | contents: Buffer.from(contents) 414 | }); 415 | 416 | function compare(files) { 417 | assert.equal(files.length, 1); 418 | expect(files).toInclude(file); 419 | assert.equal(statMode(outputPath), expectedMode); 420 | } 421 | 422 | pipe( 423 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 424 | cb 425 | ); 426 | }); 427 | 428 | it('reports i/o errors', cb => { 429 | var file = new File({ 430 | base: inputBase, 431 | path: inputPath, 432 | contents: Buffer.from(contents) 433 | }); 434 | 435 | function compare(err) { 436 | assert(err, 'expected an error'); 437 | cb(); 438 | } 439 | 440 | fs.mkdirSync(outputBase); 441 | fs.closeSync(fs.openSync(outputPath, 'w')); 442 | fs.chmodSync(outputPath, 0); 443 | 444 | pipe( 445 | [from.obj([file]), vfs.dest(outputBase)], 446 | compare 447 | ); 448 | }); 449 | 450 | it('reports stat errors', cb => { 451 | var expectedMode = applyUmask('722'); 452 | 453 | var fstatSpy = expect.spyOn(fs, 'fstat').andCall(mockError); 454 | 455 | var file = new File({ 456 | base: inputBase, 457 | path: inputPath, 458 | contents: Buffer.from(contents), 459 | stat: { 460 | mode: expectedMode 461 | } 462 | }); 463 | 464 | function compare(err) { 465 | assert(err, 'expected an error'); 466 | assert.equal(fstatSpy.calls.length, 1); 467 | cb(); 468 | } 469 | 470 | fs.mkdirSync(outputBase); 471 | fs.closeSync(fs.openSync(outputPath, 'w')); 472 | 473 | pipe( 474 | [from.obj([file]), vfs.dest(outputBase)], 475 | compare 476 | ); 477 | }); 478 | 479 | it('does not overwrite files with overwrite option set to false', cb => { 480 | var existingContents = 'Lorem Ipsum'; 481 | 482 | var file = new File({ 483 | base: inputBase, 484 | path: inputPath, 485 | contents: Buffer.from(contents) 486 | }); 487 | 488 | function compare(files) { 489 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 490 | 491 | assert.equal(files.length, 1); 492 | assert.equal(outputContents, existingContents); 493 | } 494 | 495 | // Write expected file which should not be overwritten 496 | fs.mkdirSync(outputBase); 497 | fs.writeFileSync(outputPath, existingContents); 498 | 499 | pipe( 500 | [from.obj([file]), vfs.dest(outputBase, { overwrite: false }), concat(compare)], 501 | cb 502 | ); 503 | }); 504 | 505 | it('overwrites files with overwrite option set to true', cb => { 506 | var existingContents = 'Lorem Ipsum'; 507 | 508 | var file = new File({ 509 | base: inputBase, 510 | path: inputPath, 511 | contents: Buffer.from(contents) 512 | }); 513 | 514 | function compare(files) { 515 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 516 | 517 | assert.equal(files.length, 1); 518 | assert.equal(outputContents, contents); 519 | } 520 | 521 | // This should be overwritten 522 | fs.mkdirSync(outputBase); 523 | fs.writeFileSync(outputPath, existingContents); 524 | 525 | pipe( 526 | [from.obj([file]), vfs.dest(outputBase, { overwrite: true }), concat(compare)], 527 | cb 528 | ); 529 | }); 530 | 531 | it('does not overwrite files with overwrite option set to a function that returns false', cb => { 532 | var existingContents = 'Lorem Ipsum'; 533 | 534 | var file = new File({ 535 | base: inputBase, 536 | path: inputPath, 537 | contents: Buffer.from(contents) 538 | }); 539 | 540 | function overwrite(f) { 541 | assert.equal(f, file); 542 | return false; 543 | } 544 | 545 | function compare(files) { 546 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 547 | 548 | assert.equal(files.length, 1); 549 | assert.equal(outputContents, existingContents); 550 | } 551 | 552 | // Write expected file which should not be overwritten 553 | fs.mkdirSync(outputBase); 554 | fs.writeFileSync(outputPath, existingContents); 555 | 556 | pipe( 557 | [from.obj([file]), vfs.dest(outputBase, { overwrite: overwrite }), concat(compare)], 558 | cb 559 | ); 560 | }); 561 | 562 | it('overwrites files with overwrite option set to a function that returns true', cb => { 563 | var existingContents = 'Lorem Ipsum'; 564 | 565 | var file = new File({ 566 | base: inputBase, 567 | path: inputPath, 568 | contents: Buffer.from(contents) 569 | }); 570 | 571 | function overwrite(f) { 572 | assert.equal(f, file); 573 | return true; 574 | } 575 | 576 | function compare(files) { 577 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 578 | 579 | assert.equal(files.length, 1); 580 | assert.equal(outputContents, contents); 581 | } 582 | 583 | // This should be overwritten 584 | fs.mkdirSync(outputBase); 585 | fs.writeFileSync(outputPath, existingContents); 586 | 587 | pipe( 588 | [from.obj([file]), vfs.dest(outputBase, { overwrite: overwrite }), concat(compare)], 589 | cb 590 | ); 591 | }); 592 | 593 | it('appends content with append option set to true', cb => { 594 | var existingContents = 'Lorem Ipsum'; 595 | 596 | var file = new File({ 597 | base: inputBase, 598 | path: inputPath, 599 | contents: Buffer.from(contents) 600 | }); 601 | 602 | function compare(files) { 603 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 604 | 605 | assert.equal(files.length, 1); 606 | assert.equal(outputContents, existingContents + contents); 607 | } 608 | 609 | // This should be overwritten 610 | fs.mkdirSync(outputBase); 611 | fs.writeFileSync(outputPath, existingContents); 612 | 613 | pipe( 614 | [from.obj([file]), vfs.dest(outputBase, { append: true }), concat(compare)], 615 | cb 616 | ); 617 | }); 618 | 619 | it('appends content with append option set to a function that returns true', cb => { 620 | var existingContents = 'Lorem Ipsum'; 621 | 622 | var file = new File({ 623 | base: inputBase, 624 | path: inputPath, 625 | contents: Buffer.from(contents) 626 | }); 627 | 628 | function append(f) { 629 | assert.equal(f, file); 630 | return true; 631 | } 632 | 633 | function compare(files) { 634 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 635 | 636 | assert.equal(files.length, 1); 637 | assert.equal(outputContents, existingContents + contents); 638 | } 639 | 640 | // This should be overwritten 641 | fs.mkdirSync(outputBase); 642 | fs.writeFileSync(outputPath, existingContents); 643 | 644 | pipe( 645 | [from.obj([file]), vfs.dest(outputBase, { append: append }), concat(compare)], 646 | cb 647 | ); 648 | }); 649 | 650 | it('emits a finish event', cb => { 651 | var destStream = vfs.dest(outputBase); 652 | 653 | destStream.once('finish', cb); 654 | 655 | var file = new File({ 656 | base: inputBase, 657 | path: inputPath, 658 | contents: Buffer.from('1234567890') 659 | }); 660 | 661 | pipe([from.obj([file]), destStream]); 662 | }); 663 | 664 | it('does not get clogged by highWaterMark', cb => { 665 | var expectedCount = 17; 666 | var highwatermarkFiles = []; 667 | for (var idx = 0; idx < expectedCount; idx++) { 668 | var file = new File({ 669 | base: inputBase, 670 | path: inputPath, 671 | contents: Buffer.from(contents) 672 | }); 673 | highwatermarkFiles.push(file); 674 | } 675 | 676 | pipe( 677 | [ 678 | from.obj(highwatermarkFiles), 679 | count(expectedCount), 680 | // Must be in the Writable position to test this 681 | // So concat-stream cannot be used 682 | vfs.dest(outputBase) 683 | ], 684 | cb 685 | ); 686 | }); 687 | 688 | it('allows backpressure when piped to another, slower stream', function(cb) { 689 | this.timeout(20000); 690 | 691 | var expectedCount = 24; 692 | var highwatermarkFiles = []; 693 | for (var idx = 0; idx < expectedCount; idx++) { 694 | var file = new File({ 695 | base: inputBase, 696 | path: inputPath, 697 | contents: Buffer.from(contents) 698 | }); 699 | highwatermarkFiles.push(file); 700 | } 701 | 702 | pipe( 703 | [from.obj(highwatermarkFiles), count(expectedCount), vfs.dest(outputBase), slowCount(expectedCount)], 704 | cb 705 | ); 706 | }); 707 | 708 | it('respects readable listeners on destination stream', cb => { 709 | var file = new File({ 710 | base: inputBase, 711 | path: inputDirpath, 712 | contents: null 713 | }); 714 | 715 | var destStream = vfs.dest(outputBase); 716 | var readables = 0; 717 | destStream.on('readable', function() { 718 | var data = destStream.read(); 719 | if (data != null) { 720 | readables++; 721 | } 722 | }); 723 | 724 | function compare(err) { 725 | assert.equal(readables, 1); 726 | cb(err); 727 | } 728 | 729 | pipe([from.obj([file]), destStream], compare); 730 | }); 731 | 732 | it('respects data listeners on destination stream', cb => { 733 | var file = new File({ 734 | base: inputBase, 735 | path: inputDirpath, 736 | contents: null 737 | }); 738 | 739 | var destStream = vfs.dest(outputBase); 740 | 741 | var datas = 0; 742 | destStream.on('data', function() { 743 | datas++; 744 | }); 745 | 746 | function compare(err) { 747 | assert.equal(datas, 1); 748 | cb(err); 749 | } 750 | 751 | pipe([from.obj([file]), destStream], compare); 752 | }); 753 | 754 | it('sinks the stream if all the readable event handlers are removed', cb => { 755 | var expectedCount = 17; 756 | var highwatermarkFiles = []; 757 | for (var idx = 0; idx < expectedCount; idx++) { 758 | var file = new File({ 759 | base: inputBase, 760 | path: inputPath, 761 | contents: Buffer.from(contents) 762 | }); 763 | highwatermarkFiles.push(file); 764 | } 765 | 766 | var destStream = vfs.dest(outputBase); 767 | 768 | destStream.on('readable', noop); 769 | 770 | pipe( 771 | [ 772 | from.obj(highwatermarkFiles), 773 | count(expectedCount), 774 | // Must be in the Writable position to test this 775 | // So concat-stream cannot be used 776 | destStream 777 | ], 778 | cb 779 | ); 780 | 781 | process.nextTick(function() { 782 | destStream.removeListener('readable', noop); 783 | }); 784 | }); 785 | 786 | it('sinks the stream if all the data event handlers are removed', cb => { 787 | var expectedCount = 17; 788 | var highwatermarkFiles = []; 789 | for (var idx = 0; idx < expectedCount; idx++) { 790 | var file = new File({ 791 | base: inputBase, 792 | path: inputPath, 793 | contents: Buffer.from(contents) 794 | }); 795 | highwatermarkFiles.push(file); 796 | } 797 | 798 | var destStream = vfs.dest(outputBase); 799 | 800 | destStream.on('data', noop); 801 | 802 | pipe( 803 | [ 804 | from.obj(highwatermarkFiles), 805 | count(expectedCount), 806 | // Must be in the Writable position to test this 807 | // So concat-stream cannot be used 808 | destStream 809 | ], 810 | cb 811 | ); 812 | 813 | process.nextTick(function() { 814 | destStream.removeListener('data', noop); 815 | }); 816 | }); 817 | 818 | it('successfully processes files with streaming contents', cb => { 819 | var file = new File({ 820 | base: inputBase, 821 | path: inputPath, 822 | contents: from([contents]) 823 | }); 824 | 825 | pipe([from.obj([file]), vfs.dest(outputBase)], cb); 826 | }); 827 | 828 | it('errors when a non-Vinyl object is emitted', cb => { 829 | let file = {}; 830 | pipe([from.obj([file]), vfs.dest(outputBase)], err => { 831 | assert(err, 'expected an error'); 832 | assert.equal(err.message, 'Received a non-Vinyl object in `dest()`'); 833 | cb(); 834 | }); 835 | }); 836 | 837 | it('errors when a buffer-mode stream is piped to it', cb => { 838 | let file = Buffer.from('test'); 839 | 840 | pipe([from([file]), vfs.dest(outputBase)], err => { 841 | assert(err, 'expected an error'); 842 | assert.equal(err.message, 'Received a non-Vinyl object in `dest()`'); 843 | cb(); 844 | }); 845 | }); 846 | 847 | it('errors if we cannot mkdirp', cb => { 848 | let mkdirSpy = expect.spyOn(fs, 'mkdir').andCall(mockError); 849 | let file = new File({ 850 | base: inputBase, 851 | path: inputPath, 852 | contents: null 853 | }); 854 | 855 | pipe([from.obj([file]), vfs.dest(outputBase)], err => { 856 | assert(err, 'expected an error'); 857 | assert.equal(mkdirSpy.calls.length, 1); 858 | cb(); 859 | }); 860 | }); 861 | 862 | it('errors if vinyl object is a directory and we cannot mkdirp', cb => { 863 | var ogMkdir = fs.mkdir; 864 | var mkdirSpy = expect.spyOn(fs, 'mkdir').andCall(function() { 865 | if (mkdirSpy.calls.length > 1) { 866 | mockError.apply(this, arguments); 867 | } else { 868 | ogMkdir.apply(this, arguments); 869 | } 870 | }); 871 | 872 | var file = new File({ 873 | base: inputBase, 874 | path: inputDirpath, 875 | contents: null, 876 | stat: { 877 | isDirectory: always(true) 878 | } 879 | }); 880 | 881 | function compare(err) { 882 | assert(err, 'expected an error'); 883 | assert.equal(mkdirSpy.calls.length, 2); 884 | cb(); 885 | } 886 | 887 | pipe( 888 | [from.obj([file]), vfs.dest(outputBase)], 889 | compare 890 | ); 891 | }); 892 | 893 | // TODO: is this correct behavior? had to adjust it 894 | it('does not error if vinyl object is a directory and we cannot open it', cb => { 895 | var file = new File({ 896 | base: inputBase, 897 | path: inputDirpath, 898 | contents: null, 899 | stat: { 900 | isDirectory: always(true), 901 | mode: applyUmask('000') 902 | } 903 | }); 904 | 905 | function compare() { 906 | var exists = fs.existsSync(outputDirpath); 907 | assert.equal(exists, true); 908 | } 909 | 910 | pipe( 911 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 912 | cb 913 | ); 914 | }); 915 | 916 | it('errors if vinyl object is a directory and open errors', cb => { 917 | var openSpy = expect.spyOn(fs, 'open').andCall(mockError); 918 | 919 | var file = new File({ 920 | base: inputBase, 921 | path: inputDirpath, 922 | contents: null, 923 | stat: { 924 | isDirectory: always(true) 925 | } 926 | }); 927 | 928 | function compare(err) { 929 | assert(err, 'expected an error'); 930 | assert.equal(openSpy.calls.length, 1); 931 | cb(); 932 | } 933 | 934 | pipe( 935 | [from.obj([file]), vfs.dest(outputBase)], 936 | compare 937 | ); 938 | }); 939 | 940 | it('errors if content stream errors', cb => { 941 | var contentStream = from(function(size, cb) { 942 | cb(new Error('mocked error')); 943 | }); 944 | 945 | var file = new File({ 946 | base: inputBase, 947 | path: inputPath, 948 | contents: contentStream 949 | }); 950 | 951 | function compare(err) { 952 | assert(err, 'expected an error'); 953 | cb(); 954 | } 955 | 956 | pipe( 957 | [from.obj([file]), vfs.dest(outputBase)], 958 | compare 959 | ); 960 | }); 961 | 962 | it('does not pass options on to through2', cb => { 963 | var file = new File({ 964 | base: inputBase, 965 | path: inputPath, 966 | contents: null 967 | }); 968 | 969 | // Reference: https://github.com/gulpjs/vinyl-fs/issues/153 970 | var read = expect.createSpy().andReturn(false); 971 | 972 | function compare() { 973 | // Called never because it's not a valid option 974 | assert.equal(read.calls.length, 0); 975 | } 976 | 977 | pipe( 978 | [from.obj([file]), vfs.dest(outputBase, { read: read }), concat(compare)], 979 | cb 980 | ); 981 | }); 982 | 983 | it('does not marshall a Vinyl object with isSymbolic method', cb => { 984 | var file = new File({ 985 | base: outputBase, 986 | path: outputPath 987 | }); 988 | 989 | function compare(files) { 990 | assert.equal(files.length, 1); 991 | // Avoid comparing stats because they get reflected 992 | delete files[0].stat; 993 | expect(files[0]).toMatch(file); 994 | expect(files[0]).toBe(file); 995 | } 996 | 997 | pipe( 998 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 999 | cb 1000 | ); 1001 | }); 1002 | 1003 | it('marshalls a Vinyl object without isSymbolic to a newer Vinyl', cb => { 1004 | var file = new File({ 1005 | base: outputBase, 1006 | path: outputPath 1007 | }); 1008 | 1009 | breakPrototype(file); 1010 | 1011 | function compare(files) { 1012 | assert.equal(files.length, 1); 1013 | // Avoid comparing stats because they get reflected 1014 | delete files[0].stat; 1015 | expect(files[0]).toMatch(file); 1016 | expect(files[0]).toNotBe(file); 1017 | } 1018 | 1019 | pipe( 1020 | [from.obj([file]), vfs.dest(outputBase), concat(compare)], 1021 | cb 1022 | ); 1023 | }); 1024 | }); 1025 | -------------------------------------------------------------------------------- /test/vfs/symlink.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var fs = require('graceful-fs'); 6 | var File = require('vinyl'); 7 | var expect = require('expect'); 8 | var miss = require('mississippi'); 9 | 10 | const App = require('templates'); 11 | let plugin = require('../..'); 12 | let vfs; 13 | 14 | var cleanup = require('./utils/cleanup'); 15 | var isWindows = require('./utils/is-windows'); 16 | var testStreams = require('./utils/test-streams'); 17 | var always = require('./utils/always'); 18 | var testConstants = require('./utils/test-constants'); 19 | var breakPrototype = require('./utils/break-prototype'); 20 | 21 | var from = miss.from; 22 | var pipe = miss.pipe; 23 | var concat = miss.concat; 24 | 25 | var count = testStreams.count; 26 | var slowCount = testStreams.slowCount; 27 | 28 | function noop() {} 29 | 30 | var outputRelative = testConstants.outputRelative; 31 | var inputBase = testConstants.inputBase; 32 | var outputBase = testConstants.outputBase; 33 | var inputPath = testConstants.inputPath; 34 | var outputPath = testConstants.outputPath; 35 | var inputDirpath = testConstants.inputDirpath; 36 | var outputDirpath = testConstants.outputDirpath; 37 | var contents = testConstants.contents; 38 | 39 | var clean = cleanup(outputBase); 40 | 41 | describe('symlink stream', function() { 42 | beforeEach(() => { 43 | vfs = new App(); 44 | vfs.use(plugin()); 45 | }); 46 | 47 | beforeEach(clean); 48 | afterEach(clean); 49 | 50 | it('throws on no folder argument', function(done) { 51 | function noFolder() { 52 | vfs.symlink(); 53 | } 54 | 55 | expect(noFolder).toThrow('Invalid symlink() folder argument. Please specify a non-empty string or a function.'); 56 | done(); 57 | }); 58 | 59 | it('throws on empty string folder argument', function(done) { 60 | function emptyFolder() { 61 | vfs.symlink(''); 62 | } 63 | 64 | expect(emptyFolder).toThrow('Invalid symlink() folder argument. Please specify a non-empty string or a function.'); 65 | done(); 66 | }); 67 | 68 | it('passes through writes with cwd', function(done) { 69 | var file = new File({ 70 | base: inputBase, 71 | path: inputPath, 72 | contents: null, 73 | }); 74 | 75 | function assert(files) { 76 | expect(files.length).toEqual(1); 77 | expect(files).toInclude(file); 78 | expect(files[0].cwd).toEqual(__dirname, 'cwd should have changed'); 79 | } 80 | 81 | pipe([ 82 | from.obj([file]), 83 | vfs.symlink(outputRelative, { cwd: __dirname }), 84 | concat(assert), 85 | ], done); 86 | }); 87 | 88 | it('passes through writes with default cwd', function(done) { 89 | var file = new File({ 90 | base: inputBase, 91 | path: inputPath, 92 | contents: null, 93 | }); 94 | 95 | function assert(files) { 96 | expect(files.length).toEqual(1); 97 | expect(files).toInclude(file); 98 | expect(files[0].cwd).toEqual(process.cwd(), 'cwd should not have changed'); 99 | } 100 | 101 | pipe([ 102 | from.obj([file]), 103 | vfs.symlink(outputBase), 104 | concat(assert), 105 | ], done); 106 | }); 107 | 108 | it('creates a link to the right folder with relative cwd', function(done) { 109 | var cwd = path.relative(process.cwd(), __dirname); 110 | 111 | var file = new File({ 112 | base: inputBase, 113 | path: inputPath, 114 | contents: null, 115 | }); 116 | 117 | function assert(files) { 118 | var outputLink = fs.readlinkSync(outputPath); 119 | 120 | expect(files.length).toEqual(1); 121 | expect(files).toInclude(file); 122 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 123 | expect(files[0].path).toEqual(outputPath, 'path should have changed'); 124 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 125 | expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); 126 | expect(outputLink).toEqual(inputPath); 127 | } 128 | 129 | pipe([ 130 | from.obj([file]), 131 | vfs.symlink(outputRelative, { cwd: cwd }), 132 | concat(assert), 133 | ], done); 134 | }); 135 | 136 | it('creates a link to the right folder with function and relative cwd', function(done) { 137 | var cwd = path.relative(process.cwd(), __dirname); 138 | 139 | var file = new File({ 140 | base: inputBase, 141 | path: inputPath, 142 | contents: null, 143 | }); 144 | 145 | function outputFn(f) { 146 | expect(f).toExist(); 147 | expect(f).toEqual(file); 148 | return outputRelative; 149 | } 150 | 151 | function assert(files) { 152 | var outputLink = fs.readlinkSync(outputPath); 153 | 154 | expect(files.length).toEqual(1); 155 | expect(files).toInclude(file); 156 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 157 | expect(files[0].path).toEqual(outputPath, 'path should have changed'); 158 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 159 | expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); 160 | expect(outputLink).toEqual(inputPath); 161 | } 162 | 163 | pipe([ 164 | from.obj([file]), 165 | vfs.symlink(outputFn, { cwd: cwd }), 166 | concat(assert), 167 | ], done); 168 | }); 169 | 170 | it('creates a link for a file with buffered contents', function(done) { 171 | var file = new File({ 172 | base: inputBase, 173 | path: inputPath, 174 | contents: Buffer.from(contents), 175 | }); 176 | 177 | function assert(files) { 178 | var outputLink = fs.readlinkSync(outputPath); 179 | 180 | expect(files.length).toEqual(1); 181 | expect(files).toInclude(file); 182 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 183 | expect(files[0].path).toEqual(outputPath, 'path should have changed'); 184 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 185 | expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); 186 | expect(outputLink).toEqual(inputPath); 187 | } 188 | 189 | pipe([ 190 | from.obj([file]), 191 | vfs.symlink(outputBase), 192 | concat(assert), 193 | ], done); 194 | }); 195 | 196 | it('can create relative links', function(done) { 197 | var file = new File({ 198 | base: inputBase, 199 | path: inputPath, 200 | contents: null, 201 | }); 202 | 203 | function assert(files) { 204 | var outputLink = fs.readlinkSync(outputPath); 205 | 206 | expect(files.length).toEqual(1); 207 | expect(files).toInclude(file); 208 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 209 | expect(files[0].path).toEqual(outputPath, 'path should have changed'); 210 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 211 | expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); 212 | expect(outputLink).toEqual(path.normalize('../fixtures/test.txt')); 213 | } 214 | 215 | pipe([ 216 | from.obj([file]), 217 | vfs.symlink(outputBase, { relativeSymlinks: true }), 218 | concat(assert), 219 | ], done); 220 | }); 221 | 222 | it('creates a link for a file with streaming contents', function(done) { 223 | var file = new File({ 224 | base: inputBase, 225 | path: inputPath, 226 | contents: from([contents]), 227 | }); 228 | 229 | function assert(files) { 230 | var outputLink = fs.readlinkSync(outputPath); 231 | 232 | expect(files.length).toEqual(1); 233 | expect(files).toInclude(file); 234 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 235 | expect(files[0].path).toEqual(outputPath, 'path should have changed'); 236 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 237 | expect(files[0].isSymbolic()).toBe(true, 'file should be symbolic'); 238 | expect(outputLink).toEqual(inputPath); 239 | } 240 | 241 | pipe([ 242 | from.obj([file]), 243 | vfs.symlink(outputBase), 244 | concat(assert), 245 | ], done); 246 | }); 247 | 248 | it('emits Vinyl objects that are symbolic', function(done) { 249 | var file = new File({ 250 | base: inputBase, 251 | path: inputPath, 252 | contents: null, 253 | }); 254 | 255 | function assert(files) { 256 | expect(files.length).toEqual(1); 257 | expect(files[0].isSymbolic()).toEqual(true); 258 | } 259 | 260 | pipe([ 261 | from.obj([file]), 262 | vfs.symlink(outputBase), 263 | concat(assert), 264 | ], done); 265 | }); 266 | 267 | it('(*nix) creates a link for a directory', function(done) { 268 | if (isWindows) { 269 | this.skip(); 270 | return; 271 | } 272 | 273 | var file = new File({ 274 | base: inputBase, 275 | path: inputDirpath, 276 | contents: null, 277 | stat: { 278 | isDirectory: always(true), 279 | }, 280 | }); 281 | 282 | function assert(files) { 283 | var stats = fs.statSync(outputDirpath); 284 | var lstats = fs.lstatSync(outputDirpath); 285 | var outputLink = fs.readlinkSync(outputDirpath); 286 | 287 | expect(files.length).toEqual(1); 288 | expect(files).toInclude(file); 289 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 290 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 291 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 292 | expect(outputLink).toEqual(inputDirpath); 293 | expect(stats.isDirectory()).toEqual(true); 294 | expect(lstats.isDirectory()).toEqual(false); 295 | } 296 | 297 | pipe([ 298 | from.obj([file]), 299 | vfs.symlink(outputBase), 300 | concat(assert), 301 | ], done); 302 | }); 303 | 304 | it('(windows) creates a junction for a directory', function(done) { 305 | if (!isWindows) { 306 | this.skip(); 307 | return; 308 | } 309 | 310 | var file = new File({ 311 | base: inputBase, 312 | path: inputDirpath, 313 | contents: null, 314 | stat: { 315 | isDirectory: always(true), 316 | }, 317 | }); 318 | 319 | function assert(files) { 320 | var stats = fs.statSync(outputDirpath); 321 | var lstats = fs.lstatSync(outputDirpath); 322 | var outputLink = fs.readlinkSync(outputDirpath); 323 | 324 | expect(files.length).toEqual(1); 325 | expect(files).toInclude(file); 326 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 327 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 328 | // When creating a junction, it seems Windows appends a separator 329 | expect(files[0].symlink + path.sep).toEqual(outputLink, 'symlink should be set'); 330 | expect(outputLink).toEqual(inputDirpath + path.sep); 331 | expect(stats.isDirectory()).toEqual(true); 332 | expect(lstats.isDirectory()).toEqual(false); 333 | } 334 | 335 | pipe([ 336 | from.obj([file]), 337 | vfs.symlink(outputBase), 338 | concat(assert), 339 | ], done); 340 | }); 341 | 342 | it('(windows) options can disable junctions for a directory', function(done) { 343 | if (!isWindows) { 344 | this.skip(); 345 | return; 346 | } 347 | 348 | var file = new File({ 349 | base: inputBase, 350 | path: inputDirpath, 351 | contents: null, 352 | stat: { 353 | isDirectory: always(true), 354 | }, 355 | }); 356 | 357 | function assert(files) { 358 | var stats = fs.statSync(outputDirpath); 359 | var lstats = fs.lstatSync(outputDirpath); 360 | var outputLink = fs.readlinkSync(outputDirpath); 361 | 362 | expect(files.length).toEqual(1); 363 | expect(files).toInclude(file); 364 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 365 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 366 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 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: false }), 375 | concat(assert), 376 | ], done); 377 | }); 378 | 379 | it('(windows) options can disable junctions for a directory (as a function)', function(done) { 380 | if (!isWindows) { 381 | this.skip(); 382 | return; 383 | } 384 | 385 | var file = new File({ 386 | base: inputBase, 387 | path: inputDirpath, 388 | contents: null, 389 | stat: { 390 | isDirectory: always(true), 391 | }, 392 | }); 393 | 394 | function useJunctions(f) { 395 | expect(f).toExist(); 396 | expect(f).toBe(file); 397 | return false; 398 | } 399 | 400 | function assert(files) { 401 | var stats = fs.statSync(outputDirpath); 402 | var lstats = fs.lstatSync(outputDirpath); 403 | var outputLink = fs.readlinkSync(outputDirpath); 404 | 405 | expect(files.length).toEqual(1); 406 | expect(files).toInclude(file); 407 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 408 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 409 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 410 | expect(outputLink).toEqual(inputDirpath); 411 | expect(stats.isDirectory()).toEqual(true); 412 | expect(lstats.isDirectory()).toEqual(false); 413 | } 414 | 415 | pipe([ 416 | from.obj([file]), 417 | vfs.symlink(outputBase, { useJunctions: useJunctions }), 418 | concat(assert), 419 | ], done); 420 | }); 421 | 422 | it('(*nix) can create relative links for directories', function(done) { 423 | if (isWindows) { 424 | this.skip(); 425 | return; 426 | } 427 | 428 | var file = new File({ 429 | base: inputBase, 430 | path: inputDirpath, 431 | contents: null, 432 | stat: { 433 | isDirectory: always(true), 434 | }, 435 | }); 436 | 437 | function assert(files) { 438 | var stats = fs.statSync(outputDirpath); 439 | var lstats = fs.lstatSync(outputDirpath); 440 | var outputLink = fs.readlinkSync(outputDirpath); 441 | 442 | expect(files.length).toEqual(1); 443 | expect(files).toInclude(file); 444 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 445 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 446 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 447 | expect(outputLink).toEqual(path.normalize('../fixtures/foo')); 448 | expect(stats.isDirectory()).toEqual(true); 449 | expect(lstats.isDirectory()).toEqual(false); 450 | } 451 | 452 | pipe([ 453 | from.obj([file]), 454 | vfs.symlink(outputBase, { relativeSymlinks: true }), 455 | concat(assert), 456 | ], done); 457 | }); 458 | 459 | it('(windows) relativeSymlinks option is ignored when junctions are used', function(done) { 460 | if (!isWindows) { 461 | this.skip(); 462 | return; 463 | } 464 | 465 | var file = new File({ 466 | base: inputBase, 467 | path: inputDirpath, 468 | contents: null, 469 | stat: { 470 | isDirectory: always(true), 471 | }, 472 | }); 473 | 474 | function assert(files) { 475 | var stats = fs.statSync(outputDirpath); 476 | var lstats = fs.lstatSync(outputDirpath); 477 | var outputLink = fs.readlinkSync(outputDirpath); 478 | 479 | expect(files.length).toEqual(1); 480 | expect(files).toInclude(file); 481 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 482 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 483 | // When creating a junction, it seems Windows appends a separator 484 | expect(files[0].symlink + path.sep).toEqual(outputLink, 'symlink should be set'); 485 | expect(outputLink).toEqual(inputDirpath + path.sep); 486 | expect(stats.isDirectory()).toEqual(true); 487 | expect(lstats.isDirectory()).toEqual(false); 488 | } 489 | 490 | pipe([ 491 | from.obj([file]), 492 | vfs.symlink(outputBase, { useJunctions: true, relativeSymlinks: true }), 493 | concat(assert), 494 | ], done); 495 | }); 496 | 497 | it('(windows) supports relativeSymlinks option when link is not for a directory', function(done) { 498 | if (!isWindows) { 499 | this.skip(); 500 | return; 501 | } 502 | 503 | var file = new File({ 504 | base: inputBase, 505 | path: inputPath, 506 | contents: null, 507 | }); 508 | 509 | function assert(files) { 510 | var outputLink = fs.readlinkSync(outputPath); 511 | 512 | expect(files.length).toEqual(1); 513 | expect(files).toInclude(file); 514 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 515 | expect(files[0].path).toEqual(outputPath, 'path should have changed'); 516 | expect(outputLink).toEqual(path.normalize('../fixtures/test.txt')); 517 | } 518 | 519 | pipe([ 520 | from.obj([file]), 521 | // The useJunctions option is ignored when file is not a directory 522 | vfs.symlink(outputBase, { useJunctions: true, relativeSymlinks: true }), 523 | concat(assert), 524 | ], done); 525 | }); 526 | 527 | it('(windows) can create relative links for directories when junctions are disabled', function(done) { 528 | if (!isWindows) { 529 | this.skip(); 530 | return; 531 | } 532 | 533 | var file = new File({ 534 | base: inputBase, 535 | path: inputDirpath, 536 | contents: null, 537 | stat: { 538 | isDirectory: always(true), 539 | }, 540 | }); 541 | 542 | function assert(files) { 543 | var stats = fs.statSync(outputDirpath); 544 | var lstats = fs.lstatSync(outputDirpath); 545 | var outputLink = fs.readlinkSync(outputDirpath); 546 | 547 | expect(files.length).toEqual(1); 548 | expect(files).toInclude(file); 549 | expect(files[0].base).toEqual(outputBase, 'base should have changed'); 550 | expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); 551 | expect(files[0].symlink).toEqual(outputLink, 'symlink should be set'); 552 | expect(outputLink).toEqual(path.normalize('../fixtures/foo')); 553 | expect(stats.isDirectory()).toEqual(true); 554 | expect(lstats.isDirectory()).toEqual(false); 555 | } 556 | 557 | pipe([ 558 | from.obj([file]), 559 | vfs.symlink(outputBase, { useJunctions: false, relativeSymlinks: true }), 560 | concat(assert), 561 | ], done); 562 | }); 563 | 564 | it('reports IO errors', function(done) { 565 | // Changing the mode of a file is not supported by node.js in Windows. 566 | // This test is skipped on Windows because we have to chmod the file to 0. 567 | if (isWindows) { 568 | this.skip(); 569 | return; 570 | } 571 | 572 | var file = new File({ 573 | base: inputBase, 574 | path: inputPath, 575 | contents: null, 576 | }); 577 | 578 | fs.mkdirSync(outputBase); 579 | fs.chmodSync(outputBase, 0); 580 | 581 | function assert(err) { 582 | expect(err.code).toEqual('EACCES'); 583 | done(); 584 | } 585 | 586 | pipe([ 587 | from.obj([file]), 588 | vfs.symlink(outputDirpath), 589 | ], assert); 590 | }); 591 | 592 | it('does not overwrite links with overwrite option set to false', function(done) { 593 | var existingContents = 'Lorem Ipsum'; 594 | 595 | var file = new File({ 596 | base: inputBase, 597 | path: inputPath, 598 | contents: null, 599 | }); 600 | 601 | function assert(files) { 602 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 603 | 604 | expect(files.length).toEqual(1); 605 | expect(outputContents).toEqual(existingContents); 606 | } 607 | 608 | // Write expected file which should not be overwritten 609 | fs.mkdirSync(outputBase); 610 | fs.writeFileSync(outputPath, existingContents); 611 | 612 | pipe([ 613 | from.obj([file]), 614 | vfs.symlink(outputBase, { overwrite: false }), 615 | concat(assert), 616 | ], done); 617 | }); 618 | 619 | it('overwrites links with overwrite option set to true', function(done) { 620 | var existingContents = 'Lorem Ipsum'; 621 | 622 | var file = new File({ 623 | base: inputBase, 624 | path: inputPath, 625 | contents: null, 626 | }); 627 | 628 | function assert(files) { 629 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 630 | 631 | expect(files.length).toEqual(1); 632 | expect(outputContents).toEqual(contents); 633 | } 634 | 635 | // This should be overwritten 636 | fs.mkdirSync(outputBase); 637 | fs.writeFileSync(outputPath, existingContents); 638 | 639 | pipe([ 640 | from.obj([file]), 641 | vfs.symlink(outputBase, { overwrite: true }), 642 | concat(assert), 643 | ], done); 644 | }); 645 | 646 | it('does not overwrite links with overwrite option set to a function that returns false', function(done) { 647 | var existingContents = 'Lorem Ipsum'; 648 | 649 | var file = new File({ 650 | base: inputBase, 651 | path: inputPath, 652 | contents: null, 653 | }); 654 | 655 | function overwrite(f) { 656 | expect(f).toEqual(file); 657 | return false; 658 | } 659 | 660 | function assert(files) { 661 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 662 | 663 | expect(files.length).toEqual(1); 664 | expect(outputContents).toEqual(existingContents); 665 | } 666 | 667 | // Write expected file which should not be overwritten 668 | fs.mkdirSync(outputBase); 669 | fs.writeFileSync(outputPath, existingContents); 670 | 671 | pipe([ 672 | from.obj([file]), 673 | vfs.symlink(outputBase, { overwrite: overwrite }), 674 | concat(assert), 675 | ], done); 676 | }); 677 | 678 | it('overwrites links with overwrite option set to a function that returns true', function(done) { 679 | var existingContents = 'Lorem Ipsum'; 680 | 681 | var file = new File({ 682 | base: inputBase, 683 | path: inputPath, 684 | contents: null, 685 | }); 686 | 687 | function overwrite(f) { 688 | expect(f).toEqual(file); 689 | return true; 690 | } 691 | 692 | function assert(files) { 693 | var outputContents = fs.readFileSync(outputPath, 'utf8'); 694 | 695 | expect(files.length).toEqual(1); 696 | expect(outputContents).toEqual(contents); 697 | } 698 | 699 | // This should be overwritten 700 | fs.mkdirSync(outputBase); 701 | fs.writeFileSync(outputPath, existingContents); 702 | 703 | pipe([ 704 | from.obj([file]), 705 | vfs.symlink(outputBase, { overwrite: overwrite }), 706 | concat(assert), 707 | ], done); 708 | }); 709 | 710 | it('emits an end event', function(done) { 711 | var symlinkStream = vfs.symlink(outputBase); 712 | 713 | symlinkStream.on('end', done); 714 | 715 | var file = new File({ 716 | base: inputBase, 717 | path: inputPath, 718 | contents: null, 719 | }); 720 | 721 | pipe([ 722 | from.obj([file]), 723 | symlinkStream, 724 | ]); 725 | }); 726 | 727 | it('emits a finish event', function(done) { 728 | var symlinkStream = vfs.symlink(outputBase); 729 | 730 | symlinkStream.on('finish', done); 731 | 732 | var file = new File({ 733 | base: inputBase, 734 | path: inputPath, 735 | contents: null, 736 | }); 737 | 738 | pipe([ 739 | from.obj([file]), 740 | symlinkStream, 741 | ]); 742 | }); 743 | 744 | it('errors when a non-Vinyl object is emitted', function(done) { 745 | var file = {}; 746 | 747 | function assert(err) { 748 | expect(err).toExist(); 749 | expect(err.message).toEqual('Received a non-Vinyl object in `symlink()`'); 750 | done(); 751 | } 752 | 753 | pipe([ 754 | from.obj([file]), 755 | vfs.symlink(outputBase), 756 | ], assert); 757 | }); 758 | 759 | it('errors when a buffer-mode stream is piped to it', function(done) { 760 | var file = Buffer.from('test'); 761 | 762 | function assert(err) { 763 | expect(err).toExist(); 764 | expect(err.message).toEqual('Received a non-Vinyl object in `symlink()`'); 765 | done(); 766 | } 767 | 768 | pipe([ 769 | from([file]), 770 | vfs.symlink(outputBase), 771 | ], assert); 772 | }); 773 | 774 | it('does not get clogged by highWaterMark', function(done) { 775 | var expectedCount = 17; 776 | var highwatermarkFiles = []; 777 | for (var idx = 0; idx < expectedCount; idx++) { 778 | var file = new File({ 779 | base: inputBase, 780 | path: inputPath, 781 | contents: null, 782 | }); 783 | highwatermarkFiles.push(file); 784 | } 785 | 786 | pipe([ 787 | from.obj(highwatermarkFiles), 788 | count(expectedCount), 789 | // Must be in the Writable position to test this 790 | // So concat-stream cannot be used 791 | vfs.symlink(outputBase), 792 | ], done); 793 | }); 794 | 795 | it('allows backpressure when piped to another, slower stream', function(done) { 796 | this.timeout(20000); 797 | 798 | var expectedCount = 24; 799 | var highwatermarkFiles = []; 800 | for (var idx = 0; idx < expectedCount; idx++) { 801 | var file = new File({ 802 | base: inputBase, 803 | path: inputPath, 804 | contents: null, 805 | }); 806 | highwatermarkFiles.push(file); 807 | } 808 | 809 | pipe([ 810 | from.obj(highwatermarkFiles), 811 | count(expectedCount), 812 | vfs.symlink(outputBase), 813 | slowCount(expectedCount), 814 | ], done); 815 | }); 816 | 817 | it('respects readable listeners on symlink stream', function(done) { 818 | var file = new File({ 819 | base: inputBase, 820 | path: inputDirpath, 821 | contents: null, 822 | }); 823 | 824 | var symlinkStream = vfs.symlink(outputBase); 825 | 826 | var readables = 0; 827 | symlinkStream.on('readable', function() { 828 | var data = symlinkStream.read(); 829 | 830 | if (data != null) { 831 | readables++; 832 | } 833 | }); 834 | 835 | function assert(err) { 836 | expect(readables).toEqual(1); 837 | done(err); 838 | } 839 | 840 | pipe([ 841 | from.obj([file]), 842 | symlinkStream, 843 | ], assert); 844 | }); 845 | 846 | it('respects data listeners on symlink stream', function(done) { 847 | var file = new File({ 848 | base: inputBase, 849 | path: inputDirpath, 850 | contents: null, 851 | }); 852 | 853 | var symlinkStream = vfs.symlink(outputBase); 854 | 855 | var datas = 0; 856 | symlinkStream.on('data', function() { 857 | datas++; 858 | }); 859 | 860 | function assert(err) { 861 | expect(datas).toEqual(1); 862 | done(err); 863 | } 864 | 865 | pipe([ 866 | from.obj([file]), 867 | symlinkStream, 868 | ], assert); 869 | }); 870 | 871 | it('sinks the stream if all the readable event handlers are removed', function(done) { 872 | var expectedCount = 17; 873 | var highwatermarkFiles = []; 874 | for (var idx = 0; idx < expectedCount; idx++) { 875 | var file = new File({ 876 | base: inputBase, 877 | path: inputPath, 878 | contents: null, 879 | }); 880 | highwatermarkFiles.push(file); 881 | } 882 | 883 | var symlinkStream = vfs.symlink(outputBase); 884 | 885 | symlinkStream.on('readable', noop); 886 | 887 | pipe([ 888 | from.obj(highwatermarkFiles), 889 | count(expectedCount), 890 | // Must be in the Writable position to test this 891 | // So concat-stream cannot be used 892 | symlinkStream, 893 | ], done); 894 | 895 | process.nextTick(function() { 896 | symlinkStream.removeListener('readable', noop); 897 | }); 898 | }); 899 | 900 | it('sinks the stream if all the data event handlers are removed', function(done) { 901 | var expectedCount = 17; 902 | var highwatermarkFiles = []; 903 | for (var idx = 0; idx < expectedCount; idx++) { 904 | var file = new File({ 905 | base: inputBase, 906 | path: inputPath, 907 | contents: null, 908 | }); 909 | highwatermarkFiles.push(file); 910 | } 911 | 912 | var symlinkStream = vfs.symlink(outputBase); 913 | 914 | symlinkStream.on('data', noop); 915 | 916 | pipe([ 917 | from.obj(highwatermarkFiles), 918 | count(expectedCount), 919 | // Must be in the Writable position to test this 920 | // So concat-stream cannot be used 921 | symlinkStream, 922 | ], done); 923 | 924 | process.nextTick(function() { 925 | symlinkStream.removeListener('data', noop); 926 | }); 927 | }); 928 | 929 | it('does not pass options on to through2', function(done) { 930 | var file = new File({ 931 | base: inputBase, 932 | path: inputPath, 933 | contents: null, 934 | }); 935 | 936 | // Reference: https://github.com/gulpjs/vinyl-fs/issues/153 937 | var read = expect.createSpy().andReturn(false); 938 | 939 | function assert() { 940 | // Called never because it's not a valid option 941 | expect(read.calls.length).toEqual(0); 942 | } 943 | 944 | pipe([ 945 | from.obj([file]), 946 | vfs.symlink(outputBase, { read: read }), 947 | concat(assert), 948 | ], done); 949 | }); 950 | 951 | it('does not marshall a Vinyl object with isSymbolic method', function(done) { 952 | var file = new File({ 953 | base: outputBase, 954 | path: outputPath, 955 | }); 956 | 957 | function assert(files) { 958 | expect(files.length).toEqual(1); 959 | // Avoid comparing stats because they get reflected 960 | delete files[0].stat; 961 | expect(files[0]).toMatch(file); 962 | expect(files[0]).toBe(file); 963 | } 964 | 965 | pipe([ 966 | from.obj([file]), 967 | vfs.symlink(outputBase), 968 | concat(assert), 969 | ], done); 970 | }); 971 | 972 | it('marshalls a Vinyl object without isSymbolic to a newer Vinyl', function(done) { 973 | var file = new File({ 974 | base: outputBase, 975 | path: outputPath, 976 | // Pre-set this because it is set by symlink 977 | symlink: outputPath, 978 | }); 979 | 980 | breakPrototype(file); 981 | 982 | function assert(files) { 983 | expect(files.length).toEqual(1); 984 | // Avoid comparing stats because they get reflected 985 | delete files[0].stat; 986 | expect(files[0]).toMatch(file); 987 | expect(files[0]).toNotBe(file); 988 | } 989 | 990 | pipe([ 991 | from.obj([file]), 992 | vfs.symlink(outputBase), 993 | concat(assert), 994 | ], done); 995 | }); 996 | }); 997 | --------------------------------------------------------------------------------