├── .editorconfig ├── .gitignore ├── .tern-project ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appveyor.yml ├── package.json └── src ├── __tests__ ├── check-files-with-spaces.js ├── common-tests.json ├── copy.js ├── external_libs │ └── bootstrap │ │ ├── css │ │ └── bootstrap.css │ │ └── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 ├── helpers │ ├── index.js │ ├── make-regex.js │ ├── process-style.js │ ├── random-folder.js │ └── read-file.js ├── ignore.js ├── index.js ├── options.js ├── src │ ├── check-files-with-spaces.css │ ├── check-transform-hash.css │ ├── check-transform.css │ ├── component │ │ ├── images │ │ │ └── component.jpg │ │ └── index.css │ ├── correct-parse-url.css │ ├── fonts │ │ ├── samefile.woff │ │ └── samefile.woff2 │ ├── ignore.css │ ├── images │ │ ├── batman.jpg │ │ ├── bigimage.jpg │ │ ├── file space.jpg │ │ ├── noignore.jpg │ │ ├── other.jpg │ │ ├── superman.jpg │ │ ├── test-modified.jpg │ │ ├── test-unmodified.jpg │ │ └── test.jpg │ ├── index.css │ ├── invalid.css │ ├── no-repeat-transform.css │ └── not-found.css ├── template.js ├── tools-path.js ├── transform.js └── validate-url.js ├── index.js └── lib ├── copy.js └── tools-path.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | # Indentation override for all JS under lib directory 14 | [*.{js,css}] 15 | indent_style = space 16 | indent_size = 4 17 | 18 | # Indentation override for all JS under lib directory 19 | [*.{html,json,yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/__tests__/dest 4 | .nyc_output 5 | npm-debug.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 6, 3 | "libs": [], 4 | "plugins": { 5 | "node": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - "6" 5 | - "4" 6 | after_success: npm run coverage 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [7.1.0] - 2017-06-30 10 | ### Fixed 11 | - Wrong encode in paths with spaces: #54 12 | 13 | ## [7.0.0] - 2017-05-06 14 | ### Added 15 | - preservePath option to work with gulp or postcss-cli and their --base option 16 | 17 | ### Changed 18 | - _breaking change_: The `src` option is ambiguous to the real objective, so I changed it to `basePath` 19 | - `basePath` (old `src`) is now optional and the default path is `process.cwd()` 20 | 21 | ### Removed 22 | - relativePath and inputPath in favor of simplify the API of postcss-copy 23 | 24 | ## [6.2.1] - 2016-12-07 [YANKED] 25 | 26 | ## [6.2.0] - 2016-12-06 27 | ### Added 28 | - cache for the write process to don\`t overwrite the output file (fixed race condition) 29 | 30 | ## [6.1.0] - 2016-12-02 31 | ### Fixed 32 | - concurrent tests and cache function for the transform process 33 | - hash parameter content 34 | 35 | ## [6.0.0] - 2016-11-30 36 | ### Changed 37 | - execution of transform function: run before of the hash function (#49) 38 | 39 | ## [5.3.0] - 2016-10-10 40 | ### Fixed 41 | - sould not repeat the transform process when the source is the same (related #46) 42 | 43 | ## [5.2.0] - 2016-09-18 44 | ### Fixed 45 | - relativePath must return a valid dirname 46 | 47 | ## [5.1.0] - 2016-09-18 48 | ### Fixed 49 | - a regression in the relativePath usage :bug: 50 | 51 | ## [5.0.1] - 2016-07-20 52 | ### Added 53 | - CI for node v6 54 | 55 | ### Fixed 56 | - revert update of eslint and path-exists since the new versions does not work with node v0.12 57 | 58 | ## [5.0.0] - 2016-07-20 59 | ### Added 60 | - Query string attributes in the fileMeta: **query**, **qparams** and **qhash** 61 | - sourceInputFile attribute in the fileMeta 62 | - sourceValue attribute in the fileMeta 63 | 64 | ### Changed 65 | - Change default template to `[hash].[ext][query]` **(Breaking change)** 66 | 67 | ## [4.0.2] - 2016-04-22 68 | ### Fixed 69 | - issue with the ignore option 70 | 71 | ## [4.0.1] - 2016-04-21 72 | ### Removed 73 | - warning for ignore files 74 | 75 | ## [4.0.0] - 2016-04-18 76 | ### Added 77 | - Coverage support 78 | 79 | ### Changed 80 | - Best ignore behaviour. Pass the fileMeta to the ignore function. [#33](https://github.com/geut/postcss-copy/issues/33) 81 | - Change default template with `'[hash].[ext]'`. 82 | - Replace minimatch by micromatch. 83 | - Use keepachangelog format. 84 | 85 | ## [3.1.0] - 2016-02-23 86 | ### Added 87 | - Appveyor support @TrySound 88 | - Cache support wth watcher ability. Before nothing copy if dest exists @TrySound 89 | 90 | ### Changed 91 | - Refactory source code @TrySound 92 | - Refactory tests. Replace tape by ava @TrySound 93 | - Improve package.json @TrySound 94 | - Update dependencies and devDependencies @TrySound 95 | 96 | ### Fixed 97 | - Fix path resolving 98 | 99 | ## [3.0.0] - 2016-02-12 100 | ### Changed 101 | - replace keepRelativePath by relativePath custom function 102 | 103 | ## [2.6.3] - 2016-02-12 [YANKED] 104 | 105 | ## [2.6.2] - 2016-02-12 [YANKED] 106 | 107 | ## [2.6.1] - 2016-02-11 108 | ### Fixed 109 | - replace `_extend` by `Object.assign` 110 | 111 | ## [2.6.0] - 2016-02-10 [YANKED] 112 | 113 | ## [2.5.0] - 2016-02-09 114 | ### Added 115 | - minimatch support for ignore option 116 | 117 | ## [2.4.1] - 2016-02-09 [YANKED] 118 | 119 | ## [2.4.0] - 2016-02-01 120 | ### Fixed 121 | - Correct parse/replace url - issue [#4](https://github.com/geut/postcss-copy/issues/4) 122 | 123 | ## [2.3.9] - 2015-12-10 124 | ### Fixed 125 | - issue #3 ([320507e](https://github.com/geut/postcss-copy/commit/320507e)) 126 | 127 | ## [2.3.8] - 2015-11-07 [YANKED] 128 | 129 | ## [2.3.7] - 2015-11-07 130 | ### Added 131 | - add conventional changelog 132 | 133 | ## [2.3.6] - 2015-11-06 [YANKED] 134 | 135 | ## [2.3.5] - 2015-11-06 [YANKED] 136 | 137 | ## [2.3.4] - 2015-11-06 [YANKED] 138 | 139 | ## [2.3.3] - 2015-11-06 [YANKED] 140 | 141 | ## [2.3.2] - 2015-11-06 142 | ### Changed 143 | - rename transformPath to inputPath 144 | 145 | ## [2.3.1] - 2015-11-06 [YANKED] 146 | 147 | ## [2.3.0] - 2015-11-05 148 | ### Changed 149 | - Refactory source code 150 | 151 | ## [2.2.13] - 2015-10-06 [YANKED] 152 | 153 | ## [2.2.12] - 2015-10-06 154 | ### Fixed 155 | - fix error with the parse url (pathname) when the directories has empty spaces 156 | 157 | ## [2.2.11] - 2015-10-06 158 | ### Changed 159 | - Update travis 160 | - Update readme with vertical section contents and more examples 161 | 162 | ### Fixed 163 | - Minor error using postcss-copy with postcss-import 164 | 165 | ## [2.2.10] - 2015-09-18 166 | ### Changed 167 | - return early when processing data/absolute/hash urls 168 | 169 | ## [2.2.9] - 2015-09-16 170 | ### Added 171 | - new tests to check the correct multiple url parser 172 | 173 | ### Fixed 174 | - error with multiple urls in one line, e.g fonts of bootstrap 175 | 176 | ## [2.2.2] - 2015-09-14 177 | ### Changed 178 | - simple refactory in copyFile func 179 | 180 | ### Fixed 181 | - issue allow extra rules before/after url [#1](https://github.com/geut/postcss-copy/issues/1) 182 | 183 | ## [2.2.1] - 2015-09-14 [YANKED] 184 | 185 | ## [2.2.0] - 2015-09-14 186 | ### Added 187 | - new option/feature: transform and add test for it 188 | - eslint in test script 189 | 190 | ## [2.1.7] - 2015-09-13 [YANKED] 191 | 192 | ## [2.1.3] - 2015-09-07 [YANKED] 193 | 194 | ## [2.1.1] - 2015-09-07 [YANKED] 195 | 196 | ## [2.1.0] - 2015-09-07 [YANKED] 197 | 198 | ## [2.0.1] - 2015-09-07 [YANKED] 199 | 200 | ## [2.0.0] - 2015-09-07 201 | ### Changed 202 | - Switch to PostCSS Async 203 | - Remove the fs-extra dependency 204 | 205 | ## [1.1.3] - 2015-09-06 [YANKED] 206 | 207 | ## [1.1.2] - 2015-09-05 [YANKED] 208 | 209 | ## 1.1.0 - 2015-09-05 210 | - First release tagged! 211 | 212 | [unreleased]: https://github.com/geut/postcss-copy/compare/v7.1.0...HEAD 213 | [7.1.0]: https://github.com/geut/postcss-copy/compare/v7.0.0...v7.1.0 214 | [7.0.0]: https://github.com/geut/postcss-copy/compare/v6.2.1...v7.0.0 215 | [6.2.1]: https://github.com/geut/postcss-copy/compare/v6.2.0...v6.2.1 216 | [6.2.0]: https://github.com/geut/postcss-copy/compare/v6.1.0...v6.2.0 217 | [6.1.0]: https://github.com/geut/postcss-copy/compare/v6.0.0...v6.1.0 218 | [6.0.0]: https://github.com/geut/postcss-copy/compare/v5.3.0...v6.0.0 219 | [5.3.0]: https://github.com/geut/postcss-copy/compare/v5.2.0...v5.3.0 220 | [5.2.0]: https://github.com/geut/postcss-copy/compare/v5.1.0...v5.2.0 221 | [5.1.0]: https://github.com/geut/postcss-copy/compare/v5.0.1...v5.1.0 222 | [5.0.1]: https://github.com/geut/postcss-copy/compare/v5.0.0...v5.0.1 223 | [5.0.0]: https://github.com/geut/postcss-copy/compare/v4.0.2...v5.0.0 224 | [4.0.2]: https://github.com/geut/postcss-copy/compare/v4.0.1...v4.0.2 225 | [4.0.1]: https://github.com/geut/postcss-copy/compare/v4.0.0...v4.0.1 226 | [4.0.0]: https://github.com/geut/postcss-copy/compare/v3.1.0...v4.0.0 227 | [3.1.0]: https://github.com/geut/postcss-copy/compare/v3.0.0...v3.1.0 228 | [3.0.0]: https://github.com/geut/postcss-copy/compare/v2.6.3...v3.0.0 229 | [2.6.3]: https://github.com/geut/postcss-copy/compare/v2.6.2...v2.6.3 230 | [2.6.2]: https://github.com/geut/postcss-copy/compare/v2.6.1...v2.6.2 231 | [2.6.1]: https://github.com/geut/postcss-copy/compare/v2.6.0...v2.6.1 232 | [2.6.0]: https://github.com/geut/postcss-copy/compare/v2.5.0...v2.6.0 233 | [2.5.0]: https://github.com/geut/postcss-copy/compare/v2.4.1...v2.5.0 234 | [2.4.1]: https://github.com/geut/postcss-copy/compare/v2.4.0...v2.4.1 235 | [2.4.0]: https://github.com/geut/postcss-copy/compare/v2.3.9...v2.4.0 236 | [2.3.9]: https://github.com/geut/postcss-copy/compare/v2.3.8...v2.3.9 237 | [2.3.8]: https://github.com/geut/postcss-copy/compare/v2.3.7...v2.3.8 238 | [2.3.7]: https://github.com/geut/postcss-copy/compare/v2.3.6...v2.3.7 239 | [2.3.6]: https://github.com/geut/postcss-copy/compare/v2.3.5...v2.3.6 240 | [2.3.5]: https://github.com/geut/postcss-copy/compare/v2.3.4...v2.3.5 241 | [2.3.4]: https://github.com/geut/postcss-copy/compare/v2.3.3...v2.3.4 242 | [2.3.3]: https://github.com/geut/postcss-copy/compare/v2.3.2...v2.3.3 243 | [2.3.2]: https://github.com/geut/postcss-copy/compare/v2.3.1...v2.3.2 244 | [2.3.1]: https://github.com/geut/postcss-copy/compare/v2.3.0...v2.3.1 245 | [2.3.0]: https://github.com/geut/postcss-copy/compare/v2.2.13...v2.3.0 246 | [2.2.13]: https://github.com/geut/postcss-copy/compare/v2.2.12...v2.2.13 247 | [2.2.12]: https://github.com/geut/postcss-copy/compare/v2.2.11...v2.2.12 248 | [2.2.11]: https://github.com/geut/postcss-copy/compare/v2.2.10...v2.2.11 249 | [2.2.10]: https://github.com/geut/postcss-copy/compare/v2.2.9...v2.2.10 250 | [2.2.9]: https://github.com/geut/postcss-copy/compare/v2.2.2...v2.2.9 251 | [2.2.2]: https://github.com/geut/postcss-copy/compare/v2.2.1...v2.2.2 252 | [2.2.1]: https://github.com/geut/postcss-copy/compare/v2.2.0...v2.2.1 253 | [2.2.0]: https://github.com/geut/postcss-copy/compare/v2.1.7...v2.2.0 254 | [2.1.7]: https://github.com/geut/postcss-copy/compare/v2.1.3...v2.1.7 255 | [2.1.3]: https://github.com/geut/postcss-copy/compare/v2.1.1...v2.1.3 256 | [2.1.1]: https://github.com/geut/postcss-copy/compare/v2.1.0...v2.1.1 257 | [2.1.0]: https://github.com/geut/postcss-copy/compare/v2.0.1...v2.1.0 258 | [2.0.1]: https://github.com/geut/postcss-copy/compare/v2.0.0...v2.0.1 259 | [2.0.0]: https://github.com/geut/postcss-copy/compare/v1.1.3...v2.0.0 260 | [1.1.3]: https://github.com/geut/postcss-copy/compare/v1.1.2...v1.1.3 261 | [1.1.2]: https://github.com/geut/postcss-copy/compare/v1.1.0...v1.1.2 262 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to postcss-copy 2 | 3 | ## Issue Contributions 4 | 5 | When opening new issues or commenting on existing issues on this repository 6 | please make sure discussions are related to concrete technical issues with the 7 | *postcss-copy* plugin. 8 | 9 | Try to be *friendly* (we are not animals :monkey: or bad people :rage4:) and explain correctly how we can reproduce your issue. 10 | - Share the version of our plugin and PostCSS that you are using. 11 | - Share your PostCSS configuration. 12 | 13 | ## Code Contributions 14 | 15 | This document will guide you through the contribution process. 16 | 17 | ### Step 1: Fork 18 | 19 | Fork the project [on GitHub](https://github.com/geut/postcss-copy) and check out your copy locally. 20 | 21 | ```text 22 | $ git clone git@github.com:username/postcss-copy.git 23 | $ cd postcss-copy 24 | $ git remote add upstream git://github.com/geut/postcss-copy.git 25 | ``` 26 | 27 | #### Which branch? 28 | 29 | For developing new features and bug fixes, the `master` branch should be pulled 30 | and built upon. 31 | 32 | ### Step 2: Branch 33 | 34 | Create a feature branch and start hacking: 35 | 36 | ```text 37 | $ git checkout -b my-feature-branch -t origin/master 38 | ``` 39 | 40 | ### Step 3: Test 41 | 42 | Bug fixes and features **should come with tests**. We use [AVA](https://github.com/avajs/ava) to do that. 43 | 44 | ```text 45 | $ npm test 46 | ``` 47 | 48 | Make sure the linter is happy and that all tests pass. Please, do not submit 49 | patches that fail either check. 50 | 51 | ### Step 4: Commit 52 | 53 | Make sure git knows your name and email address: 54 | 55 | ```text 56 | $ git config --global user.name "J. Random User" 57 | $ git config --global user.email "j.random.user@example.com" 58 | ``` 59 | 60 | Writing good commit logs is important. A commit log should describe what 61 | changed and why. 62 | 63 | ### Step 5: Push 64 | 65 | ```text 66 | $ git push origin my-feature-branch 67 | ``` 68 | 69 | ### Step 6: Make a pull request ;) 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 GEUT 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-copy 2 | [![Build Status](https://travis-ci.org/geut/postcss-copy.svg?branch=master)](https://travis-ci.org/geut/postcss-copy) 3 | [![Build status](https://ci.appveyor.com/api/projects/status/hx0tmjv1qi0au9oy?svg=true)](https://ci.appveyor.com/project/tinchoz49/postcss-copy) 4 | [![Coverage Status](https://coveralls.io/repos/github/geut/postcss-copy/badge.svg?branch=master)](https://coveralls.io/github/geut/postcss-copy?branch=master) 5 | [![Dependency Status](https://david-dm.org/geut/postcss-copy.svg)](https://david-dm.org/geut/postcss-copy) 6 | [![devDependency Status](https://david-dm.org/geut/postcss-copy/dev-status.svg)](https://david-dm.org/geut/postcss-copy#info=devDependencies) 7 | > An **async** postcss plugin to copy all assets referenced in CSS files to a custom destination folder and updating the URLs. 8 | 9 | Sections | 10 | --- | 11 | [Install](#install) | 12 | [Quick Start](#quick-start) | 13 | [Options](#options) | 14 | [Custom Hash Function](#custom-hash-function) | 15 | [Transform](#using-transform) | 16 | [Using postcss-import](#using-postcss-import) | 17 | [About lifecyle and the fileMeta object](#lifecyle) | 18 | [Roadmap](#roadmap) | 19 | [Credits](#credits) | 20 | 21 | 22 | ## Install 23 | 24 | With [npm](https://npmjs.com/package/postcss-copy) do: 25 | 26 | ``` 27 | $ npm install postcss-copy 28 | ``` 29 | 30 | ## Quick Start 31 | 32 | ### Using [postcss-cli](https://github.com/postcss/postcss-cli) 33 | ```js 34 | // postcss.config.js 35 | module.exports = { 36 | plugins: [ 37 | require('postcss-copy')({ 38 | dest: 'dist' 39 | }) 40 | ] 41 | }; 42 | ``` 43 | ```bash 44 | $ postcss src/index.css 45 | ``` 46 | 47 | ### Using [Gulp](https://github.com/postcss/gulp-postcss) 48 | 49 | ```js 50 | var gulp = require('gulp'); 51 | var postcss = require('gulp-postcss'); 52 | var postcssCopy = require('postcss-copy'); 53 | 54 | gulp.task('buildCss', function () { 55 | var processors = [ 56 | postcssCopy({ 57 | basePath: ['src', 'otherSrc'] 58 | dest: 'dist' 59 | }) 60 | ]; 61 | 62 | return gulp 63 | .src(['src/**/*.css', 'otherSrc/**/*.css']) 64 | .pipe(postcss(processors)) 65 | .pipe(gulp.dest('dist')); 66 | }); 67 | ``` 68 | 69 | ## Options 70 | 71 | #### basePath ({string|array} default = process.cwd()) 72 | Define one/many base path for your CSS files. 73 | 74 | #### dest ({string} required) 75 | Define the dest path of your CSS files and assets. 76 | 77 | #### template ({string | function} default = '[hash].[ext][query]') 78 | Define a template name for your final url assets. 79 | * string template 80 | * **[hash]**: Let you use a hash name based on the contents of the file. 81 | * **[name]**: Real name of your asset. 82 | * **[path]**: Original relative path of your asset. 83 | * **[ext]**: Extension of the asset. 84 | * **[query]**: Query string. 85 | * **[qparams]**: Query string params without the ```?```. 86 | * **[qhash]**: Query string hash without the ```#```. 87 | * function template 88 | ```js 89 | var copyOpts = { 90 | ..., 91 | template(fileMeta) { 92 | return 'assets/custom-name-' + fileMeta.name + '.' + fileMeta.ext; 93 | } 94 | } 95 | ``` 96 | 97 | #### preservePath ({boolean} default = false) 98 | Flag option to notify to postcss-copy that your CSS files destination are going to preserve the directory structure. 99 | It's helpful if you are using `postcss-cli` with the --base option or `gulp-postcss` with multiple files (e.g: `gulp.src('src/**/*.css')`) 100 | 101 | #### ignore ({string | string[] | function} default = []) 102 | Option to ignore assets in your CSS file. 103 | 104 | ##### Using the ```!``` key in your CSS: 105 | ```css 106 | .btn { 107 | background-image: url('!images/button.jpg'); 108 | } 109 | .background { 110 | background-image: url('!images/background.jpg'); 111 | } 112 | ``` 113 | 114 | ##### Using a string or array with [micromatch](https://github.com/jonschlinkert/micromatch) support to ignore files: 115 | ```js 116 | // ignore with string 117 | var copyOpts = { 118 | ..., 119 | ignore: 'images/*.jpg' 120 | } 121 | // ignore with array 122 | var copyOpts = { 123 | ..., 124 | ignore: ['images/button.+(jpg|png)', 'images/background.jpg'] 125 | } 126 | ``` 127 | ##### Using a custom function: 128 | ```js 129 | // ignore function 130 | var copyOpts = { 131 | ..., 132 | ignore(fileMeta, opts) { 133 | return (fileMeta.filename.indexOf('images/button.jpg') || 134 | fileMeta.filename.indexOf('images/background.jpg')); 135 | } 136 | } 137 | ``` 138 | 139 | #### hashFunction 140 | Define a custom function to create the hash name. 141 | ```js 142 | var copyOpts = { 143 | ..., 144 | hashFunction(contents) { 145 | // borschik 146 | return crypto.createHash('sha1') 147 | .update(contents) 148 | .digest('base64') 149 | .replace(/\+/g, '-') 150 | .replace(/\//g, '_') 151 | .replace(/=/g, '') 152 | .replace(/^[+-]+/g, ''); 153 | } 154 | }; 155 | ``` 156 | 157 | #### transform 158 | Extend the copy method to apply a transform in the contents (e.g: optimize images). 159 | 160 | **IMPORTANT:** The function must return the fileMeta (modified) or a promise using ```resolve(fileMeta)```. 161 | ```js 162 | var Imagemin = require('imagemin'); 163 | var imageminJpegtran = require('imagemin-jpegtran'); 164 | var imageminPngquant = require('imagemin-pngquant'); 165 | 166 | var copyOpts = { 167 | ..., 168 | transform(fileMeta) { 169 | if (['jpg', 'png'].indexOf(fileMeta.ext) === -1) { 170 | return fileMeta; 171 | } 172 | return Imagemin.buffer(fileMeta.contents, { 173 | plugins: [ 174 | imageminPngquant(), 175 | imageminJpegtran({ 176 | progressive: true 177 | }) 178 | ] 179 | }) 180 | .then(result => { 181 | fileMeta.contents = result; 182 | return fileMeta; // <- important 183 | }); 184 | } 185 | }; 186 | ``` 187 | 188 | #### Using copy with postcss-import 189 | [postcss-import](https://github.com/postcss/postcss-import) is a great plugin that allow us work our css files in a modular way with the same behavior of CommonJS. 190 | 191 | ***One thing more...*** 192 | postcss-import has the ability of load files from node_modules. If you are using a custom `basePath` and you want to 193 | track your assets from `node_modules` you need to add the `node_modules` folder in the `basePath` option: 194 | 195 | ``` 196 | myProject/ 197 | |-- node_modules/ 198 | |-- dest/ 199 | |-- src/ 200 | ``` 201 | 202 | ### Full example 203 | ```js 204 | var gulp = require('gulp'); 205 | var postcss = require('gulp-postcss'); 206 | var postcssCopy = require('postcss-copy'); 207 | var postcssImport = require('postcss-import'); 208 | var path = require('path'); 209 | 210 | gulp.task('buildCss', function () { 211 | var processors = [ 212 | postcssImport(), 213 | postcssCopy({ 214 | basePath: ['src', 'node_modules'], 215 | preservePath: true, 216 | dest: 'dist' 217 | }) 218 | ]; 219 | 220 | return gulp 221 | .src('src/**/*.css') 222 | .pipe(postcss(processors, {to: 'dist/css/index.css'})) 223 | .pipe(gulp.dest('dist/css')); 224 | }); 225 | ``` 226 | 227 | #### About lifecyle and the fileMeta object 228 | The fileMeta is a literal object with meta information about the copy process. Its information grows with the progress of the copy process. 229 | 230 | The lifecyle of the copy process is: 231 | 232 | 1. Detect the url in the CSS files 233 | 2. Validate url 234 | 3. Initialize the fileMeta: 235 | 236 | ```js 237 | { 238 | sourceInputFile, // path to the origin CSS file 239 | sourceValue, // origin asset value taked from the CSS file 240 | filename, // filename normalized without query string 241 | absolutePath, // absolute path of the asset file 242 | fullName, // name of the asset file 243 | path, // relative path of the asset file 244 | name, // name without extension 245 | ext, // extension name 246 | query, // full query string 247 | qparams, // query string params without the char '?' 248 | qhash, // query string hash without the char '#' 249 | basePath // basePath found 250 | } 251 | ``` 252 | 4. Check ignore function 253 | 5. Read the asset file (using a cache buffer if exists) 254 | 6. Add ```content``` property in the fileMeta object 255 | 7. Execute custom transform 256 | 8. Create hash name based on the custom transform 257 | 9. Add ```hash``` property in the fileMeta object 258 | 10. Define template for the new asset 259 | 11. Add ```resultAbsolutePath``` and ```extra``` properties in the fileMeta object 260 | 12. Write in destination 261 | 13. Write the new URL in the PostCSS node value. 262 | 263 | ## On roadmap 264 | 265 | nothing for now :) 266 | 267 | ## Credits 268 | 269 | * Thanks to @conradz and his rework plugin [rework-assets](https://github.com/conradz/rework-assets) my inspiration in this plugin. 270 | * Thanks to @MoOx for let me create the copy function in his [postcss-url](https://github.com/postcss/postcss-url) plugin. 271 | * Thanks to @webpack, i take the idea of define templates from his awesome [file-loader](https://github.com/webpack/file-loader) 272 | * Huge thanks to @TrySound for his work in this project. 273 | 274 | ## License 275 | 276 | MIT 277 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "7" 4 | - nodejs_version: "6" 5 | - nodejs_version: "4" 6 | 7 | version: "{build}" 8 | build: off 9 | deploy: off 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version 13 | - npm cache clean 14 | - npm install 15 | 16 | test_script: 17 | - node --version 18 | - npm --version 19 | - npm test 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-copy", 3 | "version": "7.1.0", 4 | "description": "A postcss plugin to copy all assets referenced in CSS to a custom destination folder and updating the URLs.", 5 | "main": "dist/index.js", 6 | "dependencies": { 7 | "micromatch": "^3.0.3", 8 | "mkdirp": "^0.5.1", 9 | "pify": "^3.0.0", 10 | "postcss": "^6.0.3", 11 | "postcss-value-parser": "^3.3.0" 12 | }, 13 | "devDependencies": { 14 | "ava": "^0.20.0", 15 | "babel-cli": "^6.24.1", 16 | "babel-plugin-add-module-exports": "^0.2.1", 17 | "babel-plugin-transform-object-assign": "^6.22.0", 18 | "babel-preset-es2015": "^6.24.1", 19 | "babel-register": "^6.24.1", 20 | "coveralls": "^2.13.1", 21 | "del-cli": "^1.1.0", 22 | "escape-string-regexp": "^1.0.4", 23 | "eslint": "^4.1.1", 24 | "eslint-config-postcss": "^2.0.0", 25 | "eslint-config-tinchoz49": "^2.1.0", 26 | "hasha": "^3.0.0", 27 | "nyc": "^11.0.3", 28 | "path-exists": "^3.0.0" 29 | }, 30 | "files": [ 31 | "dist" 32 | ], 33 | "scripts": { 34 | "pretest": "del-cli src/__tests__/dest", 35 | "test": "ava --verbose --no-cache", 36 | "posttest": "eslint src", 37 | "nycreport": "nyc npm test", 38 | "coverage": "npm run nycreport && nyc report --reporter=text-lcov | coveralls", 39 | "build": "del-cli dist && babel src --out-dir dist --ignore __tests__", 40 | "start": "babel src --watch --source-maps --out-dir dist --ignore __tests__", 41 | "prepublish": "npm run test && npm run build", 42 | "version": "chan release ${npm_package_version} && git add ." 43 | }, 44 | "eslintConfig": { 45 | "extends": "tinchoz49" 46 | }, 47 | "babel": { 48 | "presets": [ 49 | "es2015" 50 | ], 51 | "plugins": [ 52 | "transform-object-assign", 53 | "add-module-exports" 54 | ] 55 | }, 56 | "ava": { 57 | "require": "babel-register", 58 | "files": [ 59 | "src/__tests__/*.js" 60 | ], 61 | "babel": "inherit" 62 | }, 63 | "nyc": { 64 | "exclude": [ 65 | "src/__tests__/**" 66 | ] 67 | }, 68 | "repository": { 69 | "type": "git", 70 | "url": "git+https://github.com/geut/postcss-copy.git" 71 | }, 72 | "keywords": [ 73 | "postcss", 74 | "css", 75 | "postcss-plugin", 76 | "copy", 77 | "assets" 78 | ], 79 | "author": "Geut ", 80 | "license": "MIT", 81 | "bugs": { 82 | "url": "https://github.com/geut/postcss-copy/issues" 83 | }, 84 | "homepage": "https://github.com/geut/postcss-copy#readme" 85 | } 86 | -------------------------------------------------------------------------------- /src/__tests__/check-files-with-spaces.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import randomFolder from './helpers/random-folder'; 3 | import makeRegex from './helpers/make-regex'; 4 | import path from 'path'; 5 | import { testFileExists, checkForWarnings } from './helpers'; 6 | 7 | test.beforeEach(t => { 8 | t.context.processStyle = require('./helpers/process-style'); 9 | }); 10 | 11 | test('should copy files with spaces', t => { 12 | const tempFolder = randomFolder('dest', t.title); 13 | const expected = 'images/file space.jpg'; 14 | 15 | return t.context.processStyle('src/check-files-with-spaces.css', { 16 | basePath: 'src', 17 | dest: tempFolder, 18 | template: '[path]/[name].[ext]' 19 | }) 20 | .then(result => { 21 | checkForWarnings(t, result); 22 | const css = result.css; 23 | t.regex(css, makeRegex(expected)); 24 | return testFileExists(t, path.join(tempFolder, expected)); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/__tests__/common-tests.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "template: 'assets/[hash].[ext][query]'", 3 | "opts": { 4 | "template": "assets/[hash].[ext][query]" 5 | }, 6 | "assertions": { 7 | "index": [{ 8 | "desc": "@index.css => process url image (simple)", 9 | "match": "assets/b6c8f21e92b50900.jpg" 10 | }, { 11 | "desc": "@index.css => process url image (with parameters)", 12 | "match": "assets/0ed7c955a2951f04.jpg?#iefix&v=4.4.0" 13 | }, { 14 | "desc": "@index.css => extra rules in image (simple)", 15 | "match": "url('assets/b6c8f21e92b50900.jpg') center center", 16 | "regex-simple": true 17 | }], 18 | "component": [{ 19 | "desc": "@component/index.css => process url image (simple)", 20 | "match": "assets/27da26a06634b050.jpg" 21 | }, { 22 | "desc": "@component/index.css => process url image (with parameters)", 23 | "match": "assets/0ed7c955a2951f04.jpg?#iefix&v=4.4.0" 24 | }], 25 | "external_libs": [{ 26 | "desc": "@external_libs/bootstrap.css => process url fonts ttf", 27 | "match": "assets/44bc1850f5709722.ttf" 28 | }, { 29 | "desc": "@external_libs/bootstrap.css => process url fonts eot", 30 | "match": "assets/86b6f62b7853e67d.eot" 31 | }, { 32 | "desc": "@external_libs/bootstrap.css => process url fonts eot?#iefix", 33 | "match": "assets/86b6f62b7853e67d.eot?#iefix" 34 | }, { 35 | "desc": "@external_libs/bootstrap.css => process url fonts woff", 36 | "match": "assets/278e49a86e634da6.woff" 37 | }, { 38 | "desc": "@external_libs/bootstrap.css => process url fonts woff2", 39 | "match": "assets/ca35b697d99cae4d.woff2" 40 | }], 41 | "exists": [ 42 | "assets/b6c8f21e92b50900.jpg", 43 | "assets/0ed7c955a2951f04.jpg", 44 | "assets/27da26a06634b050.jpg", 45 | "assets/44bc1850f5709722.ttf", 46 | "assets/86b6f62b7853e67d.eot", 47 | "assets/278e49a86e634da6.woff", 48 | "assets/ca35b697d99cae4d.woff2" 49 | ], 50 | "no-modified": "assets/0ed7c955a2951f04.jpg" 51 | } 52 | }, { 53 | "name": "template: '[path]/[hash].[ext][query]'", 54 | "opts": { 55 | "template": "[path]/[hash].[ext][query]" 56 | }, 57 | "assertions": { 58 | "index": [{ 59 | "desc": "@index.css => process url image (simple)", 60 | "match": "images/b6c8f21e92b50900.jpg" 61 | }, { 62 | "desc": "@index.css => process url image (with parameters)", 63 | "match": "images/0ed7c955a2951f04.jpg?#iefix&v=4.4.0" 64 | }, { 65 | "desc": "@index.css => extra rules in image (simple)", 66 | "match": "url('images/b6c8f21e92b50900.jpg') center center", 67 | "regex-simple": true 68 | }], 69 | "component": [{ 70 | "desc": "@component/index.css => process url image (simple)", 71 | "match": "component/images/27da26a06634b050.jpg" 72 | }, { 73 | "desc": "@component/index.css => process url image (with parameters)", 74 | "match": "images/0ed7c955a2951f04.jpg?#iefix&v=4.4.0" 75 | }], 76 | "external_libs": [{ 77 | "desc": "@external_libs/bootstrap.css => process url fonts ttf", 78 | "match": "bootstrap/fonts/44bc1850f5709722.ttf" 79 | }, { 80 | "desc": "@external_libs/bootstrap.css => process url fonts eot", 81 | "match": "bootstrap/fonts/86b6f62b7853e67d.eot" 82 | }, { 83 | "desc": "@external_libs/bootstrap.css => process url fonts eot?#iefix", 84 | "match": "bootstrap/fonts/86b6f62b7853e67d.eot?#iefix" 85 | }, { 86 | "desc": "@external_libs/bootstrap.css => process url fonts woff", 87 | "match": "bootstrap/fonts/278e49a86e634da6.woff" 88 | }, { 89 | "desc": "@external_libs/bootstrap.css => process url fonts woff2", 90 | "match": "bootstrap/fonts/ca35b697d99cae4d.woff2" 91 | }], 92 | "exists": [ 93 | "images/b6c8f21e92b50900.jpg", 94 | "images/0ed7c955a2951f04.jpg", 95 | "component/images/27da26a06634b050.jpg", 96 | "bootstrap/fonts/44bc1850f5709722.ttf", 97 | "bootstrap/fonts/86b6f62b7853e67d.eot", 98 | "bootstrap/fonts/278e49a86e634da6.woff", 99 | "bootstrap/fonts/ca35b697d99cae4d.woff2" 100 | ], 101 | "no-modified": "images/0ed7c955a2951f04.jpg" 102 | } 103 | }, { 104 | "name": "template: '[path]/[name].[ext][query]'", 105 | "opts": { 106 | "template": "[path]/[name].[ext][query]" 107 | }, 108 | "assertions": { 109 | "index": [{ 110 | "desc": "@index.css => process url image (simple)", 111 | "match": "images/other.jpg" 112 | }, { 113 | "desc": "@index.css => process url image (with parameters)", 114 | "match": "images/test.jpg?#iefix&v=4.4.0" 115 | }, { 116 | "desc": "@index.css => extra rules in image (simple)", 117 | "match": "url('images/other.jpg') center center", 118 | "regex-simple": true 119 | }], 120 | "component": [{ 121 | "desc": "@component/index.css => process url image (simple)", 122 | "match": "component/images/component.jpg" 123 | }, { 124 | "desc": "@component/index.css => process url image (with parameters)", 125 | "match": "images/test.jpg?#iefix&v=4.4.0" 126 | }], 127 | "external_libs": [{ 128 | "desc": "@external_libs/bootstrap.css => process url fonts ttf", 129 | "match": "bootstrap/fonts/glyphicons-halflings-regular.ttf" 130 | }, { 131 | "desc": "@external_libs/bootstrap.css => process url fonts eot", 132 | "match": "bootstrap/fonts/glyphicons-halflings-regular.eot" 133 | }, { 134 | "desc": "@external_libs/bootstrap.css => process url fonts eot?#iefix", 135 | "match": "bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix" 136 | }, { 137 | "desc": "@external_libs/bootstrap.css => process url fonts woff", 138 | "match": "bootstrap/fonts/glyphicons-halflings-regular.woff" 139 | }, { 140 | "desc": "@external_libs/bootstrap.css => process url fonts woff2", 141 | "match": "bootstrap/fonts/glyphicons-halflings-regular.woff2" 142 | }], 143 | "exists": [ 144 | "images/other.jpg", 145 | "images/test.jpg", 146 | "component/images/component.jpg", 147 | "bootstrap/fonts/glyphicons-halflings-regular.ttf", 148 | "bootstrap/fonts/glyphicons-halflings-regular.eot", 149 | "bootstrap/fonts/glyphicons-halflings-regular.woff", 150 | "bootstrap/fonts/glyphicons-halflings-regular.woff2" 151 | ], 152 | "no-modified": "images/test.jpg" 153 | } 154 | }, { 155 | "name": "template: '[hash].[ext][query]', hashFunction: {custom}", 156 | "opts": { 157 | "hashFunction": "custom" 158 | }, 159 | "assertions": { 160 | "index": [{ 161 | "desc": "@index.css => process url image (simple)", 162 | "match": "tsjyHpK1CQAzLrWA3hok0f01nks.jpg" 163 | }, { 164 | "desc": "@index.css => process url image (with parameters)", 165 | "match": "DtfJVaKVHwRz_PkVXOweAq13S0o.jpg?#iefix&v=4.4.0" 166 | }, { 167 | "desc": "@index.css => extra rules in image (simple)", 168 | "match": "url('tsjyHpK1CQAzLrWA3hok0f01nks.jpg') center center", 169 | "regex-simple": true 170 | }], 171 | "component": [{ 172 | "desc": "@component/index.css => process url image (simple)", 173 | "match": "J9omoGY0sFB4U5nyxLRB6t3Ms7w.jpg" 174 | }, { 175 | "desc": "@component/index.css => process url image (with parameters)", 176 | "match": "DtfJVaKVHwRz_PkVXOweAq13S0o.jpg?#iefix&v=4.4.0" 177 | }], 178 | "external_libs": [{ 179 | "desc": "@external_libs/bootstrap.css => process url fonts ttf", 180 | "match": "RLwYUPVwlyJnsWmuGPHLBrYR_6I.ttf" 181 | }, { 182 | "desc": "@external_libs/bootstrap.css => process url fonts eot", 183 | "match": "hrb2K3hT5n0-Y19lEqWl78WOo8M.eot" 184 | }, { 185 | "desc": "@external_libs/bootstrap.css => process url fonts eot?#iefix", 186 | "match": "hrb2K3hT5n0-Y19lEqWl78WOo8M.eot?#iefix" 187 | }, { 188 | "desc": "@external_libs/bootstrap.css => process url fonts woff", 189 | "match": "J45JqG5jTabyoC87R92dKo8mIQ8.woff" 190 | }, { 191 | "desc": "@external_libs/bootstrap.css => process url fonts woff2", 192 | "match": "yjW2l9mcrk0bYPLWD803dxmH6wc.woff2" 193 | }], 194 | "exists": [ 195 | "tsjyHpK1CQAzLrWA3hok0f01nks.jpg", 196 | "DtfJVaKVHwRz_PkVXOweAq13S0o.jpg", 197 | "J9omoGY0sFB4U5nyxLRB6t3Ms7w.jpg", 198 | "RLwYUPVwlyJnsWmuGPHLBrYR_6I.ttf", 199 | "hrb2K3hT5n0-Y19lEqWl78WOo8M.eot", 200 | "J45JqG5jTabyoC87R92dKo8mIQ8.woff", 201 | "yjW2l9mcrk0bYPLWD803dxmH6wc.woff2" 202 | ], 203 | "no-modified": "DtfJVaKVHwRz_PkVXOweAq13S0o.jpg" 204 | } 205 | }, { 206 | "name": "template: 'assets/[hash].[ext]?custom=1&[qparams]#[qhash]'", 207 | "opts": { 208 | "template": "assets/[hash].[ext]?custom=1&[qparams]#[qhash]" 209 | }, 210 | "assertions": { 211 | "index": [{ 212 | "desc": "@index.css => process url image (simple)", 213 | "match": "assets/b6c8f21e92b50900.jpg?custom=1&#" 214 | }, { 215 | "desc": "@index.css => process url image (with parameters)", 216 | "match": "assets/0ed7c955a2951f04.jpg?custom=1&#iefix&v=4.4.0" 217 | }, { 218 | "desc": "@index.css => extra rules in image (simple)", 219 | "match": "url('assets/b6c8f21e92b50900.jpg?custom=1&#') center center", 220 | "regex-simple": true 221 | }], 222 | "component": [], 223 | "external_libs": [], 224 | "exists": [ 225 | "assets/b6c8f21e92b50900.jpg", 226 | "assets/0ed7c955a2951f04.jpg" 227 | ], 228 | "no-modified": "assets/0ed7c955a2951f04.jpg" 229 | } 230 | }] 231 | -------------------------------------------------------------------------------- /src/__tests__/copy.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import randomFolder from './helpers/random-folder'; 5 | import pify from 'pify'; 6 | 7 | test.beforeEach(t => { 8 | t.context.copy = require('../lib/copy'); 9 | t.context.cwd = 'src/__tests__'; 10 | }); 11 | 12 | test('should copy file', t => { 13 | const tempFolder = randomFolder('dest', t.title); 14 | const srcFile = `${t.context.cwd}/src/images/test.jpg`; 15 | const destFile = path.join(tempFolder, 'test.jpg'); 16 | 17 | return t.context.copy(srcFile, destFile).then(() => { 18 | const srcBuffer = fs.readFileSync(srcFile); 19 | const destBuffer = fs.readFileSync(destFile); 20 | const compared = Buffer.compare(srcBuffer, destBuffer); 21 | t.is(compared, 0); 22 | }); 23 | }); 24 | 25 | test('should copy file with dynamical dest', t => { 26 | const tempFolder = randomFolder('dest', t.title); 27 | const srcFile = `${t.context.cwd}/src/images/test.jpg`; 28 | const destFile = path.join(tempFolder, 'test.jpg'); 29 | 30 | return t.context.copy(srcFile, () => destFile).then(() => { 31 | const srcBuffer = fs.readFileSync(srcFile); 32 | const destBuffer = fs.readFileSync(destFile); 33 | const compared = Buffer.compare(srcBuffer, destBuffer); 34 | t.is(compared, 0); 35 | }); 36 | }); 37 | 38 | test('should copy and transform file', t => { 39 | const tempFolder = randomFolder('dest', t.title); 40 | const srcFile = `${t.context.cwd}/src/images/test.jpg`; 41 | const destFile = path.join(tempFolder, 'test.jpg'); 42 | const srcBuffer = fs.readFileSync(srcFile); 43 | const customBuffer = new Buffer([1, 2, 3, 4, 5]); 44 | 45 | return t.context.copy(srcFile, destFile, contents => { 46 | const compared = Buffer.compare(srcBuffer, contents); 47 | t.is(compared, 0); 48 | return customBuffer; 49 | }) 50 | .then(() => { 51 | const destBuffer = fs.readFileSync(destFile); 52 | const compared = Buffer.compare(destBuffer, customBuffer); 53 | t.is(compared, 0); 54 | }); 55 | }); 56 | 57 | test('should copy file once', t => { 58 | const tempFolder = randomFolder('dest', t.title); 59 | const srcFile = `${t.context.cwd}/src/images/test.jpg`; 60 | const destFile = path.join(tempFolder, 'test.jpg'); 61 | let prevTime; 62 | 63 | return t.context.copy(srcFile, destFile).then(() => { 64 | prevTime = fs.statSync(destFile).mtime.getTime(); 65 | return t.context.copy(srcFile, destFile); 66 | }) 67 | .then(() => { 68 | t.is(prevTime, fs.statSync(destFile).mtime.getTime()); 69 | }); 70 | }); 71 | 72 | test('should copy file if source was not modified ' + 73 | 'but the file is missing in the destination', t => { 74 | const tempFolder = randomFolder('dest', t.title); 75 | const srcFile = `${t.context.cwd}/src/images/test.jpg`; 76 | const destFile = path.join(tempFolder, 'test.jpg'); 77 | 78 | const process = t.context.copy(srcFile, destFile) 79 | .then(() => pify(fs.unlink)(destFile)) 80 | .then(() => t.context.copy(srcFile, destFile)) 81 | .then(() => pify(fs.stat)(destFile)); 82 | 83 | return t.notThrows(process); 84 | }); 85 | 86 | test('should copy again if source was modified', t => { 87 | const tempFolder = randomFolder('dest', t.title); 88 | const srcFile = `${t.context.cwd}/src/images/test-modified.jpg`; 89 | const destFile = path.join(tempFolder, 'test.jpg'); 90 | const srcBuffer = fs.readFileSync(srcFile); 91 | const modifiedBuffer = new Buffer([srcBuffer, srcBuffer]); 92 | 93 | return t.context.copy(srcFile, destFile).then(() => { 94 | fs.writeFileSync(srcFile, modifiedBuffer); 95 | return t.context.copy(srcFile, destFile); 96 | }) 97 | .then(() => { 98 | fs.writeFileSync(srcFile, srcBuffer); 99 | const destBuffer = fs.readFileSync(destFile); 100 | const compared = Buffer.compare(modifiedBuffer, destBuffer); 101 | t.is(compared, 0); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/__tests__/external_libs/bootstrap/css/bootstrap.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Glyphicons Halflings'; 3 | 4 | src: url('../fonts/glyphicons-halflings-regular.eot'); 5 | src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); 6 | } 7 | -------------------------------------------------------------------------------- /src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/external_libs/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/__tests__/helpers/index.js: -------------------------------------------------------------------------------- 1 | import pathExists from 'path-exists'; 2 | 3 | export function testFileExists(t, file) { 4 | return pathExists(file).then(exists => { 5 | t.truthy(exists, `File "${file}" created.`); 6 | }); 7 | } 8 | 9 | export function checkForWarnings(t, result) { 10 | const warnings = result.warnings(); 11 | 12 | t.is( 13 | warnings.length, 14 | 0, 15 | [ 16 | 'Should not had postcss warnings', 17 | ...warnings.map(w => w.text) 18 | ] 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/helpers/make-regex.js: -------------------------------------------------------------------------------- 1 | import escapeStringRegexp from 'escape-string-regexp'; 2 | 3 | export default function makeRegex(str, simple = false) { 4 | let value; 5 | if (simple) { 6 | value = str; 7 | } else { 8 | value = '(\'' + str + '\')'; 9 | } 10 | return new RegExp(escapeStringRegexp(value)); 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/helpers/process-style.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import postcss from 'postcss'; 3 | import copy from '../../index.js'; 4 | 5 | export default function processStyle(filename, opts, to) { 6 | return new Promise((resolve, reject) => { 7 | fs.readFile(`src/__tests__/${filename}`, 'utf8', (err, file) => { 8 | if (err) { 9 | reject(err); 10 | } else { 11 | resolve(file); 12 | } 13 | }); 14 | }) 15 | .then(file => { 16 | const postcssOpts = { 17 | from: `src/__tests__/${filename}`, 18 | to 19 | }; 20 | if (opts.basePath) { 21 | const { basePath } = opts; 22 | if (typeof opts.basePath === 'string') { 23 | opts.basePath = basePath.indexOf('src/__tests__') === -1 ? 24 | `src/__tests__/${basePath}` : 25 | basePath; 26 | } else { 27 | opts.basePath = basePath 28 | .map(bPath => { 29 | return bPath.indexOf('src/__tests__') === -1 ? 30 | `src/__tests__/${bPath}` : 31 | bPath; 32 | }); 33 | } 34 | } 35 | return postcss([ 36 | copy(opts) 37 | ]).process(file.trim(), postcssOpts); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/__tests__/helpers/random-folder.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import hasha from 'hasha'; 3 | 4 | export default function randomFolder(pathname, title) { 5 | return path.join('src/__tests__', 6 | pathname, 7 | hasha(title, { algorithm: 'md5' }) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/helpers/read-file.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export default function readFile(filename) { 4 | return new Promise((resolve, reject) => { 5 | fs.readFile(filename, 'utf8', (err, file) => { 6 | if (err) { 7 | reject(err); 8 | } else { 9 | resolve(file); 10 | } 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/__tests__/ignore.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import randomFolder from './helpers/random-folder'; 3 | import makeRegex from './helpers/make-regex'; 4 | 5 | test.beforeEach(t => { 6 | t.context.processStyle = require('./helpers/process-style'); 7 | }); 8 | 9 | test('should keep working if the ignore option is invalid', t => { 10 | return t.context.processStyle('src/ignore.css', { 11 | basePath: 'src', 12 | dest: randomFolder('dest', t.title), 13 | template: 'invalid-ignore-option/[path]/[name].[ext][query]', 14 | ignore: 4 15 | }) 16 | .then(result => { 17 | const css = result.css; 18 | t.regex(css, makeRegex('images/test.jpg?#iefix&v=4.4.0')); 19 | t.regex(css, makeRegex('invalid-ignore-option/images/other.jpg')); 20 | t.regex(css, 21 | makeRegex('invalid-ignore-option/images/noignore.jpg')); 22 | }); 23 | }); 24 | 25 | test('should ignore files with string expression', t => { 26 | return t.context.processStyle('src/ignore.css', { 27 | basePath: 'src', 28 | dest: randomFolder('dest', t.title), 29 | template: 'ignore-path-array/[path]/[name].[ext][query]', 30 | ignore: 'images/other.+(jpg|png)' 31 | }) 32 | .then(result => { 33 | const css = result.css; 34 | t.regex(css, makeRegex('images/test.jpg?#iefix&v=4.4.0')); 35 | t.regex(css, makeRegex('images/other.jpg')); 36 | t.regex(css, makeRegex('ignore-path-array/images/noignore.jpg')); 37 | }); 38 | }); 39 | 40 | test('should ignore files with array of paths', t => { 41 | return t.context.processStyle('src/ignore.css', { 42 | basePath: 'src', 43 | dest: randomFolder('dest', t.title), 44 | template: 'ignore-path-array/[path]/[name].[ext][query]', 45 | ignore: ['images/other.jpg'] 46 | }) 47 | .then(result => { 48 | const css = result.css; 49 | t.regex(css, makeRegex('images/test.jpg?#iefix&v=4.4.0')); 50 | t.regex(css, makeRegex('images/other.jpg')); 51 | t.regex(css, makeRegex('ignore-path-array/images/noignore.jpg')); 52 | }); 53 | }); 54 | 55 | test('should ignore files with custom function', t => { 56 | return t.context.processStyle('src/ignore.css', { 57 | basePath: 'src', 58 | dest: randomFolder('dest', t.title), 59 | template: 'ignore-path-func/[path]/[name].[ext][query]', 60 | ignore(fileMeta) { 61 | return fileMeta.filename === 'images/other.jpg'; 62 | } 63 | }) 64 | .then((result) => { 65 | const css = result.css; 66 | t.regex(css, makeRegex('images/test.jpg?#iefix&v=4.4.0')); 67 | t.regex(css, makeRegex('images/other.jpg')); 68 | t.regex(css, makeRegex('ignore-path-func/images/noignore.jpg')); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import crypto from 'crypto'; 5 | import randomFolder from './helpers/random-folder'; 6 | import makeRegex from './helpers/make-regex'; 7 | import commonTests from './common-tests.json'; 8 | import { testFileExists, checkForWarnings } from './helpers'; 9 | 10 | test.beforeEach(t => { 11 | t.context.processStyle = require('./helpers/process-style'); 12 | }); 13 | 14 | commonTests.forEach(item => { 15 | if (item.opts.hashFunction === 'custom') { 16 | item.opts.hashFunction = contents => { 17 | // borschik 18 | return crypto 19 | .createHash('sha1') 20 | .update(contents) 21 | .digest('base64') 22 | .replace(/\+/g, '-') 23 | .replace(/\//g, '_') 24 | .replace(/\=/g, '') 25 | .replace(/^[+-]+/g, ''); 26 | }; 27 | } 28 | 29 | test(item.name, t => { 30 | const tempFolder = randomFolder('dest', t.title); 31 | const copyOpts = Object.assign( 32 | { 33 | basePath: item.opts.basePath || 'src', 34 | dest: tempFolder 35 | }, 36 | item.opts 37 | ); 38 | 39 | let oldTime; 40 | let newTime; 41 | 42 | if (item.opts.to) { 43 | item.opts.to = path.join(tempFolder, item.opts.to); 44 | } 45 | 46 | return t.context 47 | .processStyle('src/index.css', copyOpts, item.opts.to) 48 | .then(result => { 49 | checkForWarnings(t, result); 50 | 51 | const css = result.css; 52 | item.assertions.index.forEach(assertion => { 53 | t.regex( 54 | css, 55 | makeRegex(assertion.match, assertion['regex-simple']), 56 | assertion.desc 57 | ); 58 | }); 59 | 60 | oldTime = fs 61 | .statSync( 62 | path.join( 63 | tempFolder, 64 | item.assertions['no-modified'] 65 | ) 66 | ) 67 | .mtime.getTime(); 68 | 69 | copyOpts.basePath = ['src', 'external_libs']; 70 | 71 | return t.context.processStyle( 72 | 'src/component/index.css', 73 | copyOpts, 74 | item.opts.to 75 | ); 76 | }) 77 | .then(result => { 78 | checkForWarnings(t, result); 79 | 80 | const css = result.css; 81 | item.assertions.component.forEach(assertion => { 82 | t.regex( 83 | css, 84 | makeRegex(assertion.match, assertion['regex-simple']), 85 | assertion.desc 86 | ); 87 | }); 88 | 89 | newTime = fs 90 | .statSync( 91 | path.join( 92 | tempFolder, 93 | item.assertions['no-modified'] 94 | ) 95 | ) 96 | .mtime.getTime(); 97 | 98 | t.is( 99 | oldTime, 100 | newTime, 101 | `${item.assertions['no-modified']} was not modified.` 102 | ); 103 | 104 | if (item.opts.to === 'equal-from') { 105 | item.opts.to = 'src/component/index.css'; 106 | } 107 | 108 | return t.context.processStyle( 109 | 'external_libs/bootstrap/css/bootstrap.css', 110 | copyOpts, 111 | item.opts.to 112 | ); 113 | }) 114 | .then(result => { 115 | checkForWarnings(t, result); 116 | 117 | const css = result.css; 118 | item.assertions.external_libs.forEach(assertion => { 119 | t.regex( 120 | css, 121 | makeRegex(assertion.match, assertion['regex-simple']), 122 | assertion.desc 123 | ); 124 | }); 125 | 126 | newTime = fs 127 | .statSync( 128 | path.join( 129 | tempFolder, 130 | item.assertions['no-modified'] 131 | ) 132 | ) 133 | .mtime.getTime(); 134 | 135 | t.is( 136 | oldTime, 137 | newTime, 138 | `${item.assertions['no-modified']} was not modified.` 139 | ); 140 | 141 | return Promise.all( 142 | item.assertions.exists.map(file => { 143 | return testFileExists(t, path.join(tempFolder, file)); 144 | }) 145 | ); 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /src/__tests__/options.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import randomFolder from './helpers/random-folder'; 3 | import makeRegex from './helpers/make-regex.js'; 4 | 5 | test.beforeEach(t => { 6 | t.context.processStyle = require('./helpers/process-style'); 7 | }); 8 | 9 | test('should set basePath to process.cwd() as default', t => { 10 | return t.context 11 | .processStyle('src/index.css', { 12 | dest: randomFolder('dest', t.title), 13 | template: 'assets/[path]/[name].[ext]' 14 | }) 15 | .then(result => { 16 | t.regex( 17 | result.css, 18 | makeRegex('assets/src/__tests__/src/images/test.jpg') 19 | ); 20 | }); 21 | }); 22 | 23 | test('should throw an error if the "dest" option is not set', t => { 24 | return t.context 25 | .processStyle('src/index.css', { 26 | basePath: 'src' 27 | }) 28 | .then(() => t.fail()) 29 | .catch(err => { 30 | t.is(err.message, 'Option `dest` is required in postcss-copy'); 31 | }); 32 | }); 33 | 34 | test('should warn if the filename not belongs to the "basePath" option', t => { 35 | return t.context 36 | .processStyle('external_libs/bootstrap/css/bootstrap.css', { 37 | basePath: 'src', 38 | dest: randomFolder('dest', t.title) 39 | }) 40 | .then(result => { 41 | const warnings = result.warnings(); 42 | t.is(warnings.length, 6); 43 | warnings.forEach(warning => { 44 | t.is(warning.text.indexOf('"basePath" not found in '), 0); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/__tests__/src/check-files-with-spaces.css: -------------------------------------------------------------------------------- 1 | span { 2 | background: url('images/file space.jpg'); 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/src/check-transform-hash.css: -------------------------------------------------------------------------------- 1 | span { 2 | background: url('images/superman.jpg'); 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/src/check-transform.css: -------------------------------------------------------------------------------- 1 | span { 2 | background: url('images/bigimage.jpg'); 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/src/component/images/component.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/component/images/component.jpg -------------------------------------------------------------------------------- /src/__tests__/src/component/index.css: -------------------------------------------------------------------------------- 1 | .check-relative { 2 | background: url('images/component.jpg'); 3 | } 4 | 5 | .check-parent-relative { 6 | background: url('../images/test.jpg?#iefix&v=4.4.0'); 7 | } 8 | -------------------------------------------------------------------------------- /src/__tests__/src/correct-parse-url.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(images); 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url('fonts/MaterialIcons-Regular.woff2') format('woff2'), 9 | url('fonts/MaterialIcons-Regular.woff') format('woff'); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/src/fonts/samefile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/fonts/samefile.woff -------------------------------------------------------------------------------- /src/__tests__/src/fonts/samefile.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/fonts/samefile.woff2 -------------------------------------------------------------------------------- /src/__tests__/src/ignore.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url('!images/test.jpg?#iefix&v=4.4.0'); 3 | } 4 | 5 | div { 6 | background: url('images/other.jpg'); 7 | } 8 | 9 | span { 10 | background: url('images/noignore.jpg'); 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/src/images/batman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/batman.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/bigimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/bigimage.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/file space.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/file space.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/noignore.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/noignore.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/other.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/other.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/superman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/superman.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/test-modified.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/test-modified.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/test-unmodified.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/test-unmodified.jpg -------------------------------------------------------------------------------- /src/__tests__/src/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geut/postcss-copy/50f6f086336632c22f5db89143cc07b61f34093d/src/__tests__/src/images/test.jpg -------------------------------------------------------------------------------- /src/__tests__/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url('images/test.jpg?#iefix&v=4.4.0'); 3 | } 4 | 5 | div { 6 | background: url('images/other.jpg'); 7 | } 8 | 9 | span { 10 | background: url('images/other.jpg') center center; 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/src/invalid.css: -------------------------------------------------------------------------------- 1 | div { 2 | background: url('images/other.jpg'); 3 | } 4 | 5 | span { 6 | background: url('data:image/gif;base64,R0lGOD'); 7 | } 8 | -------------------------------------------------------------------------------- /src/__tests__/src/no-repeat-transform.css: -------------------------------------------------------------------------------- 1 | span { 2 | background: url('images/batman.jpg'); 3 | } 4 | 5 | div { 6 | background: url('images/batman.jpg'); 7 | } 8 | -------------------------------------------------------------------------------- /src/__tests__/src/not-found.css: -------------------------------------------------------------------------------- 1 | div { 2 | background: url('images/other.jpg'); 3 | } 4 | 5 | span { 6 | background: url('images/image-not-found.jpg'); 7 | } 8 | -------------------------------------------------------------------------------- /src/__tests__/template.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import randomFolder from './helpers/random-folder'; 3 | import makeRegex from './helpers/make-regex'; 4 | import { sync as exists } from 'path-exists'; 5 | import { join } from 'path'; 6 | 7 | test.beforeEach(t => { 8 | t.context.processStyle = require('./helpers/process-style'); 9 | }); 10 | 11 | test('should rename files via default template', t => { 12 | const tempFolder = randomFolder('dest', t.title); 13 | return t.context.processStyle('src/index.css', { 14 | basePath: 'src', 15 | dest: tempFolder 16 | }) 17 | .then(result => { 18 | const css = result.css; 19 | 20 | t.is(result.warnings().length, 0); 21 | 22 | t.regex(css, makeRegex('0ed7c955a2951f04.jpg?#iefix&v=4.4.0')); 23 | t.truthy(exists(join(tempFolder, '0ed7c955a2951f04.jpg'))); 24 | 25 | t.regex(css, makeRegex('b6c8f21e92b50900.jpg')); 26 | t.truthy(exists(join(tempFolder, 'b6c8f21e92b50900.jpg'))); 27 | }); 28 | }); 29 | 30 | test('should rename files via custom template', t => { 31 | const tempFolder = randomFolder('dest', t.title); 32 | return t.context.processStyle('src/index.css', { 33 | basePath: 'src', 34 | dest: tempFolder, 35 | template: '[path]/[name]-[hash].[ext][query]' 36 | }) 37 | .then(result => { 38 | const css = result.css; 39 | 40 | t.is(result.warnings().length, 0); 41 | 42 | t.regex(css, makeRegex( 43 | 'images/test-0ed7c955a2951f04.jpg?#iefix&v=4.4.0' 44 | )); 45 | t.truthy( 46 | exists(join(tempFolder, 'images/test-0ed7c955a2951f04.jpg')) 47 | ); 48 | 49 | t.regex(css, makeRegex('images/other-b6c8f21e92b50900.jpg')); 50 | t.truthy( 51 | exists(join(tempFolder, 'images/other-b6c8f21e92b50900.jpg')) 52 | ); 53 | }); 54 | }); 55 | 56 | test('should rename files via template function', t => { 57 | const tempFolder = randomFolder('dest', t.title); 58 | return t.context.processStyle('src/index.css', { 59 | basePath: 'src', 60 | dest: tempFolder, 61 | template(fileMeta) { 62 | return `custom-path/custom-name-${fileMeta.name}.` + 63 | `${fileMeta.ext}${fileMeta.query}`; 64 | } 65 | }) 66 | .then(result => { 67 | const css = result.css; 68 | 69 | t.is(result.warnings().length, 0); 70 | 71 | t.regex(css, makeRegex( 72 | 'custom-path/custom-name-test.jpg?#iefix&v=4.4.0' 73 | )); 74 | t.truthy( 75 | exists(join(tempFolder, 'custom-path/custom-name-test.jpg')) 76 | ); 77 | 78 | t.regex(css, makeRegex('custom-path/custom-name-other.jpg')); 79 | t.truthy( 80 | exists(join(tempFolder, 'custom-path/custom-name-other.jpg')) 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/__tests__/tools-path.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { defineCSSDestPath } from '../lib/tools-path'; 3 | import path from 'path'; 4 | 5 | test(`defineCSSDestPath should return the 'opts.dest' dirname 6 | if ('to' === undefined) && preservePath === false`, t => { 7 | const result = defineCSSDestPath( 8 | 'src', 9 | 'src', 10 | { 11 | opts: { 12 | from: 'src/index.css' 13 | } 14 | }, 15 | { 16 | dest: 'dest' 17 | } 18 | ); 19 | 20 | t.is(result, 'dest'); 21 | }); 22 | 23 | test(`defineCSSDestPath should return the 'opts.dest' 24 | if (to === from) && preservePath == false`, t => { 25 | const result = defineCSSDestPath( 26 | 'src', 27 | 'src', 28 | { 29 | opts: { 30 | from: 'src/index.css', 31 | to: 'src/index.css' 32 | } 33 | }, 34 | { 35 | dest: 'dest' 36 | } 37 | ); 38 | 39 | t.is(result, 'dest'); 40 | }); 41 | 42 | 43 | test(`defineCSSDestPath should return the 'to' 44 | if (to !== undefined && to !== from) && preservePath == false`, t => { 45 | const result = defineCSSDestPath( 46 | 'src', 47 | 'src', 48 | { 49 | opts: { 50 | from: 'src/index.css', 51 | to: 'output/index.css' 52 | } 53 | }, 54 | { 55 | dest: 'dest' 56 | } 57 | ); 58 | 59 | t.is(result, 'output'); 60 | }); 61 | 62 | test(`defineCSSDestPath should preserve the structure directories 63 | if preservePath == true && !postcss-import`, t => { 64 | const result = defineCSSDestPath( 65 | path.resolve('src'), 66 | 'src', 67 | { 68 | opts: { 69 | from: 'src/index.css' 70 | } 71 | }, 72 | { 73 | preservePath: true, 74 | dest: 'dest' 75 | } 76 | ); 77 | 78 | t.is(result, 'dest'); 79 | }); 80 | 81 | test(`defineCSSDestPath should preserve the structure directories 82 | if preservePath == true && postcss-import`, t => { 83 | const result = defineCSSDestPath( 84 | 'different-path/index.css', 85 | 'src', 86 | { 87 | opts: { 88 | from: 'src/index.css' 89 | } 90 | }, 91 | { 92 | basePath: [path.resolve('src')], 93 | preservePath: true, 94 | dest: 'dest' 95 | } 96 | ); 97 | 98 | t.is(result, 'dest'); 99 | }); 100 | -------------------------------------------------------------------------------- /src/__tests__/transform.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import randomFolder from './helpers/random-folder'; 5 | import pify from 'pify'; 6 | 7 | test.beforeEach(t => { 8 | t.context.processStyle = require('./helpers/process-style'); 9 | }); 10 | 11 | const stat = pify(fs.stat); 12 | 13 | function transform(fileMeta) { 14 | if (['jpg', 'png'].indexOf(fileMeta.ext) === -1) { 15 | return fileMeta; 16 | } 17 | 18 | fileMeta.contents = new Buffer(''); 19 | 20 | return fileMeta; 21 | } 22 | 23 | test('should process assets via transform', t => { 24 | const tempFolder = randomFolder('dest', t.title); 25 | return t.context.processStyle('src/check-transform.css', { 26 | basePath: 'src', 27 | dest: tempFolder, 28 | template: '[path]/[name].[ext]', 29 | transform 30 | }) 31 | .then(() => { 32 | const oldSize = fs 33 | .statSync('src/__tests__/src/images/bigimage.jpg') 34 | .size; 35 | 36 | const newSize = fs 37 | .statSync(path.join(tempFolder, 'images', 'bigimage.jpg')) 38 | .size; 39 | 40 | t.truthy(newSize < oldSize); 41 | }); 42 | }); 43 | 44 | test('should process assets via transform and use ' + 45 | 'the hash property based on the transform content', t => { 46 | const tempFolder = randomFolder('dest', t.title); 47 | const process = t.context.processStyle('src/check-transform-hash.css', { 48 | basePath: 'src', 49 | dest: tempFolder, 50 | template: '[path]/[hash].[ext]', 51 | transform 52 | }) 53 | .then(() => { 54 | return stat( 55 | path.join(tempFolder, 'images', 'da39a3ee5e6b4b0d.jpg') 56 | ); 57 | }); 58 | 59 | return t.notThrows(process); 60 | }); 61 | 62 | test('should not transform when the source is the same', (t) => { 63 | const tempFolder = randomFolder('dest', t.title); 64 | let times = 0; 65 | 66 | return t.context.processStyle('src/no-repeat-transform.css', { 67 | basePath: 'src', 68 | dest: tempFolder, 69 | template: '[path]/[name].[ext]', 70 | transform(fileMeta) { 71 | times++; 72 | return transform(fileMeta); 73 | } 74 | }) 75 | .then(() => { 76 | t.truthy(times === 1); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/validate-url.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import path from 'path'; 3 | import randomFolder from './helpers/random-folder'; 4 | import makeRegex from './helpers/make-regex'; 5 | 6 | test.beforeEach(t => { 7 | t.context.processStyle = require('./helpers/process-style'); 8 | }); 9 | 10 | test('should correctly parse url', t => { 11 | return t.context.processStyle('src/correct-parse-url.css', { 12 | basePath: 'src', 13 | dest: randomFolder('dest', t.title), 14 | template: '[path]/[name].[ext]' 15 | }) 16 | .then(result => { 17 | const css = result.css; 18 | t.regex(css, makeRegex('fonts/MaterialIcons-Regular.woff')); 19 | t.regex(css, makeRegex('fonts/MaterialIcons-Regular.woff2')); 20 | }); 21 | }); 22 | 23 | test('should ignore if the url() is not valid', t => { 24 | return t.context.processStyle('src/invalid.css', { 25 | basePath: 'src', 26 | dest: randomFolder('dest', t.title) 27 | }) 28 | .then(result => { 29 | const css = result.css; 30 | t.is(result.warnings().length, 0); 31 | t.regex(css, makeRegex('b6c8f21e92b50900.jpg')); 32 | t.regex(css, makeRegex('data:image/gif;base64,R0lGOD')); 33 | }); 34 | }); 35 | 36 | test('should ignore if the asset is not found in the src path', t => { 37 | return t.context.processStyle('src/not-found.css', { 38 | basePath: 'src', 39 | dest: randomFolder('dest', t.title) 40 | }) 41 | .then(result => { 42 | const css = result.css; 43 | const warnings = result.warnings(); 44 | t.is(warnings.length, 1); 45 | t.is(warnings[0].text, `Can't read the file in ${ 46 | path.resolve('src/__tests__/src/images/image-not-found.jpg') 47 | }`); 48 | t.regex(css, makeRegex('b6c8f21e92b50900.jpg')); 49 | t.regex(css, makeRegex('images/image-not-found.jpg')); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import path from 'path'; 3 | import valueParser from 'postcss-value-parser'; 4 | import url from 'url'; 5 | import crypto from 'crypto'; 6 | import micromatch from 'micromatch'; 7 | import copy from './lib/copy'; 8 | import { defineCSSDestPath, findBasePath } from './lib/tools-path'; 9 | 10 | const tags = ['path', 'name', 'hash', 'ext', 'query', 'qparams', 'qhash']; 11 | 12 | /** 13 | * Helper function to ignore files 14 | * 15 | * @param {string} filename 16 | * @param {string} extra 17 | * @param {Object} opts plugin options 18 | * @return {boolean} 19 | */ 20 | function ignore(fileMeta, opts) { 21 | if (typeof opts.ignore === 'function') { 22 | return opts.ignore(fileMeta, opts); 23 | } 24 | 25 | if (typeof opts.ignore === 'string' || Array.isArray(opts.ignore)) { 26 | return micromatch.any(fileMeta.sourceValue, opts.ignore); 27 | } 28 | 29 | return false; 30 | } 31 | 32 | /** 33 | * Helper function that reads the file ang get some helpful information 34 | * to the copy process. 35 | * 36 | * @param {string} dirname path of the read file css 37 | * @param {string} sourceInputFile path to the source input file css 38 | * @param {string} value url 39 | * @param {Object} opts plugin options 40 | * @return {Promise} resolve => fileMeta | reject => error message 41 | */ 42 | function getFileMeta(dirname, sourceInputFile, value, opts) { 43 | const parsedUrl = url.parse(value, true); 44 | const filename = decodeURI(parsedUrl.pathname); 45 | const pathname = path.resolve(dirname, filename); 46 | const params = parsedUrl.search || ''; 47 | const hash = parsedUrl.hash || ''; 48 | 49 | // path between the basePath and the filename 50 | const basePath = findBasePath(opts.basePath, pathname); 51 | if (!basePath) { 52 | throw Error(`"basePath" not found in ${pathname}`); 53 | } 54 | 55 | const ext = path.extname(pathname); 56 | const fileMeta = { 57 | sourceInputFile, 58 | sourceValue: value, 59 | filename, 60 | // the absolute path without the #hash param and ?query 61 | absolutePath: pathname, 62 | fullName: path.basename(pathname), 63 | path: path.relative(basePath, path.dirname(pathname)), 64 | // name without extension 65 | name: path.basename(pathname, ext), 66 | // extension without the '.' 67 | ext: ext.slice(1), 68 | query: params + hash, 69 | qparams: params.length > 0 ? params.slice(1) : '', 70 | qhash: hash.length > 0 ? hash.slice(1) : '', 71 | basePath 72 | }; 73 | 74 | return fileMeta; 75 | } 76 | 77 | /** 78 | * process to copy an asset based on the css file, destination 79 | * and the url value 80 | * 81 | * @param {Object} result 82 | * @param {Object} decl postcss declaration 83 | * @param {Object} node postcss-value-parser 84 | * @param {Object} opts plugin options 85 | * @return {Promise} 86 | */ 87 | function processUrl(result, decl, node, opts) { 88 | // ignore from the css file by `!` 89 | if (node.value.indexOf('!') === 0) { 90 | node.value = node.value.slice(1); 91 | return Promise.resolve(); 92 | } 93 | 94 | if ( 95 | node.value.indexOf('/') === 0 || 96 | node.value.indexOf('data:') === 0 || 97 | node.value.indexOf('#') === 0 || 98 | /^[a-z]+:\/\//.test(node.value) 99 | ) { 100 | return Promise.resolve(); 101 | } 102 | 103 | /** 104 | * dirname of the read file css 105 | * @type {String} 106 | */ 107 | const dirname = path.dirname(decl.source.input.file); 108 | 109 | let fileMeta = getFileMeta( 110 | dirname, 111 | decl.source.input.file, 112 | node.value, 113 | opts 114 | ); 115 | 116 | // ignore from the fileMeta config 117 | if (ignore(fileMeta, opts)) { 118 | return Promise.resolve(); 119 | } 120 | 121 | return copy( 122 | fileMeta.absolutePath, 123 | () => fileMeta.resultAbsolutePath, 124 | (contents, isModified) => { 125 | fileMeta.contents = contents; 126 | 127 | return Promise.resolve( 128 | isModified ? opts.transform(fileMeta) : fileMeta 129 | ) 130 | .then(fileMetaTransformed => { 131 | fileMetaTransformed.hash = opts.hashFunction( 132 | fileMetaTransformed.contents 133 | ); 134 | let tpl = opts.template; 135 | if (typeof tpl === 'function') { 136 | tpl = tpl(fileMetaTransformed); 137 | } else { 138 | tags.forEach(tag => { 139 | tpl = tpl.replace( 140 | '[' + tag + ']', 141 | fileMetaTransformed[tag] || opts[tag] || '' 142 | ); 143 | }); 144 | } 145 | 146 | const resultUrl = url.parse(tpl); 147 | fileMetaTransformed.resultAbsolutePath = decodeURI( 148 | path.resolve( 149 | opts.dest, 150 | resultUrl.pathname 151 | ) 152 | ); 153 | fileMetaTransformed.extra = (resultUrl.search || '') + 154 | (resultUrl.hash || ''); 155 | 156 | return fileMetaTransformed; 157 | }) 158 | .then(fileMetaTransformed => fileMetaTransformed.contents); 159 | } 160 | ).then(() => { 161 | const destPath = defineCSSDestPath( 162 | dirname, 163 | fileMeta.basePath, 164 | result, 165 | opts 166 | ); 167 | 168 | node.value = path 169 | .relative(destPath, fileMeta.resultAbsolutePath) 170 | .split('\\') 171 | .join('/') + fileMeta.extra; 172 | }); 173 | } 174 | 175 | /** 176 | * Processes each declaration using postcss-value-parser 177 | * 178 | * @param {Object} result 179 | * @param {Object} decl postcss declaration 180 | * @param {Object} opts plugin options 181 | * @return {Promise} 182 | */ 183 | function processDecl(result, decl, opts) { 184 | const promises = []; 185 | 186 | decl.value = valueParser(decl.value).walk(node => { 187 | if ( 188 | node.type !== 'function' || 189 | node.value !== 'url' || 190 | node.nodes.length === 0 191 | ) { 192 | return; 193 | } 194 | 195 | const promise = Promise.resolve() 196 | .then(() => processUrl(result, decl, node.nodes[0], opts)) 197 | .catch(err => { 198 | decl.warn(result, err.message); 199 | }); 200 | 201 | promises.push(promise); 202 | }); 203 | 204 | return Promise.all(promises).then(() => decl); 205 | } 206 | 207 | /** 208 | * Initialize the postcss-copy plugin 209 | * @param {Object} plugin options 210 | * @return {plugin} 211 | */ 212 | function init(userOpts = {}) { 213 | const opts = Object.assign( 214 | { 215 | template: '[hash].[ext][query]', 216 | preservePath: false, 217 | hashFunction(contents) { 218 | return crypto 219 | .createHash('sha1') 220 | .update(contents) 221 | .digest('hex') 222 | .substr(0, 16); 223 | }, 224 | transform(fileMeta) { 225 | return fileMeta; 226 | }, 227 | ignore: [] 228 | }, 229 | userOpts 230 | ); 231 | 232 | return (style, result) => { 233 | if (opts.basePath) { 234 | if (typeof opts.basePath === 'string') { 235 | opts.basePath = [path.resolve(opts.basePath)]; 236 | } else { 237 | opts.basePath = opts.basePath.map(elem => path.resolve(elem)); 238 | } 239 | } else { 240 | opts.basePath = [process.cwd()]; 241 | } 242 | 243 | if (opts.dest) { 244 | opts.dest = path.resolve(opts.dest); 245 | } else { 246 | throw new Error('Option `dest` is required in postcss-copy'); 247 | } 248 | 249 | const promises = []; 250 | style.walkDecls(decl => { 251 | if (decl.value && decl.value.indexOf('url(') > -1) { 252 | promises.push(processDecl(result, decl, opts)); 253 | } 254 | }); 255 | return Promise.all(promises).then(decls => 256 | decls.forEach(decl => { 257 | decl.value = String(decl.value); 258 | }) 259 | ); 260 | }; 261 | } 262 | 263 | export default postcss.plugin('postcss-copy', init); 264 | -------------------------------------------------------------------------------- /src/lib/copy.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import pify from 'pify'; 4 | import mkdirp from 'mkdirp'; 5 | 6 | const mkdir = pify(mkdirp); 7 | const writeFile = pify(fs.writeFile); 8 | const readFile = pify(fs.readFile); 9 | const stat = pify(fs.stat); 10 | const cacheReader = {}; 11 | const cacheWriter = {}; 12 | 13 | function checkOutput(file) { 14 | if (cacheWriter[file]) { 15 | return cacheWriter[file].then(() => stat(file)); 16 | } 17 | 18 | return stat(file); 19 | } 20 | 21 | function write(file, contents) { 22 | cacheWriter[file] = mkdir(path.dirname(file)).then(() => { 23 | return writeFile(file, contents); 24 | }); 25 | 26 | return cacheWriter[file]; 27 | } 28 | 29 | export default function copy( 30 | input, 31 | output, 32 | transform = (contents) => contents 33 | ) { 34 | let isModified; 35 | let mtime; 36 | 37 | return stat(input) 38 | .catch(() => { 39 | throw Error(`Can't read the file in ${input}`); 40 | }) 41 | .then(stats => { 42 | const item = cacheReader[input]; 43 | mtime = stats.mtime.getTime(); 44 | 45 | if (item && item.mtime === mtime) { 46 | return item 47 | .contents 48 | .then(transform); 49 | } 50 | 51 | isModified = true; 52 | const fileReaded = readFile(input) 53 | .then(contents => transform(contents, isModified)); 54 | 55 | cacheReader[input] = { 56 | mtime, 57 | contents: fileReaded 58 | }; 59 | 60 | return fileReaded; 61 | }) 62 | .then(contents => { 63 | if (typeof output === 'function') { 64 | output = output(); 65 | } 66 | 67 | if (isModified) { 68 | return write(output, contents); 69 | } 70 | 71 | return checkOutput(output).then(() => { 72 | return; 73 | }, () => { 74 | return write(output, contents); 75 | }); 76 | }); 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/lib/tools-path.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | /** 4 | * Quick function to find a basePath where the 5 | * the asset file belongs. 6 | * 7 | * @param paths 8 | * @param pathname 9 | * @returns {string|boolean} 10 | */ 11 | export function findBasePath(paths, pathname) { 12 | for (const item of paths) { 13 | if (pathname.indexOf(item) === 0) { 14 | return item; 15 | } 16 | } 17 | 18 | return false; 19 | } 20 | 21 | /** 22 | * Function to define the dest path of your CSS file 23 | * 24 | * @param dirname 25 | * @param basePath 26 | * @param result 27 | * @param opts 28 | * @returns {string} 29 | */ 30 | export function defineCSSDestPath(dirname, basePath, result, opts) { 31 | const from = path.resolve(result.opts.from); 32 | let to; 33 | 34 | if (result.opts.to) { 35 | /** 36 | * if to === from we can't use it as a valid dest path 37 | * e.g: gulp-postcss comes with this problem 38 | * 39 | */ 40 | to = path.resolve(result.opts.to) === from ? 41 | opts.dest : 42 | path.dirname(result.opts.to); 43 | } else { 44 | to = opts.dest; 45 | } 46 | 47 | if (opts.preservePath) { 48 | let srcPath; 49 | let realBasePath; 50 | 51 | if (dirname === path.dirname(from)) { 52 | srcPath = dirname; 53 | realBasePath = basePath; 54 | } else { 55 | /** 56 | * dirname !== path.dirname(result.opts.from) means that 57 | * postcss-import is grouping different css files in 58 | * only one destination, so, the relative path must be defined 59 | * based on the CSS file where we read the @imports 60 | */ 61 | srcPath = path.dirname(from); 62 | realBasePath = findBasePath(opts.basePath, from); 63 | } 64 | 65 | return path.join(to, path.relative(realBasePath, srcPath)); 66 | } 67 | 68 | return to; 69 | } 70 | 71 | --------------------------------------------------------------------------------