├── .gitignore ├── .travis.yml ├── test ├── rev-qs-manifest.json ├── app.css ├── rev-manifest.json ├── test-qs.js └── test.js ├── .editorconfig ├── .jshintrc ├── package.json ├── readme.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8.15.0' 4 | - '10.16.3' 5 | - '12.9.0' 6 | -------------------------------------------------------------------------------- /test/rev-qs-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "images/favicon.ico": "images/favicon-99999.ico", 3 | "js/site.js": "js/site.js?v=10923", 4 | "js/other.js": "js/other.js?v=190283091" 5 | } -------------------------------------------------------------------------------- /test/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: url("/images/body-bg.jpg"); 3 | background-attachment: fixed; } 4 | 5 | .logo { 6 | background-image: url("/images/some-logo.png"); 7 | } 8 | -------------------------------------------------------------------------------- /test/rev-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "images/body-bg.jpg": "images/body-bg-2d4a1176.jpg", 3 | "images/some-logo.png": "images/some-logo-abd84705.png", 4 | "images/some-logo2.png": "images/some-logo2-abd84715.png" 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": false 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-fingerprint", 3 | "version": "1.0.0", 4 | "description": "Rename assets with fingerprinted assets", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/vincentmac/gulp-fingerprint" 9 | }, 10 | "author": { 11 | "name": "Vincent Mac", 12 | "email": "vincent@simplicity.io", 13 | "url": "http://simplicity.io" 14 | }, 15 | "engines": { 16 | "node": ">=8.15.0" 17 | }, 18 | "keywords": [ 19 | "asset", 20 | "assetpipeline", 21 | "fingerprint", 22 | "gulpplugin", 23 | "manifest", 24 | "pipeline", 25 | "regex", 26 | "rename", 27 | "streams", 28 | "vinyl" 29 | ], 30 | "main": "index.js", 31 | "scripts": { 32 | "test": "mocha --reporter spec" 33 | }, 34 | "dependencies": { 35 | "chalk": "2.4.2", 36 | "fancy-log": "1.3.3", 37 | "plugin-error": "1.0.1", 38 | "split2": "3.1.1", 39 | "through2": "3.0.1", 40 | "vinyl": "2.2.0" 41 | }, 42 | "devDependencies": { 43 | "mocha": "6.2.0" 44 | }, 45 | "readmeFilename": "readme.md", 46 | "bugs": { 47 | "url": "https://github.com/vincentmac/gulp-fingerprint/issues" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/test-qs.js: -------------------------------------------------------------------------------- 1 | /* globals it*/ 2 | 'use strict'; 3 | 4 | var assert = require('assert'); 5 | var Vinyl = require('vinyl'); 6 | var fingerprint = require('../'); 7 | var manifest = require('./rev-qs-manifest'); 8 | var fakeFile = '' + 9 | '\n' + 10 | ' Some Web Site\n' + 11 | ' \n' + 12 | ' \n' + 13 | ' \n' + 14 | '\n' + 15 | '\n' + 16 | '\n' + 17 | ''; 18 | 19 | ['regex', 'replace'].forEach(function(mode) { 20 | 21 | describe('in `' + mode + '` mode', function () { 22 | 23 | it('should replace query string based fingerprints', function (done) { 24 | var stream = fingerprint(manifest, {regex: /(?:href=|src=)"([^\"]*)"/}); 25 | 26 | stream.on('data', function (file) { 27 | var updatedHTML = file.contents.toString(); 28 | // console.log(updatedHTML); 29 | var regex1 = /images\/favicon-99999\.ico/; 30 | var regex2 = /js\/site\.js\?v=10923/; 31 | var regex3 = /js\/other\.js\?v=190283091/; 32 | var match1 = regex1.exec(updatedHTML); 33 | var match2 = regex2.exec(updatedHTML); 34 | var match3 = regex3.exec(updatedHTML); 35 | 36 | assert.equal(match1[0], 'images/favicon-99999.ico'); 37 | assert.equal(match2[0], 'js/site.js?v=10923'); 38 | assert.equal(match3[0], 'js/other.js?v=190283091'); 39 | done(); 40 | }); 41 | 42 | stream.write(new Vinyl({ 43 | path: 'app.html', 44 | contents: new Buffer(fakeFile) 45 | })); 46 | 47 | }); 48 | 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [gulp](http://gulpjs.com)-fingerprint [![Build Status](https://travis-ci.org/vincentmac/gulp-fingerprint.svg?branch=master)](https://travis-ci.org/vincentmac/gulp-fingerprint) 2 | 3 | ## Install 4 | 5 | ```bash 6 | $ npm install --save-dev gulp-fingerprint 7 | ``` 8 | 9 | 10 | ## Usage 11 | 12 | Update a source file with fingerprinted assets. 13 | 14 | ```js 15 | var gulp = require('gulp'); 16 | var fingerprint = require('gulp-fingerprint'); 17 | 18 | // rev-manifest.json produced from gulp-rev 19 | var manifest = require('../../dist/rev-manifest'); 20 | 21 | gulp.task('default', function () { 22 | var options = { 23 | base: 'assets/', 24 | prefix: '//cdn.example.com/', 25 | verbose: true 26 | }; 27 | 28 | return gulp.src('.tmp/styles/app.css') 29 | .pipe(fingerprint(manifest, options)) 30 | .pipe(gulp.dest('dist')); 31 | }); 32 | ``` 33 | 34 | 35 | ## API 36 | 37 | ### fingerprint(manifest, [options]) 38 | 39 | #### manifest 40 | 41 | _Type_: `object, string` 42 | 43 | _Example_: `rev-manifest.json` produced from using [gulp-rev](https://www.npmjs.org/package/gulp-rev) 44 | ```json 45 | { 46 | "images/logo.jpg": "images/logo-2d4a1176.jpg", 47 | "images/some-image.png": "images/some-image-abd84705.png", 48 | "images/some-logo2.png": "images/some-logo2-abd84715.png" 49 | } 50 | ``` 51 | 52 | If a `string` is passed in as the manifest, gulp-fingerprint will interpret this as a path and automatically require the json file. 53 | 54 | #### options 55 | 56 | ##### mode 57 | _Type_: `string` 58 | 59 | _Default_: `regex` 60 | 61 | _Usage_: Setting a `mode` will change the method of url replacing. There are two methods: `regex` and `replace`. The `replace` method is less accurate but doesn't require specifying a regular expression. 62 | 63 | ##### regex 64 | _Type_: [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) 65 | 66 | _Usage_: Sets a custom regex to match on your file. 67 | 68 | _ **Note** The default regex, `/(?:url\(["']?(.*?)['"]?\)|src=["'](.*?)['"]|src=([^\s\>]+)(?:\>|\s)|href=["'](.*?)['"]|href=([^\s\>]+)(?:\>|\s))/g`, will match: 69 | 70 | - `url('path/to/resource')` 71 | - `url("path/to/resource")` 72 | - `url(path/to/resource)` 73 | - `href='path/to/resource'` 74 | - `href="path/to/resource"` 75 | - `href=path/to/resource` 76 | - `src='path/to/resource'` 77 | - `src="path/to/resource"` 78 | - `src=path/to/resource` 79 | 80 | ##### prefix 81 | _Type_: `string` 82 | 83 | _Usage_: Setting a `prefix` will prepend the string to a match in the src 84 | ```js 85 | ... 86 | .pipe(fingerprint(manifest, {prefix: '//cdn.example.com/'})) 87 | ... 88 | // Original: `background-image: url("/images/some-logo.png");` 89 | // Replaced: `background-image: url("//cdn.example.com/images/logo-2d4a1176.jpg");` in src file 90 | ``` 91 | 92 | ##### base 93 | _Type_: `string` 94 | 95 | _Usage_: Setting a `base` will remove that string from the beginning of a match in the src 96 | ```js 97 | ... 98 | .pipe(fingerprint(manifest, {base: 'assets/'})) 99 | ... 100 | 101 | // Original: `background-image: url("assets/images/some-logo2.png");` 102 | // Replaced: `background-image: url("images/some-logo2-abd84715.png");` in src file 103 | ``` 104 | 105 | ##### strip 106 | _Type_: `string` 107 | 108 | _Usage_: Setting a `strip` will remove that string from the beginning of a result path 109 | ```js 110 | ... 111 | .pipe(fingerprint(manifest, {strip: 'images/'})) 112 | ... 113 | 114 | // Original: `background-image: url("/images/some-logo2.png");` 115 | // Replaced: `background-image: url("some-logo2-abd84715.png");` in src file 116 | ``` 117 | 118 | ##### verbose 119 | _Type_: `boolean` 120 | 121 | _Usage_: Outputs to stdout. 122 | 123 | ```bash 124 | 125 | [gulp] gulp-fingerprint Found: images/some-logo.png 126 | [gulp] gulp-fingerprint Replaced: background-image: url("//cdn.example.com/images/logo-2d4a1176.jpg"); } 127 | ``` 128 | 129 | ## License 130 | 131 | [MIT](http://opensource.org/licenses/MIT) © [Vincent Mac](http://simplicity.io) 132 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chalk = require('chalk'); 4 | var path = require('path'); 5 | var split = require('split2'); 6 | var through = require('through2'); 7 | var log = require('fancy-log'); 8 | var PluginError = require('plugin-error'); 9 | 10 | var PLUGIN_NAME = 'gulp-fingerprint'; 11 | 12 | /** 13 | * Gulp Plugin to stream through a file and rename regex matches 14 | * 15 | * @param {Object} manifest - rev-manifest 16 | * @param {Object} options 17 | */ 18 | var plugin = function(manifest, options) { 19 | options = options || {}; 20 | 21 | // Default regex to allow for single and double quotes 22 | // var regex = new RegExp('url\\("(.*)"\\)|src="(.*)"|href="(.*)"|url\\(\'(.*)\'\\)|src=\'(.*)\'|href=\'(.*)\'', 'g'); 23 | var regex = /(?:url\(["']?(.*?)['"]?\)|src=["'](.*?)['"]|src=([^\s\>]+)(?:\>|\s)|href=["'](.*?)['"]|href=([^\s\>]+)(?:\>|\s))/g; 24 | var prefix = ''; 25 | var base = ''; 26 | var strip = ''; 27 | var mode = 'regex'; 28 | var content = []; 29 | 30 | // Use custom RegExp 31 | if (options.regex) regex = options.regex; 32 | 33 | if (options.prefix) prefix = options.prefix; 34 | 35 | if (options.base) base = options.base.replace(/^\//, ''); 36 | 37 | if (options.strip) strip = options.strip.replace(/^\//, ''); 38 | 39 | if (options.mode === 'replace') { 40 | mode = 'replace'; 41 | } 42 | 43 | if (strip) { 44 | var stripRegex = new RegExp('^\/' + strip + '|^' + strip); 45 | } 46 | 47 | if (base) { 48 | var baseRegex = new RegExp('^\/' + base + '|^' + base); 49 | } 50 | 51 | if (typeof(manifest) === 'string') { 52 | manifest = require(path.resolve(manifest)); 53 | } 54 | 55 | function regexMode(buf, enc, cb) { 56 | var line = buf.toString(); 57 | 58 | line = line.replace(regex, function(str, i) { 59 | var url = Array.prototype.slice.call(arguments, 1).filter(function(a) { return typeof a === 'string'; })[0]; 60 | if (options.verbose) log(PLUGIN_NAME, 'Found:', chalk.yellow(url.replace(/^\//, ''))); 61 | var replaced = manifest[url] || manifest[url.replace(/^\//, '')] || manifest[url.split(/[#?]/)[0]]; 62 | if (!replaced && base) replaced = manifest[url.replace(baseRegex, '')]; 63 | if (replaced) { 64 | if (strip) { 65 | replaced = replaced.replace(stripRegex, ''); 66 | } 67 | str = str.replace(url, prefix + replaced); 68 | } 69 | if (options.verbose) log(PLUGIN_NAME, 'Replaced:', chalk.green(prefix + replaced)); 70 | return str; 71 | }); 72 | 73 | content.push(line); 74 | cb(); 75 | } 76 | 77 | function replaceMode(buf, enc, cb) { 78 | var line = buf.toString(); 79 | 80 | base = base.replace(/(^\/|\/$)/g, ''); 81 | 82 | for (var url in manifest) { 83 | var dest = manifest[url], replaced, bases; 84 | if (strip) { 85 | replaced = prefix + dest.replace(stripRegex, ''); 86 | } else { 87 | replaced = prefix + dest; 88 | } 89 | bases = ['/', '']; 90 | if (base) { 91 | bases.unshift('/' + base + '/', base + '/'); 92 | } 93 | for (var i = 0; i < bases.length; i++) { 94 | var newLine = line.split(bases[i] + url).join(replaced); 95 | if (line !== newLine) { 96 | if (options.verbose) log(PLUGIN_NAME, 'Found:', chalk.yellow(url.replace(/^\//, ''))); 97 | if (options.verbose) log(PLUGIN_NAME, 'Replaced:', chalk.green(prefix + replaced)); 98 | line = newLine; 99 | break; 100 | } 101 | } 102 | } 103 | 104 | content.push(line); 105 | cb(); 106 | } 107 | 108 | var stream = through.obj(function(file, enc, cb) { 109 | var that = this; 110 | content = []; // reset file content 111 | 112 | if (file.isNull()) { 113 | this.push(file); 114 | return cb(); 115 | } 116 | // console.log(file.contents); 117 | 118 | if (file.isStream()) { 119 | // console.log('is Stream'); 120 | this.emit('error', new PluginError(PLUGIN_NAME, 'Streaming not supported')); 121 | return cb(); 122 | } 123 | 124 | if (file.isBuffer()) { 125 | // console.log('is Buffer'); 126 | 127 | // ugly fix to put in back the deprecated pipe fn they have removed, see: 128 | // https://github.com/gulpjs/vinyl/commit/d14ba4a7b51f0f3682f65f2aa4314d981eb1029d 129 | // although this works here and restores same functionality. 130 | // TODO consider rewriting whole stream logic 131 | file.pipe = function(stream, opt = {end: true}) { 132 | if (this.isStream()) { 133 | return this.contents.pipe(stream, opt); 134 | } 135 | 136 | if (this.isBuffer()) { 137 | if (opt.end) { 138 | stream.end(this.contents); 139 | } else { 140 | stream.write(this.contents); 141 | } 142 | return stream; 143 | } 144 | 145 | if (opt.end) { 146 | stream.end(); 147 | } 148 | 149 | return stream; 150 | }; 151 | 152 | file 153 | .pipe(split()) 154 | .pipe(through(mode === 'regex' ? regexMode : replaceMode, function(callback) { 155 | if (content.length) { 156 | file.contents = new Buffer(content.join('\n')); 157 | that.push(file); 158 | } 159 | // callback(); 160 | cb(); 161 | })); 162 | } 163 | 164 | }); 165 | 166 | return stream; 167 | }; 168 | 169 | module.exports = plugin; 170 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* globals it*/ 2 | 'use strict'; 3 | 4 | var assert = require('assert'); 5 | var Vinyl = require('vinyl'); 6 | var fingerprint = require('../'); 7 | var manifest = require('./rev-manifest'); 8 | 9 | var fakeCssFile = 'body {\n' + 10 | ' background-image: url("/images/body-bg.jpg");' + 11 | ' background-image: url("/images/body-bg.jpg");\n' + 12 | ' background-attachment: fixed;\n' + 13 | '}\n' + 14 | '.logo {\n' + 15 | ' background-image: url(/images/some-logo.png);\n' + 16 | '}\n' + 17 | '.logo2 {\n' + 18 | ' background-image: url(\'assets/images/some-logo2.png\');\n' + 19 | ' background-image: url(\'assets/images/some-logo2.png\');\n' + 20 | ' background-image: url("/images/some-logo2.png");\n' + 21 | ' background-image: url("/images/some-logo2.png");\n' + 22 | '}' 23 | ; 24 | var fakeHtmlFile = '\n' + 25 | ' \n' + 26 | ' \n' + 27 | ' \n' + 28 | ' \n' + 29 | ' \n' + 30 | ' \n' + 31 | ' \n' + 32 | ''; 33 | 34 | ['regex', 'replace'].forEach(function(mode) { 35 | 36 | describe('in `' + mode + '` mode', function() { 37 | 38 | it('should update multiple assets in one file', function (done) { 39 | var stream = fingerprint(manifest, { mode: mode }); 40 | 41 | stream.on('data', function (file) { 42 | var updatedCSS = file.contents.toString(); 43 | var regex1 = /images\/body-bg-2d4a1176.jpg/g; 44 | var regex2 = /images\/some-logo-abd84705.png/; 45 | var match1 = regex1.exec(updatedCSS); 46 | var match2 = regex2.exec(updatedCSS); 47 | 48 | assert.equal(match1[0], 'images/body-bg-2d4a1176.jpg'); 49 | assert.equal(match2[0], 'images/some-logo-abd84705.png'); 50 | done(); 51 | }); 52 | 53 | stream.write(new Vinyl({ 54 | path: 'app.css', 55 | contents: new Buffer(fakeCssFile) 56 | })); 57 | 58 | }); 59 | 60 | it('should prepend assets in one file', function (done) { 61 | var stream = fingerprint(manifest, {prefix: 'https://cdn.example.com/'}); 62 | 63 | stream.on('data', function (file) { 64 | var updatedCSS = file.contents.toString(); 65 | var regex1 = /https\:\/\/cdn.example.com\/images\/body-bg-2d4a1176.jpg/; 66 | var regex2 = /https\:\/\/cdn.example.com\/images\/some-logo-abd84705.png/; 67 | var match1 = regex1.exec(updatedCSS); 68 | var match2 = regex2.exec(updatedCSS); 69 | 70 | assert.equal(match1[0], 'https://cdn.example.com/images/body-bg-2d4a1176.jpg'); 71 | assert.equal(match2[0], 'https://cdn.example.com/images/some-logo-abd84705.png'); 72 | done(); 73 | }); 74 | 75 | stream.write(new Vinyl({ 76 | path: 'app.css', 77 | contents: new Buffer(fakeCssFile) 78 | })); 79 | 80 | }); 81 | 82 | it('should match assets with an optional base', function (done) { 83 | var stream = fingerprint(manifest, {base: 'assets/'}); 84 | 85 | stream.on('data', function (file) { 86 | var updatedCSS = file.contents.toString(); 87 | var regex1 = /images\/body-bg-2d4a1176.jpg/; 88 | var regex2 = /images\/some-logo-abd84705.png/; 89 | var regex3 = /images\/some-logo2-abd84715.png/; 90 | var match1 = regex1.exec(updatedCSS); 91 | var match2 = regex2.exec(updatedCSS); 92 | var match3 = regex3.exec(updatedCSS); 93 | 94 | assert.equal(match1[0], 'images/body-bg-2d4a1176.jpg'); 95 | assert.equal(match2[0], 'images/some-logo-abd84705.png'); 96 | assert.equal(match3[0], 'images/some-logo2-abd84715.png'); 97 | done(); 98 | }); 99 | 100 | stream.write(new Vinyl({ 101 | path: 'app.css', 102 | contents: new Buffer(fakeCssFile) 103 | })); 104 | 105 | }); 106 | 107 | 108 | it('should match assets with an optional base and prepend text', function (done) { 109 | var stream = fingerprint(manifest, { 110 | base: 'assets\\/', 111 | prefix: 'https://cdn.example.com/' 112 | }); 113 | 114 | stream.on('data', function (file) { 115 | var updatedCSS = file.contents.toString(); 116 | var regex1 = /https\:\/\/cdn.example.com\/images\/body-bg-2d4a1176.jpg/; 117 | var regex2 = /https\:\/\/cdn.example.com\/images\/some-logo-abd84705.png/; 118 | var regex3 = /https\:\/\/cdn.example.com\/images\/some-logo2-abd84715.png/; 119 | var match1 = regex1.exec(updatedCSS); 120 | var match2 = regex2.exec(updatedCSS); 121 | var match3 = regex3.exec(updatedCSS); 122 | 123 | assert.equal(match1[0], 'https://cdn.example.com/images/body-bg-2d4a1176.jpg'); 124 | assert.equal(match2[0], 'https://cdn.example.com/images/some-logo-abd84705.png'); 125 | assert.equal(match3[0], 'https://cdn.example.com/images/some-logo2-abd84715.png'); 126 | done(); 127 | }); 128 | 129 | stream.write(new Vinyl({ 130 | path: 'app.css', 131 | contents: new Buffer(fakeCssFile) 132 | })); 133 | 134 | }); 135 | 136 | it('should match several assets in one line', function(done) { 137 | var stream = fingerprint(manifest, { mode: mode }); 138 | 139 | stream.on('data', function (file) { 140 | var updatedCSS = file.contents.toString(); 141 | var regex = /images\/body-bg-2d4a1176.jpg/g; 142 | 143 | assert.equal(updatedCSS.match(regex).length, 2); 144 | done(); 145 | }); 146 | 147 | stream.write(new Vinyl({ 148 | path: 'app.css', 149 | contents: new Buffer(fakeCssFile) 150 | })); 151 | }); 152 | 153 | it('should match assets in html', function(done) { 154 | var stream = fingerprint(manifest, { mode: mode }); 155 | 156 | stream.on('data', function (file) { 157 | var updatedCSS = file.contents.toString(); 158 | var regex1 = /images\/body-bg-2d4a1176.jpg/g; 159 | var regex2 = /images\/some-logo-abd84705.png/g; 160 | var match1 = regex1.exec(updatedCSS); 161 | var match2 = regex2.exec(updatedCSS); 162 | 163 | assert.equal(match1[0], 'images/body-bg-2d4a1176.jpg'); 164 | assert.equal(match2[0], 'images/some-logo-abd84705.png'); 165 | assert.equal(updatedCSS.match(regex1).length, 3); 166 | assert.equal(updatedCSS.match(regex2).length, 3); 167 | done(); 168 | }); 169 | 170 | stream.write(new Vinyl({ 171 | path: 'app.html', 172 | contents: new Buffer(fakeHtmlFile) 173 | })); 174 | }); 175 | 176 | it('should strip asset path', function (done) { 177 | var stream = fingerprint(manifest, { 178 | strip: '/images/' 179 | }); 180 | 181 | stream.on('data', function (file) { 182 | var updatedCSS = file.contents.toString(); 183 | var regex1 = /"body-bg-2d4a1176.jpg"/g; 184 | var match1 = regex1.exec(updatedCSS); 185 | 186 | assert.equal(match1[0], '"body-bg-2d4a1176.jpg"'); 187 | done(); 188 | }); 189 | 190 | stream.write(new Vinyl({ 191 | path: 'app.css', 192 | contents: new Buffer(fakeCssFile) 193 | })); 194 | 195 | }); 196 | 197 | }); 198 | 199 | }); 200 | --------------------------------------------------------------------------------