├── .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 | [](https://travis-ci.org/geut/postcss-copy)
3 | [](https://ci.appveyor.com/project/tinchoz49/postcss-copy)
4 | [](https://coveralls.io/github/geut/postcss-copy?branch=master)
5 | [](https://david-dm.org/geut/postcss-copy)
6 | [](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=1iefix&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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------