├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── gulpfile.babel.js ├── license ├── package.json ├── readme.md ├── src └── index.js └── test └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dustinspecker/esnext", 3 | plugins: [ 4 | "no-use-extend-native" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | notifications: 6 | email: false 7 | before_script: 8 | - npm prune 9 | script: 10 | - npm run test 11 | after_success: 12 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' 13 | - python travis_after_all.py 14 | - 'export $(cat .to_export_back) &> /dev/null' 15 | - npm run-script coveralls 16 | branches: 17 | except: 18 | - "/^v\\d+\\.\\d+\\.\\d+$/" 19 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import babel from 'gulp-babel' 3 | import del from 'del' 4 | import gulp from 'gulp' 5 | import eslint from 'gulp-eslint' 6 | import istanbul from 'gulp-istanbul' 7 | import mocha from 'gulp-mocha' 8 | 9 | const configFiles = './gulpfile.babel.js' 10 | const srcFiles = 'src/*.js' 11 | const testFiles = 'test/*.js' 12 | 13 | const destDir = './lib/' 14 | 15 | gulp.task('clean', () => del(destDir)) 16 | 17 | gulp.task('lint', ['clean'], () => 18 | gulp.src([configFiles, srcFiles, testFiles]) 19 | .pipe(eslint()) 20 | .pipe(eslint.failOnError()) 21 | ) 22 | 23 | gulp.task('compile', ['lint'], () => 24 | gulp.src(srcFiles) 25 | .pipe(babel()) 26 | .pipe(gulp.dest(destDir)) 27 | ) 28 | 29 | gulp.task('build', ['compile']) 30 | 31 | gulp.task('test', ['build'], cb => { 32 | gulp.src([`${destDir}*.js`]) 33 | .pipe(istanbul()) 34 | .pipe(istanbul.hookRequire()) 35 | .on('finish', () => { 36 | gulp.src([testFiles]) 37 | .pipe(mocha({ 38 | compilers: { 39 | js: 'js:babel-core/register' 40 | } 41 | })) 42 | .pipe(istanbul.writeReports()) 43 | .on('end', cb) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Dustin Specker 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-modify-css-urls", 3 | "version": "2.0.0", 4 | "description": "Gulp plugin for modifying CSS URLs", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/dustinspecker/gulp-modify-css-urls.git" 10 | }, 11 | "author": { 12 | "name": "Dustin Specker", 13 | "email": "DustinSpecker@DustinSpecker.com", 14 | "url": "https://github.com/dustinspecker" 15 | }, 16 | "engines": { 17 | "node": ">= 4" 18 | }, 19 | "scripts": { 20 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 21 | "test": "gulp test" 22 | }, 23 | "files": [ 24 | "lib" 25 | ], 26 | "keywords": [ 27 | "gulpplugin", 28 | "css" 29 | ], 30 | "dependencies": { 31 | "is-fn": "^1.0.0", 32 | "plugin-error": "^1.0.1", 33 | "rework": "^1.0.1", 34 | "rework-plugin-function": "^1.0.2", 35 | "through2": "^2.0.3", 36 | "vinyl": "^2.1.0", 37 | "vinyl-sourcemaps-apply": "^0.2.1" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.0.12", 41 | "babel-preset-es2015": "^6.0.12", 42 | "chai": "^4.1.2", 43 | "coveralls": "^3.0.0", 44 | "cz-conventional-changelog": "^2.1.0", 45 | "del": "^3.0.0", 46 | "eslint-config-dustinspecker": "^3.0.0", 47 | "eslint-plugin-new-with-error": "^1.1.0", 48 | "eslint-plugin-no-use-extend-native": "^0.3.1", 49 | "eslint-plugin-xo": "^1.0.0", 50 | "gulp": "^3.9.0", 51 | "gulp-babel": "^7.0.0", 52 | "gulp-eslint": "^4.0.0", 53 | "gulp-file": "^0.4.0", 54 | "gulp-istanbul": "^1.0.0", 55 | "gulp-mocha": "^5.0.0", 56 | "gulp-sourcemaps": "^2.6.1" 57 | }, 58 | "config": { 59 | "commitizen": { 60 | "path": "./node_modules/cz-conventional-changelog" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gulp-modify-css-urls 2 | [![NPM version](https://badge.fury.io/js/gulp-modify-css-urls.svg)](http://badge.fury.io/js/gulp-modify-css-urls) 3 | [![Build Status](https://travis-ci.org/dustinspecker/gulp-modify-css-urls.svg?branch=master)](https://travis-ci.org/dustinspecker/gulp-modify-css-urls) 4 | [![Coverage Status](https://img.shields.io/coveralls/dustinspecker/gulp-modify-css-urls.svg)](https://coveralls.io/r/dustinspecker/gulp-modify-css-urls?branch=master) 5 | 6 | [![Dependencies](https://david-dm.org/dustinspecker/gulp-modify-css-urls.svg)](https://david-dm.org/dustinspecker/gulp-modify-css-urls/#info=dependencies&view=table) 7 | [![DevDependencies](https://david-dm.org/dustinspecker/gulp-modify-css-urls/dev-status.svg)](https://david-dm.org/dustinspecker/gulp-modify-css-urls/#info=devDependencies&view=table) 8 | [![PeerDependencies](https://david-dm.org/dustinspecker/gulp-modify-css-urls/peer-status.svg)](https://david-dm.org/dustinspecker/gulp-modify-css-urls/#info=peerDependencies&view=table) 9 | 10 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 11 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 12 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 13 | 14 | > Gulp plugin for modifying CSS URLs 15 | 16 | ## Install 17 | `npm install --save-dev gulp-modify-css-urls` 18 | 19 | ## Usage 20 | 21 | ### ES2015 22 | 23 | ```javascript 24 | /* gulpfile.babel.js */ 25 | import gulp from 'gulp'; 26 | import modifyCssUrls from 'gulp-modify-css-urls'; 27 | 28 | /* style.css 29 | body { 30 | background-image: url('images/logo.png'); 31 | } 32 | */ 33 | gulp.task('modifyUrls', () => 34 | gulp.src('style.css') 35 | .pipe(modifyCssUrls({ 36 | modify(url, filePath) { 37 | return `app/${url}`; 38 | }, 39 | prepend: 'https://fancycdn.com/', 40 | append: '?cache-buster' 41 | })) 42 | .pipe(gulp.dest('./')) 43 | ); 44 | /* style.css 45 | body { 46 | background-image: url('https://fancycdn.com/app/images/logo.png?cache-buster'); 47 | } 48 | */ 49 | ``` 50 | 51 | ### ES5 52 | 53 | ```javascript 54 | /* gulpfile.js */ 55 | var gulp = require('gulp') 56 | , modifyCssUrls = require('gulp-modify-css-urls'); 57 | 58 | /* style.css 59 | body { 60 | background-image: url('images/logo.png'); 61 | } 62 | */ 63 | gulp.task('modifyUrls', function () { 64 | return gulp.src('style.css') 65 | .pipe(modifyCssUrls({ 66 | modify: function (url, filePath) { 67 | return 'app/' + url; 68 | }, 69 | prepend: 'https://fancycdn.com/', 70 | append: '?cache-buster' 71 | })) 72 | .pipe(gulp.dest('./')); 73 | }); 74 | /* style.css 75 | body { 76 | background-image: url('https://fancycdn.com/app/images/logo.png?cache-buster'); 77 | } 78 | */ 79 | ``` 80 | 81 | ## Options 82 | ### modify 83 | A function that is passed the current URL and file path and then returns the modified URL to replace the existent URL. 84 | 85 | **The modify function is always ran *before* append and prepend options.** 86 | 87 | ### append 88 | A string that is appended to every URL. 89 | 90 | ### prepend 91 | A string that is prepended to every URL. 92 | 93 | ## License 94 | MIT 95 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import applySourceMap from 'vinyl-sourcemaps-apply' 2 | import PluginError from 'plugin-error' 3 | import isFn from 'is-fn' 4 | import rework from 'rework' 5 | import reworkFunc from 'rework-plugin-function' 6 | import through from 'through2' 7 | 8 | /** 9 | * Transforms URLs in files 10 | * @param {String} filePath - path of CSS file that may be used by options.modify 11 | * @param {String} fileContents - contents of the file at filePath 12 | * @param {Boolean} sourcemap - is sourcemap enabled for this file? 13 | * @param {Object} [options={}] - rules for modifying URLs 14 | * @param {String} [options.append] - URLs are appended with this value 15 | * @param {Function} [options.modify] - This function is always called before append and prepend 16 | * @param {String} [options.prepend] - URLs are prepended with this value 17 | * @return {String} - transformed URL 18 | */ 19 | const modifyUrls = (filePath, fileContents, sourcemap, options = {}) => { 20 | const {append, modify, prepend} = options 21 | 22 | return rework(fileContents, {source: filePath}) 23 | .use(reworkFunc({ 24 | url(url) { 25 | /** 26 | * The split/join/trim logic is copied from rework-plugin-url to remove redundant quotes. 27 | * Currently removed due to: https://github.com/reworkcss/rework-plugin-url/issues/7 28 | */ 29 | if (url.indexOf('data:') === 0) { 30 | return `url("${url}")` 31 | } 32 | 33 | let formattedUrl = url 34 | .split('"') 35 | .join('') 36 | .split('\'') 37 | .join('') 38 | .trim() 39 | 40 | if (isFn(modify)) { 41 | formattedUrl = modify(formattedUrl, filePath) 42 | } 43 | 44 | if (typeof prepend === 'string') { 45 | formattedUrl = prepend + formattedUrl 46 | } 47 | 48 | if (typeof append === 'string') { 49 | formattedUrl += append 50 | } 51 | 52 | return `url("${formattedUrl}")` 53 | } 54 | }, false)) 55 | .toString({sourcemap, sourcemapAsObject: true}) 56 | } 57 | 58 | /** 59 | * Pushes along files with transformed URLs 60 | * @param {Object} [options] - same as described for modifyUrls function 61 | * @return {Stream} - file with transformed URLs 62 | */ 63 | module.exports = options => 64 | through.obj(function (file, enc, cb) { 65 | try { 66 | /* eslint no-invalid-this: 0 */ 67 | const modifiedContents = modifyUrls(file.path, file.contents.toString(), file.sourceMap, options) 68 | 69 | if (file.sourceMap) { 70 | file.contents = Buffer.from(modifiedContents.code) 71 | modifiedContents.map.file = file.path 72 | applySourceMap(file, modifiedContents.map) 73 | } else { 74 | file.contents = Buffer.from(modifiedContents) 75 | } 76 | 77 | this.push(file) 78 | 79 | return cb() 80 | } catch (e) { 81 | return cb(new PluginError('modify-css-urls', e)) 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, it */ 2 | import assert from 'assert' 3 | import gulp from 'gulp' 4 | import gFile from 'gulp-file' 5 | import Vinyl from 'vinyl' 6 | import PluginError from 'plugin-error' 7 | import sourcemaps from 'gulp-sourcemaps' 8 | 9 | import modifyCssUrls from '../lib' 10 | 11 | describe('gulp-modify-css-urls', () => { 12 | let fileContents, stream 13 | 14 | beforeEach(() => { 15 | fileContents = [ 16 | 'body {\n', 17 | ' background-image: url("images/logo.png");\n', 18 | '}' 19 | ].join('') 20 | }) 21 | 22 | it('should not change anything in fileContents if no option set', done => { 23 | stream = modifyCssUrls() 24 | 25 | stream.on('data', file => { 26 | assert(file.contents.toString() === fileContents) 27 | done() 28 | }) 29 | 30 | stream.write(new Vinyl({ 31 | base: '.', 32 | path: './style.css', 33 | contents: Buffer.from(fileContents) 34 | })) 35 | 36 | stream.end() 37 | }) 38 | 39 | it('should return error when file has invalid CSS and not pass file through', done => { 40 | stream = modifyCssUrls() 41 | 42 | stream.on('data', () => { 43 | assert(false) 44 | }) 45 | 46 | stream.on('error', error => { 47 | assert(error instanceof PluginError) 48 | assert(error.plugin === 'modify-css-urls') 49 | done() 50 | }) 51 | 52 | stream.write(new Vinyl({ 53 | base: '.', 54 | path: './style.css', 55 | contents: Buffer.from('invalid css') 56 | })) 57 | 58 | stream.end() 59 | }) 60 | 61 | it('should support sourcemaps when enabled', done => { 62 | let originalSourcemap 63 | 64 | stream = gulp.src([]) 65 | .pipe(gFile('./style.css', fileContents)) 66 | .pipe(sourcemaps.init()) 67 | .on('data', file => { 68 | originalSourcemap = file.sourceMap 69 | }) 70 | .pipe(modifyCssUrls({ 71 | modify: (url, filePath) => `app/${filePath}${url}` 72 | })) 73 | .on('data', file => { 74 | assert(file.sourceMap !== originalSourcemap) 75 | }) 76 | .pipe(sourcemaps.write()) 77 | 78 | stream.on('finish', () => { 79 | done() 80 | }) 81 | 82 | stream.write(new Vinyl({ 83 | base: '.', 84 | path: './style.css', 85 | contents: Buffer.from(fileContents) 86 | })) 87 | }) 88 | 89 | it('should add app folder to CSS URL', done => { 90 | stream = modifyCssUrls({ 91 | modify: (url, filePath) => `app/${filePath}${url}` 92 | }) 93 | 94 | stream.on('data', file => { 95 | const expectedCss = [ 96 | 'body {\n', 97 | ' background-image: url("app/style.cssimages/logo.png");\n', 98 | '}' 99 | ].join('') 100 | 101 | assert(file.contents.toString() === expectedCss) 102 | done() 103 | }) 104 | 105 | stream.write(new Vinyl({ 106 | base: '.', 107 | path: './style.css', 108 | contents: Buffer.from(fileContents) 109 | })) 110 | 111 | stream.end() 112 | }) 113 | 114 | it('should prepend url with string value', done => { 115 | stream = modifyCssUrls({ 116 | prepend: 'https://fancycdn.com/' 117 | }) 118 | 119 | stream.on('data', file => { 120 | const expectedCss = [ 121 | 'body {\n', 122 | ' background-image: url("https://fancycdn.com/images/logo.png");\n', 123 | '}' 124 | ].join('') 125 | 126 | assert(file.contents.toString() === expectedCss) 127 | done() 128 | }) 129 | 130 | stream.write(new Vinyl({ 131 | base: '.', 132 | path: './style.css', 133 | contents: Buffer.from(fileContents) 134 | })) 135 | 136 | stream.end() 137 | }) 138 | 139 | it('should append url with string value', done => { 140 | stream = modifyCssUrls({ 141 | append: '?abcd1234' 142 | }) 143 | 144 | stream.on('data', file => { 145 | const expectedCss = [ 146 | 'body {\n', 147 | ' background-image: url("images/logo.png?abcd1234");\n', 148 | '}' 149 | ].join('') 150 | 151 | assert(file.contents.toString() === expectedCss) 152 | done() 153 | }) 154 | 155 | stream.write(new Vinyl({ 156 | base: '.', 157 | path: './style.css', 158 | contents: Buffer.from(fileContents) 159 | })) 160 | 161 | stream.end() 162 | }) 163 | 164 | it('should not modify data uris', done => { 165 | const fileContentsWithDataURI = [ 166 | 'body {\n', 167 | ' background-image: url("data:image/png;base64,iVBORw");\n', 168 | '}' 169 | ].join('') 170 | 171 | stream = modifyCssUrls({ 172 | append: '?abcd1234' 173 | }) 174 | 175 | stream.on('data', file => { 176 | assert(file.contents.toString() === fileContentsWithDataURI) 177 | done() 178 | }) 179 | 180 | stream.write(new Vinyl({ 181 | base: '.', 182 | path: './style.css', 183 | contents: Buffer.from(fileContentsWithDataURI) 184 | })) 185 | 186 | stream.end() 187 | }) 188 | 189 | it('should not strip quotes from data uris', done => { 190 | const fileContentsWithDataURI = [ 191 | 'body {\n', 192 | ' background-image: url("data:image/svg+xml;utf8,");\n', 193 | '}' 194 | ].join('') 195 | 196 | stream = modifyCssUrls({ 197 | append: '?abcd1234' 198 | }) 199 | 200 | stream.on('data', file => { 201 | assert(file.contents.toString() === fileContentsWithDataURI) 202 | done() 203 | }) 204 | 205 | stream.write(new Vinyl({ 206 | base: '.', 207 | path: './style.css', 208 | contents: Buffer.from(fileContentsWithDataURI) 209 | })) 210 | 211 | stream.end() 212 | }) 213 | }) 214 | --------------------------------------------------------------------------------