├── .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 [](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 |
--------------------------------------------------------------------------------