├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── examples ├── .gitignore ├── copy.js ├── exclude.js ├── fixtures │ ├── .dotfile │ ├── file-1.txt │ └── file-2.txt ├── tar-gz.js └── transform.js ├── index.js ├── lib ├── artifact │ ├── index.js │ ├── runtime-artifact.js │ └── static-artifact.js ├── config │ ├── defaults.js │ └── index.js ├── fs │ ├── copy-symlink.js │ └── index.js ├── index.js ├── patterns │ ├── compose.js │ ├── index.js │ └── is-negative-pattern.js ├── streams │ ├── abstract-writable-stream.js │ ├── copy-stream.js │ ├── filter-stream.js │ ├── glob-stream.js │ ├── index.js │ ├── limited-stream.js │ ├── tar-stream.js │ └── transform-stream.js └── tartifacts.js ├── package.json └── test ├── artifacts ├── write-artifact │ ├── defaults.test.js │ ├── dest-dir.test.js │ ├── dest.test.js │ ├── dotfiles.test.js │ ├── empty-dirs.test.js │ ├── empty-files.test.js │ ├── errors.test.js │ ├── patterns.test.js │ ├── symlink.test.js │ └── tarball.test.js └── write-artifacts.test.js ├── fs └── copy-symlink.test.js ├── options ├── defaults.test.js ├── handle-errors.test.js ├── options.test.js └── override-options.test.js ├── patterns └── compose │ ├── compose-array-notation.test.js │ ├── compose-object-notation.test.js │ └── validate.test.js └── streams ├── copy-stream.test.js ├── tar-stream.test.js └── transform-stream.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.{json,*rc,yml}] 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output/** 2 | coverage/** 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 2017 3 | sourceType: script 4 | 5 | env: 6 | node: true 7 | es6: true 8 | 9 | extends: pedant 10 | 11 | rules: 12 | strict: [error, global] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | node_modules 4 | coverage 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | branches: 4 | only: 5 | - master 6 | - /^greenkeeper/.*$/ 7 | 8 | language: node_js 9 | 10 | matrix: 11 | include: 12 | - node_js: "12" 13 | env: COVERALLS=1 14 | 15 | after_success: 16 | - if [ "x$COVERALLS" = "x1" ]; then npm run coveralls; fi 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v1.1.3 (2016-09-02) 5 | ------------------- 6 | 7 | ### Performance 8 | 9 | * Improved performance of file search by patterns (glob): there is no need to sort files (@blond [#41]). 10 | 11 | [#41]: https://github.com/blond/tartifacts/pull/41 12 | 13 | ### Dependencies 14 | 15 | * Updated `glob-stream` to version `5.3.4` (@greenkeeperio-bot [#38]). 16 | * Updated `archiver` to version `1.1.0` (@greenkeeperio-bot [#39]). 17 | 18 | [#38]: https://github.com/blond/tartifacts/pull/38 19 | [#39]: https://github.com/blond/tartifacts/pull/39 20 | 21 | v1.1.2 (2016-08-24) 22 | ------------------- 23 | 24 | ### Bug Fixes 25 | 26 | * Fixed path to main file (@blond [#35]). 27 | 28 | [#35]: https://github.com/blond/tartifacts/pull/35 29 | 30 | ### Dependencies 31 | 32 | * Updated `archiver` to version `1.0.1` (@greenkeeperio-bot [#28]). 33 | * Updated `copy` to version `0.3.0` (@greenkeeperio-bot [#26]). 34 | 35 | [#28]: https://github.com/blond/tartifacts/pull/28 36 | [#26]: https://github.com/blond/tartifacts/pull/26 37 | 38 | v1.1.1 (2016-06-24) 39 | ------------------- 40 | 41 | ### Commits 42 | 43 | * [[`7eff597`](https://github.com/blond/tartifacts/commit/7eff597)] - chore(package): update copy to version 0.2.3 (@greenkeeperio-bot) 44 | 45 | v1.1.0 (2016-06-16) 46 | ------------------- 47 | 48 | ### CLI 49 | 50 | * Added root dir argument to CLI (@rndD [#11]). 51 | 52 | [#11]: https://github.com/blond/tartifacts/pull/11 53 | 54 | ### Commits 55 | 56 | * [[`bbe949c`](https://github.com/blond/tartifacts/commit/bbe949c)] - docs(cli): update cli usage (@blond) 57 | * [[`6be1484`](https://github.com/blond/tartifacts/commit/6be1484)] - Root dir argument in CLI (@rndD) 58 | * [[`ee78548`](https://github.com/blond/tartifacts/commit/ee78548)] - Added .idea files to gitignore (@alex-k) 59 | * [[`cb53d91`](https://github.com/blond/tartifacts/commit/cb53d91)] - chore(package): update ava to version 0.15.0 (@greenkeeperio-bot) 60 | 61 | v1.0.1 (2016-05-18) 62 | ------------------- 63 | 64 | ### Bug Fixes 65 | 66 | * Should ignore broken symlinks ([#7]). 67 | 68 | [#7]: https://github.com/blond/tartifacts/pull/7 69 | 70 | ### Commits 71 | 72 | * [[`f851122`](https://github.com/blond/tartifacts/commit/f8511228)] - **fix**: should ignore broken symlinks (@blond) 73 | * [[`897cfdf`](https://github.com/blond/tartifacts/commit/897cfdf9)] - **fix**: tests in Node.js v6.2.0 (@blond) 74 | * [[`488143b`](https://github.com/blond/tartifacts/commit/488143b5)] - chore(package): update dependencies (@greenkeeperio-bot) 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andrew Abramov 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tartifacts 2 | ========== 3 | 4 | [![NPM Status][npm-img]][npm] 5 | [![Travis Status][test-img]][travis] 6 | [![Windows Status][appveyor-img]][appveyor] 7 | [![Coverage Status][coverage-img]][coveralls] 8 | [![Dependency Status][david-img]][david] 9 | 10 | [npm]: https://www.npmjs.org/package/tartifacts 11 | [npm-img]: https://img.shields.io/npm/v/tartifacts.svg 12 | 13 | [travis]: https://travis-ci.org/blond/tartifacts 14 | [test-img]: https://img.shields.io/travis/blond/tartifacts/master.svg?label=tests 15 | 16 | [appveyor]: https://ci.appveyor.com/project/blond/tartifacts 17 | [appveyor-img]: https://img.shields.io/appveyor/ci/blond/tartifacts/master.svg?label=windows 18 | 19 | [coveralls]: https://coveralls.io/r/blond/tartifacts 20 | [coverage-img]: https://img.shields.io/coveralls/blond/tartifacts/master.svg 21 | 22 | [david]: https://david-dm.org/blond/tartifacts 23 | [david-img]: https://img.shields.io/david/blond/tartifacts/master.svg 24 | 25 | 26 | The tool to create artifacts for your assemblies. 27 | 28 | Copy only the necessary files and pack them in `tar.gz` file. 29 | 30 | It works much faster than removing unnecessary files and packing with command-line utility `tar`. 31 | 32 | For example, **1 minute** vs **10 seconds** for project with 20 thousand files (80 Mb). 33 | 34 | Install 35 | ------- 36 | 37 | ``` 38 | $ npm install --save tartifacts 39 | ``` 40 | 41 | Usage 42 | ----- 43 | 44 | ```js 45 | const writeArtifacts = require('tartifacts'); 46 | const artifacts = [ 47 | { 48 | name: 'artifact.tar.gz', 49 | patterns: ['sources/**', '!sources/exlib/**'], 50 | tar: true, 51 | gzip: { level: 9 } 52 | }, 53 | { 54 | name: 'artifact-dir', 55 | includes: 'sources/**', 56 | excludes: 'sources/exlib/**', 57 | dotFiles: false // exclude dotfiles, override general settings 58 | } 59 | ]; 60 | 61 | writeArtifacts(artifacts, { 62 | root: './path/to/my-project/', // files of artifacts will be searched from root by artifact patterns, 63 | // for example: ./path/to/my-project/sources/** 64 | destDir: './dest/', 65 | dotFiles: true, // include dotfiles 66 | emptyFiles: true, // include empty files 67 | emptyDirs: true // include empty directories 68 | }) 69 | .then(() => console.log('Copying and packaging of artifacts completed!')) 70 | .catch(console.error); 71 | ``` 72 | 73 | or advanced one which is especially useful for `watch` mode 74 | 75 | ```js 76 | const Tartifacts = require('tartifacts').Tartifacts; 77 | const tartifacts = new Tartifacts({ 78 | watch: true // files and directories will be added to the destination artifact 79 | // in runtime as soon as they appear on the file system 80 | }); 81 | 82 | process.on('SIGTERM', () => tartifacts.closeArtifacts()); 83 | 84 | tartifacts.writeArtifacts({ 85 | name: 'artifact.tar.gz', 86 | patterns: ['sources/**'] 87 | }) 88 | .then(() => { 89 | // will be resolved only after "tartifacts.closeArtifacts()"" call on "SIGTERM" event 90 | // and all artifacts are ready 91 | }) 92 | ``` 93 | 94 | API 95 | --- 96 | 97 | ### writeArtifacts(artifacts, [options]) 98 | 99 | Searchs files of artifact by glob patterns and writes them to artifact in fs. 100 | 101 | ### Tartifacts([options]) 102 | 103 | Constructor which creates an instance of Tartifacts with the methods described below. 104 | 105 | #### Tartifacts.prototype.writeArtifacts(artifacts) 106 | 107 | Does the same as function `writeArtifacts`. 108 | 109 | #### Tartifacts.prototype.closeArtifacts 110 | 111 | Method which is useful when artifacts are created in `watch` mode and should be called in order to resolve `Tartifacts.prototype.writeArtifacts` (see the [usage](#usage) above). 112 | 113 | ### artifacts 114 | 115 | Type: `object`, `object[]` 116 | 117 | The info about artifacts or one artifact. 118 | 119 | Each artifact object has the following fields: 120 | 121 | * [name](#artifactname) 122 | * [root](#artifactroot) 123 | * [destDir](#artifactdestdir) 124 | * [patterns](#artifactpatterns) 125 | * [includes](#artifactincludes) 126 | * [excludes](#artifactexcludes) 127 | * [tar](#artifacttar) 128 | * [gzip](#artifactgzip) 129 | * [followSymlinks](#followsymlinks) 130 | * [dotFiles](#artifactdotfiles) 131 | * [emptyFiles](#artifactemptyfiles) 132 | * [emptyDirs](#artifactemptydirs) 133 | * [transform](#transform) 134 | * [watch](#artifactwatch) 135 | 136 | #### artifact.name 137 | 138 | Type: `string` 139 | 140 | The artifact name of file or directory. 141 | 142 | #### artifact.root 143 | 144 | Type: `string` 145 | 146 | Default: `precess.cwd()` 147 | 148 | The path to root directory. 149 | 150 | The `patterns`, `includes` and `excludes` will be resolved from `root`. 151 | 152 | #### artifact.destDir 153 | 154 | Type: `string` 155 | 156 | The path to destination directory of artifact. 157 | 158 | The `dest` and `name` will be resolved from `destDir`. If `destDir` is not specified, `dest` and `name` will be resolved from `root`. 159 | 160 | #### artifact.patterns 161 | 162 | Type: `string`, `string[]`, `object` 163 | 164 | Default: `[]` 165 | 166 | The paths to files which need to be included or excluded. 167 | 168 | Read more about patterns in [glob](https://github.com/isaacs/node-glob#glob-primer) package. 169 | 170 | #### artifact.includes 171 | 172 | Type: `string`, `string[]` 173 | 174 | Default: `[]` 175 | 176 | The paths to files which need to be included. 177 | 178 | #### artifact.excludes 179 | 180 | Type: `string`, `string[]` 181 | 182 | Default: `[]` 183 | 184 | The paths to files which need to be excluded. 185 | 186 | Can be specifed as an object: 187 | 188 | ```js 189 | { 190 | name: 'artifact', 191 | patterns: { 192 | './subdir1': ['dir1/**'], 193 | './subdir2': ['dir2/**'] 194 | } 195 | } 196 | ``` 197 | 198 | This means that all files which match `dir1/**` will be added to directory `artifact/subdir1` and all files which match `dir2/**` will be added to directory `artifact/subdir2`, so, for example, file `./dir1/file.ext` will be added to artifact as `./artifact/subdir1/dir1/file.ext` and file, for `./dir2/file.ext` will be added to artifact as `./artifact/subdir2/dir2/file.ext`. 199 | 200 | #### artifact.tar 201 | 202 | Type: `boolean` 203 | 204 | Default: `false` 205 | 206 | If `true`, destination directory will be packed to tarball file. 207 | 208 | Otherwise files of artifact will be copied to destination directory. 209 | 210 | #### artifact.gzip 211 | 212 | Type: `boolean`, `object` 213 | 214 | Default: `false` 215 | 216 | If `true`, tarball file will be gzipped. 217 | 218 | To change the compression level pass object with `level` field. 219 | 220 | #### artifact.followSymlinks 221 | 222 | Type: `boolean` 223 | 224 | Default: `false` 225 | 226 | Follow symlinked files and directories. 227 | 228 | *Note that this can result in a lot of duplicate references in the presence of cyclic links.* 229 | 230 | #### artifact.dotFiles 231 | 232 | Type: `boolean` 233 | 234 | Default: `true` 235 | 236 | Include dotfiles. 237 | 238 | #### artifact.emptyFiles 239 | 240 | Type: `boolean` 241 | 242 | Default: `true` 243 | 244 | Include empty files. 245 | 246 | #### artifact.emptyDirs 247 | 248 | Type: `boolean` 249 | 250 | Default: `true` 251 | 252 | Include empty directories. 253 | 254 | #### artifact.transform 255 | 256 | Type: `Function` 257 | 258 | Default: `null` 259 | 260 | It allows you to modify files before they are archived/copied. 261 | 262 | Transform function has one argument with type `{path: string, relative: string, base: string, cwd: string, history: string[]}` and should return the modified chunk or array of chunks. 263 | 264 | Note: now support only sync functions 265 | 266 | [Example](./examples/transform.js) 267 | 268 | ### artifact.watch 269 | 270 | Type: `boolean` 271 | 272 | Default: `false` 273 | 274 | Tartifacts will work in an observe mode which means that all files and directories will be added to a destination directory or archive as soon as they appear on a file system. 275 | 276 | Note: it is recommended to use this mode with the advanced API which is described in the [usage](#usage) above in order to have the ability to stop the tool 277 | 278 | ### options 279 | 280 | Type: `object` 281 | 282 | Allows you to configure settings for write artifacts. 283 | 284 | The options specify general settings for all artifacts: 285 | 286 | * [root](#artifactroot) 287 | * [destDir](#artifactdestdir) 288 | * [tar](#artifacttar) 289 | * [gzip](#artifactgzip) 290 | * [followSymlinks](#followsymlinks) 291 | * [dotFiles](#artifactdotfiles) 292 | * [emptyFiles](#artifactemptyfiles) 293 | * [emptyDirs](#artifactemptydirs) 294 | * [transform](#transform) 295 | * [watch](#artifactwatch) 296 | 297 | License 298 | ------- 299 | 300 | MIT © [Andrew Abramov](https://github.com/blond) 301 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | branches: 4 | only: 5 | - master 6 | - /^greenkeeper/.*$/ 7 | 8 | environment: 9 | nodejs_version: "6" 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version 13 | - node --version 14 | - npm --version 15 | - npm cache clean 16 | - npm install 17 | 18 | test_script: 19 | - npm run unit-test 20 | 21 | build: off 22 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | dest 2 | -------------------------------------------------------------------------------- /examples/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tartifacts = require('../index'); 4 | 5 | tartifacts({ 6 | dest: './dest/some-dir', 7 | includes: 'fixtures/**' 8 | }, { root: __dirname }) 9 | .then(() => console.log('Copying completed!')) 10 | .catch(err => console.log(err)); 11 | -------------------------------------------------------------------------------- /examples/exclude.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tartifacts = require('../index'); 4 | 5 | tartifacts({ 6 | dest: './dest/without-excludes', 7 | includes: 'fixtures/**', 8 | excludes: 'fixtures/file-1.txt' 9 | }, { root: __dirname, dot: false }) 10 | .then(() => console.log('Copying completed!')) 11 | .catch(err => console.log(err)); 12 | -------------------------------------------------------------------------------- /examples/fixtures/.dotfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/tartifacts/2519629e9e1cd2a64e7cbe57d48629a195fa516d/examples/fixtures/.dotfile -------------------------------------------------------------------------------- /examples/fixtures/file-1.txt: -------------------------------------------------------------------------------- 1 | Hi! 2 | -------------------------------------------------------------------------------- /examples/fixtures/file-2.txt: -------------------------------------------------------------------------------- 1 | Hellow! 2 | -------------------------------------------------------------------------------- /examples/tar-gz.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tartifacts = require('../index'); 4 | 5 | tartifacts({ 6 | dest: './dest/some-file.tar.gz', 7 | includes: 'fixtures/**', 8 | tar: true, 9 | gzip: { level: 1 } 10 | }, { root: __dirname }) 11 | .then(() => console.log('Packaging completed!')) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /examples/transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const tartifacts = require('../index'); 6 | 7 | const transform = (chunk) => { 8 | const f = path.parse(chunk.path); 9 | 10 | if (f.ext === '.txt') { 11 | chunk.path = path.join(f.dir, 'dir', f.base); 12 | } 13 | 14 | return chunk; 15 | }; 16 | 17 | tartifacts({ 18 | dest: './dest/some-file.tar.gz', 19 | includes: 'fixtures/**', 20 | tar: true, 21 | gzip: { level: 1 }, 22 | transform 23 | }, { root: __dirname }) 24 | .then(() => console.log('Packaging completed!')) 25 | .catch(console.error); 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/index'); 4 | -------------------------------------------------------------------------------- /lib/artifact/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const StaticArtifact = require('./static-artifact'); 4 | const RuntimeArtifact = require('./runtime-artifact'); 5 | 6 | exports.create = (options) => options.watch ? RuntimeArtifact.create(options) : StaticArtifact.create(options); 7 | -------------------------------------------------------------------------------- /lib/artifact/runtime-artifact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const StaticArtifact = require('./static-artifact'); 5 | const { FilterStream } = require('../streams'); 6 | 7 | module.exports = class RuntimeArtifact extends StaticArtifact { 8 | constructor(options) { 9 | super(options); 10 | 11 | this._map = new Map(); 12 | this._closed = false; 13 | } 14 | 15 | _write() { 16 | return new Promise((resolve, reject) => { 17 | this._writeStream = this._createWriteStream(); 18 | 19 | this._writeStream.on('error', reject).on('close', resolve); 20 | 21 | return this._watchFS(); 22 | }); 23 | } 24 | 25 | _watchFS() { 26 | return this._closed 27 | ? this._scanFS({ end: true }) 28 | : this._scanFS({ end: false }).then(() => this._watchFS()); 29 | } 30 | 31 | _scanFS(options) { 32 | return new Promise((resolve, reject) => { 33 | const readStream = this._readStream = this._createReadStream(); 34 | const statStream = this._statStream = this._createStatStream(); 35 | const filterStream = this._filterStream = this._createFilterStream(); 36 | const transformStream = this._transformStream = this._createTransformStream(); 37 | 38 | readStream.on('error', reject); 39 | statStream.on('error', reject); 40 | filterStream.on('error', reject); 41 | transformStream.on('error', reject).on('end', resolve); 42 | 43 | readStream.pipe(statStream).pipe(filterStream).pipe(transformStream).pipe(this._writeStream, options); 44 | }); 45 | } 46 | 47 | _createFilterStream() { 48 | return new FilterStream((chunk) => { 49 | const relative = path.join(chunk.subdir, path.relative(chunk.cwd, chunk.path)); 50 | const mtimeMs = this._map.get(relative); 51 | 52 | if (!mtimeMs || mtimeMs < chunk.lstats.mtimeMs) { 53 | this._map.set(relative, chunk.lstats.mtimeMs); 54 | return true; 55 | } 56 | 57 | return false; 58 | }); 59 | } 60 | 61 | close() { 62 | this._readStream.destroy(); 63 | 64 | this._closed = true; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /lib/artifact/static-artifact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('graceful-fs'); 5 | const makeDir = require('make-dir'); 6 | 7 | const { CopyStream, GlobStream, TarStream, TransformStream } = require('../streams'); 8 | 9 | module.exports = class StaticArtifact { 10 | static create(options) { 11 | return new this(options); 12 | } 13 | 14 | constructor(options) { 15 | this._options = options; 16 | } 17 | 18 | write() { 19 | return makeDir(this._destDir, { fs }) 20 | .then(() => this._write()); 21 | } 22 | 23 | _write() { 24 | return new Promise((resolve, reject) => { 25 | const readStream = this._createReadStream(); 26 | const statStream = this._createStatStream(); 27 | const transformStream = this._createTransformStream(); 28 | const writeStream = this._createWriteStream(); 29 | 30 | readStream.on('error', reject); 31 | transformStream.on('error', reject); 32 | writeStream.on('error', reject).on('close', resolve); 33 | 34 | readStream.pipe(statStream).pipe(transformStream).pipe(writeStream); 35 | }); 36 | } 37 | 38 | get _destDir() { 39 | return this._options.tar ? path.dirname(this._options.path) : this._options.path; 40 | } 41 | 42 | _createReadStream() { 43 | const readStreamOptions = { 44 | cwd: this._options.root, 45 | dot: this._options.dotFiles, 46 | nodir: !this._options.emptyDirs, 47 | follow: this._options.followSymlinks, 48 | nosort: true 49 | }; 50 | 51 | return new GlobStream(this._options.patterns, readStreamOptions); 52 | } 53 | 54 | _createWriteStream() { 55 | const writeStreamOptions = { 56 | emptyFiles: this._options.emptyFiles, 57 | emptyDirs: this._options.emptyDirs, 58 | followSymlinks: this._options.followSymlinks 59 | }; 60 | const gzipOptions = {gzip: this._options.gzip, gzipOptions: this._options.gzipOptions}; 61 | 62 | return this._options.tar 63 | ? new TarStream(this._options.path, Object.assign({}, writeStreamOptions, gzipOptions)) 64 | : new CopyStream(this._options.path, writeStreamOptions); 65 | } 66 | 67 | _createTransformStream(transform = this._options.transform) { 68 | return new TransformStream(transform); 69 | } 70 | 71 | _createStatStream() { 72 | return this._createTransformStream((chunk) => { 73 | return new Promise((resolve, reject) => { 74 | fs.lstat(chunk.path, (err, lstats) => { 75 | if (err) { 76 | return reject(err); 77 | } 78 | 79 | chunk.lstats = lstats; 80 | 81 | resolve(chunk); 82 | }); 83 | }); 84 | }); 85 | } 86 | 87 | close() {} // eslint-disable-line class-methods-use-this 88 | }; 89 | -------------------------------------------------------------------------------- /lib/config/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: process.cwd(), 5 | destDir: '.', 6 | 7 | tar: false, 8 | gzip: false, 9 | gzipOptions: { level: 1 }, 10 | 11 | followSymlinks: false, 12 | dotFiles: true, 13 | emptyFiles: true, 14 | emptyDirs: true, 15 | 16 | transform: null, 17 | 18 | watch: false 19 | }; 20 | -------------------------------------------------------------------------------- /lib/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const _ = require('lodash'); 5 | 6 | const composePatterns = require('../patterns').compose; 7 | const defaults = require('./defaults'); 8 | 9 | exports.format = (artifact, settings) => { 10 | const options = _({}).extend(settings, artifact).defaults(defaults).value(); 11 | 12 | const dest = options.dest || options.name; 13 | 14 | if (!dest) { 15 | throw new Error('Option "dest" or "name" must be specified for each artifact'); 16 | } 17 | 18 | const root = path.resolve(options.root); 19 | const destDir = path.isAbsolute(options.destDir) ? options.destDir : path.join(options.root, options.destDir); 20 | const destPath = path.isAbsolute(dest) ? dest : path.join(destDir, dest); 21 | const patterns = composePatterns(options.patterns, { 22 | include: options.includes, 23 | exclude: options.excludes 24 | }); 25 | const gzipOptions = typeof options.gzip === 'object' ? options.gzip : options.gzipOptions; 26 | const gzip = Boolean(options.gzip); 27 | 28 | if (!options.tar && options.gzip) { 29 | throw new Error('Option "gzip" must be used only with option "tar"'); 30 | } 31 | 32 | if (options.transform && typeof options.transform !== 'function') { 33 | throw new Error('Option "transform" must be a function'); 34 | } 35 | 36 | return _.extend({}, options, {root, destDir, path: destPath, patterns, gzip, gzipOptions}); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/fs/copy-symlink.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nativeFs = require('fs'); 4 | const path = require('path'); 5 | 6 | const pify = require('pify'); 7 | const makeDir = require('make-dir'); 8 | 9 | const nativeReadlink = pify(nativeFs.readlink); 10 | const nativeSymlink = pify(nativeFs.symlink); 11 | 12 | /** 13 | * Copies symlink to destination. 14 | * 15 | * @param {string} source Path to symbolic link you want to copy. 16 | * @param {string} destination Where you want the symbolic link copied. 17 | * @param {Object} [options] 18 | * @param {Object} [options.fs] Use a custom fs implementation. For example `graceful-fs`. 19 | * @returns {Promise} 20 | */ 21 | module.exports = (source, destination, options) => { 22 | const opts = options || {}; 23 | const destPath = path.resolve(destination); 24 | const destDir = path.dirname(destPath); 25 | const readlink = opts.fs ? pify(opts.fs.readlink) : nativeReadlink; 26 | const symlink = opts.fs ? pify(opts.fs.symlink) : nativeSymlink; 27 | 28 | return Promise.all([ 29 | makeDir(destDir, { fs: opts.fs || nativeFs }), 30 | readlink(source) 31 | ]).then(res => { 32 | const target = res[1]; 33 | 34 | return symlink(target, destination); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /lib/fs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | copySymlink: require('./copy-symlink') 5 | }; 6 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Tartifacts = require('./tartifacts'); 4 | 5 | function writeArtifacts(artifacts, options) { 6 | const tartifacts = new Tartifacts(options); 7 | 8 | return tartifacts.writeArtifacts(artifacts); 9 | } 10 | 11 | writeArtifacts.Tartifacts = Tartifacts; 12 | 13 | module.exports = writeArtifacts; 14 | -------------------------------------------------------------------------------- /lib/patterns/compose.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const _ = require('lodash'); 5 | const isNegativePattern = require('./is-negative-pattern'); 6 | 7 | /** 8 | * Composes glob patterns. 9 | * 10 | * In `patterns` field will be added includes and excludes patterns. 11 | * 12 | * The excludes patterns will be converted to negative patterns. 13 | * 14 | * @param {string|string[]|object} config The glob patterns to files which need to be included or excluded. 15 | * @param {Object} [actions] 16 | * @param {string[]} [actions.include] The glob patterns to files which need to be included. 17 | * @param {string[]} [actions.exclude] The glob patterns to files which need to be excluded. 18 | * @returns {object} 19 | */ 20 | module.exports = (config = [], actions = {}) => { 21 | assert(!_.isEmpty(config) || !_.isEmpty(actions.include), 'you should specify the includes or patterns parameters for artifact.'); 22 | 23 | const includes = [].concat(actions.include || []); 24 | const excludes = [].concat(actions.exclude || []).map(pattern => { 25 | return isNegativePattern(pattern) ? pattern : `!${pattern}` 26 | }); 27 | 28 | if (includes.some(isNegativePattern)) { 29 | throw new Error('the includes parameter of artifact should not contains negative patterns.'); 30 | } 31 | 32 | if (!_.isPlainObject(config)) { 33 | config = { '.': [].concat(config) }; 34 | } 35 | 36 | const normalizedConfig = _.mapValues(config, (patterns) => [].concat(patterns, includes, excludes)); 37 | 38 | _.forEach(normalizedConfig, (patterns) => { 39 | if (isNegativePattern(patterns[0])) { 40 | throw new Error('the first pattern of artifact should not be negative.'); 41 | } 42 | }); 43 | 44 | return normalizedConfig; 45 | }; 46 | -------------------------------------------------------------------------------- /lib/patterns/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | compose: require('./compose'), 5 | isNegativePattern: require('./is-negative-pattern') 6 | }; 7 | -------------------------------------------------------------------------------- /lib/patterns/is-negative-pattern.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Returns `true` if pattern is negative. 5 | * 6 | * @param {string} pattern — the glob pattern. 7 | * 8 | * @returns {boolean} 9 | */ 10 | module.exports = (pattern) => pattern.charAt(0) === '!'; 11 | -------------------------------------------------------------------------------- /lib/streams/abstract-writable-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const stream = require('stream'); 5 | 6 | const fs = require('graceful-fs'); 7 | 8 | const defaults = { 9 | emptyFiles: true, 10 | emptyDirs: true 11 | }; 12 | 13 | /** 14 | * Abstract writable stream. 15 | * 16 | * Input readable stream should have object chunks with file info in vinyl format. 17 | * 18 | * @extends stream.Writable 19 | */ 20 | module.exports = class AbstractWritableStream extends stream.Writable { 21 | /** 22 | * Create artifact stream. 23 | * 24 | * @param {string} dest The path to destination file or directory. 25 | * @param {object} [options] The options. 26 | * @param {boolean} [options.emptyFiles=true] Include empty files. 27 | * @param {boolean} [options.emptyDirs=true] Include empty directories. 28 | */ 29 | constructor(dest, options) { 30 | const opts = Object.assign(defaults, options); 31 | 32 | super({ objectMode: true }); 33 | 34 | this._followSymlinks = opts.followSymlinks; 35 | this._emptyFiles = opts.emptyFiles; 36 | this._emptyDirs = opts.emptyDirs; 37 | this._dest = path.resolve(dest); 38 | } 39 | /** 40 | * Adds directory (without its files and subdirs) to artifact. 41 | * 42 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} dir — the directory info. 43 | * @param {function} callback — call this function when processing is complete. 44 | * @abstract 45 | */ 46 | // eslint-disable-next-line class-methods-use-this 47 | addDirectory(dir, callback) { // eslint-disable-line no-unused-vars 48 | throw new Error('The `addDirectory` method is not implemented.'); 49 | } 50 | /** 51 | * Adds file to artifact. 52 | * 53 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} file — the file info. 54 | * @param {function} callback — call this function when processing is complete. 55 | * @abstract 56 | */ 57 | // eslint-disable-next-line class-methods-use-this 58 | addFile(file, callback) { // eslint-disable-line no-unused-vars 59 | throw new Error('The `addFile` method is not implemented.'); 60 | } 61 | /** 62 | * Adds symlink to artifact. 63 | * 64 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} file — the file info. 65 | * @param {function} callback — call this function when processing is complete. 66 | * @abstract 67 | */ 68 | // eslint-disable-next-line class-methods-use-this 69 | addSymbolicLink(file, callback) { // eslint-disable-line no-unused-vars 70 | throw new Error('The `addSymbolicLink` method is not implemented.'); 71 | } 72 | /** 73 | * Adds file or directory to artifact. 74 | * 75 | * @param {{path: string, base: string, cwd: string, history: string[], subdir: string}} chunk — the file to be added in artifact. 76 | * @param {string} encoding — ignore the encoding argument, need only for stream in non-object mode. 77 | * @param {object} callback — call this function when processing is complete for the supplied chunk. 78 | * @returns {undefined} 79 | */ 80 | _write(chunk, encoding, callback) { 81 | const lstats = chunk.lstats; 82 | const relative = path.join(chunk.subdir, path.relative(chunk.cwd, chunk.path)); 83 | 84 | // ignore root dir 85 | if (relative === '') { 86 | return callback(); 87 | } 88 | 89 | const file = Object.assign({ relative, stats: lstats }, chunk); 90 | 91 | const addDirectory = (dirInfo) => { 92 | // ignore empty dir 93 | if (!this._emptyDirs) { 94 | return callback(); 95 | } 96 | 97 | return this.addDirectory(dirInfo, callback); 98 | }; 99 | const addFile = (fileInfo) => { 100 | // ignore empty file 101 | if (!this._emptyFiles && lstats.size === 0) { 102 | return callback(); 103 | } 104 | 105 | this.addFile(fileInfo, callback); 106 | }; 107 | 108 | if (lstats.isSymbolicLink()) { 109 | if (this._followSymlinks) { 110 | return fs.stat(file.history[0], (error, stats) => { 111 | if (error) { 112 | return callback(error); 113 | } 114 | 115 | const targetFile = Object.assign({}, file, { stats }); 116 | 117 | if (stats.isDirectory()) { 118 | addDirectory(targetFile); 119 | } else { 120 | addFile(targetFile); 121 | } 122 | }); 123 | } 124 | 125 | return this.addSymbolicLink(file, callback); 126 | } 127 | 128 | if (lstats.isDirectory()) { 129 | return addDirectory(file); 130 | } 131 | 132 | addFile(file); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /lib/streams/copy-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | 6 | const fs = require('graceful-fs'); 7 | const cpFile = require('cp-file'); 8 | const makeDir = require('make-dir'); 9 | 10 | const copySymlink = require('../fs/copy-symlink'); 11 | 12 | const AbstractWritableStream = require('./abstract-writable-stream'); 13 | 14 | /** 15 | * Copy stream. 16 | * 17 | * Stream writes files to destination directory. 18 | * 19 | * Input readable stream should have object chunks with file info in vinyl format. 20 | * 21 | * @extends ArtifactStream 22 | */ 23 | module.exports = class CopyStream extends AbstractWritableStream { 24 | /** 25 | * Creates copy stream. 26 | * 27 | * @param {string} dest The path to destination directory. 28 | * @param {object} [options] The options. 29 | * @param {boolean} [options.emptyFiles=true] Include empty files. 30 | * @param {boolean} [options.emptyDirs=true] Include empty directories. 31 | */ 32 | constructor(dest, options) { 33 | super(dest, options); 34 | 35 | assert(dest, 'You should specify the destination path to directory.'); 36 | 37 | this.once('finish', () => this.emit('close')); 38 | } 39 | /** 40 | * Copies directory (without its files and subdirs) to destination directory. 41 | * 42 | * Keeps original path relative to cwd. 43 | * 44 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} dir — the directory info. 45 | * @param {function} callback — call this function when processing is complete. 46 | */ 47 | addDirectory(dir, callback) { 48 | const dirname = path.join(this._dest, dir.relative); 49 | 50 | makeDir(dirname, { fs }) 51 | .then(() => callback()) 52 | .catch(callback); 53 | } 54 | /** 55 | * Copies file to destination directory. 56 | * 57 | * Keeps original path relative to cwd. 58 | * 59 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} file — the directory info. 60 | * @param {function} callback — call this function when processing is complete. 61 | */ 62 | addFile(file, callback) { 63 | const dest = path.join(this._dest, file.relative); 64 | 65 | cpFile(file.history[0], dest) 66 | .then(() => callback()) 67 | .catch(callback); 68 | } 69 | /** 70 | * Adds symlink to artifact. 71 | * 72 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} file — the file info. 73 | * @param {function} callback — call this function when processing is complete. 74 | * @abstract 75 | */ 76 | addSymbolicLink(file, callback) { 77 | const dest = path.join(this._dest, file.relative); 78 | 79 | copySymlink(file.history[0], dest, { fs }) 80 | .then(() => callback()) 81 | .catch(callback); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /lib/streams/filter-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stream = require('stream'); 4 | 5 | module.exports = class FilterStream extends stream.Transform { 6 | constructor(filterFn) { 7 | super({ objectMode: true }); 8 | 9 | this._filterFn = filterFn; 10 | } 11 | 12 | _transform(chunk, encoding, callback) { 13 | try { 14 | if (this._filterFn(chunk)) { 15 | this.push(chunk); 16 | } 17 | 18 | callback(); 19 | } catch (e) { 20 | callback(e); 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/streams/glob-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stream = require('stream'); 4 | const _ = require('lodash'); 5 | const fs = require('graceful-fs'); 6 | const proxyquire = require('proxyquire'); 7 | const gs = proxyquire('glob-stream', { 8 | 'glob': proxyquire('glob', { 'fs': fs }) 9 | }); 10 | 11 | module.exports = class GlobStream extends stream.Readable { 12 | constructor(config, options) { 13 | super({ objectMode: true }); 14 | 15 | this._readables = new Set(); 16 | 17 | _.forEach(config, (patterns, subdir) => { 18 | const readable = gs(patterns, options); 19 | this._readables.add(readable); 20 | 21 | readable.on('data', (chunk) => this.push(Object.assign({ subdir }, chunk))); 22 | readable.on('error', (error) => this.emit('error', error)); 23 | readable.on('end', () => { 24 | this._readables.delete(readable); 25 | if (this._readables.size === 0) { 26 | this.push(null); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | _destroy() { 33 | this._readables.forEach((readable) => readable.destroy()); 34 | } 35 | 36 | _read() {} // eslint-disable-line class-methods-use-this 37 | }; 38 | -------------------------------------------------------------------------------- /lib/streams/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | CopyStream: require('./copy-stream'), 5 | FilterStream: require('./filter-stream'), 6 | GlobStream: require('./glob-stream'), 7 | LimitedStream: require('./limited-stream'), 8 | TarStream: require('./tar-stream'), 9 | TransformStream: require('./transform-stream') 10 | }; 11 | -------------------------------------------------------------------------------- /lib/streams/limited-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stream = require('stream'); 4 | 5 | /** 6 | * Limited Stream. 7 | * 8 | * Transform stream which can be used to limit the amount of bytes which pass from a readable stream to a writable stream. 9 | * This functionality can be necessary when a readable stream is being changed in a runtime, but a writable stream expects fixed amount of bytes. 10 | * 11 | * @extends stream.Transform 12 | */ 13 | module.exports = class LimitedStream extends stream.Transform { 14 | constructor(size) { 15 | super(); 16 | 17 | this._size = size; 18 | } 19 | 20 | _transform(chunk, encoding, callback) { 21 | if (this._size < chunk.length) { 22 | chunk = chunk.slice(0, this._size); 23 | } 24 | 25 | this.push(chunk); 26 | this._size -= chunk.length; 27 | callback(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/streams/tar-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const fs = require('graceful-fs'); 6 | const archiver = require('archiver'); 7 | 8 | const AbstractWritableStream = require('./abstract-writable-stream'); 9 | const LimitedStream = require('./limited-stream'); 10 | 11 | /** 12 | * Tarball stream. 13 | * 14 | * Stream writes tarball with files to destination file. 15 | * 16 | * Input readable stream should have object chunks with file info in vinyl format. 17 | * 18 | * @extends AbstractWritableStream 19 | */ 20 | module.exports = class TarStream extends AbstractWritableStream { 21 | /** 22 | * Creates tarball stream. 23 | * 24 | * @param {string} dest The path to destination file. 25 | * @param {object} [options] The options. 26 | * @param {boolean} [options.emptyFiles=true] Include empty files. 27 | * @param {boolean} [options.emptyDirs=true] Include empty directories. 28 | * @param {boolean} [options.gzip=false] Compress the tar archive using gzip. Passed to zlib to control compression. 29 | * @param {object} [options.gzipOptions] The gzip options. 30 | */ 31 | constructor(dest, options) { 32 | super(dest, options); 33 | 34 | assert(dest, 'You should specify the destination path to tarrball.'); 35 | 36 | options || (options = {}); 37 | 38 | try { 39 | const output = fs.createWriteStream(dest, { autoClose: true }); 40 | const archive = archiver('tar', { 41 | gzip: options.gzip, 42 | gzipOptions: options.gzipOptions 43 | }); 44 | 45 | archive.pipe(output); 46 | archive.on('error', err => this.emit('error', err)); 47 | 48 | output.once('open', () => this.emit('open')); 49 | output.once('close', () => this.emit('close')); 50 | output.on('error', err => this.emit('error', err)); 51 | 52 | this._archive = archive; 53 | 54 | this.once('finish', () => archive.finalize()); 55 | } catch (err) { 56 | this.emit('error', err); 57 | } 58 | } 59 | /** 60 | * Adds directory (without its files and subdirs) to archive. 61 | * 62 | * Keeps original path relative to cwd. 63 | * 64 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} dir — the directory info. 65 | * @param {function} callback — call this function when processing is complete. 66 | */ 67 | addDirectory(dir, callback) { 68 | this._archive.append('', { 69 | name: dir.relative, 70 | type: 'directory' 71 | }); 72 | 73 | callback(); 74 | } 75 | /** 76 | * Adds file to archive. 77 | * 78 | * Keeps original path relative to cwd. 79 | * 80 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} file — the directory info. 81 | * @param {function} callback — call this function when processing is complete. 82 | */ 83 | addFile(file, callback) { 84 | const readable = fs.createReadStream(file.history[0], { autoClose: true }) 85 | .on('error', callback) 86 | .on('end', callback); 87 | 88 | const limitedStream = new LimitedStream(file.stats.size); 89 | 90 | this._archive.append(readable.pipe(limitedStream), { 91 | name: file.relative, 92 | type: 'file', 93 | // We keep original behaviour for node-archiver (0o644 by default for files) 94 | // but add `x` flags for executables 95 | mode: 0o644 | (0o111 & file.stats.mode), 96 | _stats: file.stats, 97 | size: file.stats.size 98 | }); 99 | } 100 | /** 101 | * Adds symlink to archive. 102 | * 103 | * Keeps original path relative to cwd. 104 | * 105 | * @param {{path: string, relative: string, base: string, cwd: string, stats: fs.Stats, history: string[], subdir: string}} file — the directory info. 106 | * @param {function} callback — call this function when processing is complete. 107 | */ 108 | addSymbolicLink(file, callback) { 109 | fs.readlink(file.history[0], (err, target) => { 110 | if (err) { 111 | return callback(err); 112 | } 113 | 114 | this._archive.symlink(file.relative, target); 115 | 116 | callback(); 117 | }); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /lib/streams/transform-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stream = require('stream'); 4 | 5 | /** 6 | * Transform stream. 7 | * 8 | * @extends stream.Transform 9 | */ 10 | module.exports = class TransformStream extends stream.Transform { 11 | /** 12 | * 13 | * @param {Function} [transform] 14 | */ 15 | constructor(transform) { 16 | super({ objectMode: true }); 17 | 18 | this.transform = transform; 19 | } 20 | 21 | /** 22 | * @param {{path: string, relative: string, base: string, cwd: string, subdir: string}} chunk — the file to be transformed. 23 | * @param {string} encoding — ignore the encoding argument, need only for stream in non-object mode. 24 | * @param {object} callback — call this function when processing is complete for the supplied chunk. 25 | * @returns {undefined} 26 | */ 27 | _transform(chunk, encoding, callback) { 28 | if (chunk.history) { 29 | chunk.history.push(chunk.path); 30 | } else { 31 | chunk.history = [chunk.path]; 32 | } 33 | 34 | Promise.resolve() 35 | .then(() => this.transform ? this.transform(chunk) : chunk) 36 | .then((transformed) => { 37 | const chunks = Array.isArray(transformed) ? transformed : [transformed]; 38 | 39 | chunks.forEach((c) => c && this.push(c)); 40 | 41 | callback(); 42 | }) 43 | .catch(callback); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /lib/tartifacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Artifact = require('./artifact'); 4 | const config = require('./config'); 5 | 6 | module.exports = class Tartifacts { 7 | static create(options) { 8 | return new Tartifacts(options); 9 | } 10 | 11 | constructor(options = {}) { 12 | this._options = options; 13 | 14 | this._artifacts = []; 15 | } 16 | 17 | writeArtifacts(artifacts = []) { 18 | return Promise.all([].concat(artifacts).map((options) => { 19 | const artifact = Artifact.create(config.format(options, this._options)); 20 | 21 | this._artifacts.push(artifact); 22 | 23 | return artifact.write(); 24 | })); 25 | } 26 | 27 | closeArtifacts() { 28 | return this._artifacts.map((artifact) => artifact.close()); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tartifacts", 3 | "version": "2.0.0-15", 4 | "description": "The tool to create artifacts for your assemblies.", 5 | "license": "MIT", 6 | "repository": "blond/tartifacts", 7 | "author": "Andrew Abramov (github.com/blond)", 8 | "keywords": [ 9 | "artifacts", 10 | "assemblies", 11 | "tarball", 12 | "tar", 13 | "gzip", 14 | "tar.gz", 15 | "destination", 16 | "copy", 17 | "files" 18 | ], 19 | "main": "index.js", 20 | "files": [ 21 | "lib/**", 22 | "index.js" 23 | ], 24 | "engines": { 25 | "node": ">= 12.18" 26 | }, 27 | "dependencies": { 28 | "archiver": "2.1.1", 29 | "cp-file": "9.0.0", 30 | "glob": "7.1.2", 31 | "glob-stream": "6.1.0", 32 | "graceful-fs": "4.1.11", 33 | "lodash": "4.17.10", 34 | "make-dir": "1.2.0", 35 | "pify": "3.0.0", 36 | "proxyquire": "1.8.0" 37 | }, 38 | "devDependencies": { 39 | "ava": "^0.18.0", 40 | "coveralls": "^3.0.0", 41 | "eslint": "^4.11.0", 42 | "eslint-config-pedant": "^1.0.0", 43 | "is-gzip": "^2.0.0", 44 | "is-tar": "^1.0.0", 45 | "mock-fs": "^4.4.2", 46 | "nyc": "^11.4.1", 47 | "sinon": "^6.1.3", 48 | "stream-array": "^1.1.2", 49 | "tar": "^4.0.2" 50 | }, 51 | "scripts": { 52 | "pretest": "eslint .", 53 | "test": "nyc ava", 54 | "unit-test": "ava", 55 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 56 | }, 57 | "ava": { 58 | "serial": true, 59 | "verbose": true, 60 | "files": [ 61 | "test/**/*.test.js" 62 | ], 63 | "sources": [ 64 | "lib/**/*.js" 65 | ] 66 | }, 67 | "greenkeeper": { 68 | "ignore": [ 69 | "ava" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/defaults.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should copy artifact by default', async t => { 13 | mockFs({ 14 | 'source-dir': { 15 | 'file-1.txt': 'Hi!', 16 | 'file-2.txt': 'Hello!' 17 | } 18 | }); 19 | 20 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }); 21 | 22 | const stats = fs.statSync('artifact-dir'); 23 | 24 | t.true(stats.isDirectory()); 25 | }); 26 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/dest-dir.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should create artifact in dest-dir', async t => { 13 | mockFs({ 14 | 'source-dir': {} 15 | }); 16 | 17 | await writeArtifacts({ destDir: 'dest-dir', name: 'artifact-dir', patterns: ['source-dir/'] }); 18 | 19 | const stats = fs.statSync('dest-dir/artifact-dir'); 20 | 21 | t.true(stats.isDirectory()); 22 | }); 23 | 24 | test('should create artifact relative dest-dir', async t => { 25 | mockFs({ 26 | 'source-dir': {} 27 | }); 28 | 29 | await writeArtifacts({ destDir: 'dest-dir' , dest: '../artifact-dir', patterns: ['source-dir/'] }); 30 | 31 | const files = fs.readdirSync('.'); 32 | 33 | t.deepEqual(files, ['artifact-dir', 'source-dir']); 34 | }); 35 | 36 | test('should create artifact in root/dest-dir', async t => { 37 | mockFs({ 38 | '/root/source-dir': {} 39 | }); 40 | 41 | await writeArtifacts({ root: '/root', destDir: 'dest-dir', name: 'artifact-dir', patterns: ['source-dir/'] }); 42 | 43 | const stats = fs.statSync('/root/dest-dir/artifact-dir'); 44 | 45 | t.true(stats.isDirectory()); 46 | }); 47 | 48 | test('should create artifact relative dest-dir in root', async t => { 49 | mockFs({ 50 | '/root/source-dir': {} 51 | }); 52 | 53 | await writeArtifacts({ root: '/root', destDir: 'dest-dir', dest: '../artifact-dir', patterns: ['source-dir/'] }); 54 | 55 | const stats = fs.statSync('/root/artifact-dir'); 56 | 57 | t.true(stats.isDirectory()); 58 | }); 59 | 60 | test('should create artifact in dest-dir by absolute path', async t => { 61 | mockFs({ 62 | '/root/source-dir': {} 63 | }); 64 | 65 | await writeArtifacts({ root: '/root', destDir: '/dest-dir', name: 'artifact-dir', patterns: ['source-dir/'] }); 66 | 67 | const stats = fs.statSync('/dest-dir/artifact-dir'); 68 | 69 | t.true(stats.isDirectory()); 70 | }); 71 | 72 | test('should create artifact by absolute dest path', async t => { 73 | mockFs({ 74 | '/root/source-dir': {} 75 | }); 76 | 77 | await writeArtifacts({ root: '/root', destDir: '/dest-dir', dest: '/artifact-dir', patterns: ['source-dir/'] }); 78 | 79 | const stats = fs.statSync('/artifact-dir'); 80 | 81 | t.true(stats.isDirectory()); 82 | }); 83 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/dest.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should create artifact dir by name', async t => { 13 | mockFs({ 14 | 'source-dir': {} 15 | }); 16 | 17 | await writeArtifacts({ name: 'artifact-dir', patterns: ['source-dir/'] }); 18 | 19 | const files = fs.readdirSync('artifact-dir'); 20 | 21 | t.deepEqual(files, ['source-dir']); 22 | }); 23 | 24 | test('should create dest dir by dest', async t => { 25 | mockFs({ 26 | 'source-dir': {} 27 | }); 28 | 29 | await writeArtifacts({ dest: 'dest-dir', patterns: ['source-dir/'] }); 30 | 31 | const files = fs.readdirSync('dest-dir'); 32 | 33 | t.deepEqual(files, ['source-dir']); 34 | }); 35 | 36 | test('should create dest dir from relative root', async t => { 37 | mockFs({ 38 | '/root/source-dir': {} 39 | }); 40 | 41 | await writeArtifacts({ root: '/root', dest: 'dest-dir', patterns: ['source-dir/'] }); 42 | 43 | const stats = fs.statSync('/root/dest-dir/'); 44 | 45 | t.true(stats.isDirectory()); 46 | }); 47 | 48 | test('should create dir by depth path', async t => { 49 | mockFs({ 50 | 'source-dir': {} 51 | }); 52 | 53 | await writeArtifacts({ dest: './path/to/dest-dir/', patterns: 'source-dir/' }); 54 | 55 | const stats = fs.statSync('./path/to/dest-dir/'); 56 | 57 | t.true(stats.isDirectory()); 58 | }); 59 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/dotfiles.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should include dot files by default', async t => { 13 | mockFs({ 14 | 'source-dir': { 15 | '.dot-file': 'bla', 16 | 'file-1.txt': 'Hi!' 17 | } 18 | }); 19 | 20 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }); 21 | 22 | const files = fs.readdirSync('./artifact-dir/source-dir'); 23 | 24 | t.deepEqual(files, ['.dot-file', 'file-1.txt']); 25 | }); 26 | 27 | test('should ignore dot files', async t => { 28 | mockFs({ 29 | 'source-dir': { 30 | '.dot-file': 'bla', 31 | 'file-1.txt': 'Hi!' 32 | } 33 | }); 34 | 35 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }, { dotFiles: false }); 36 | 37 | const files = fs.readdirSync('artifact-dir/source-dir'); 38 | 39 | t.deepEqual(files, ['file-1.txt']); 40 | }); 41 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/empty-dirs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should include empty dirs by default', async t => { 13 | mockFs({ 14 | 'source-dir': { 15 | 'empty-dir': {}, 16 | 'file-1.txt': 'Hi!' 17 | } 18 | }); 19 | 20 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }); 21 | 22 | const files = fs.readdirSync('./artifact-dir/source-dir'); 23 | 24 | t.deepEqual(files, ['empty-dir', 'file-1.txt']); 25 | }); 26 | 27 | test('should ignore empty dirs', async t => { 28 | mockFs({ 29 | 'source-dir': { 30 | 'empty-dir': {}, 31 | 'file-1.txt': 'Hi!' 32 | } 33 | }); 34 | 35 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }, { emptyDirs: false }); 36 | 37 | const files = fs.readdirSync('artifact-dir/source-dir'); 38 | 39 | t.deepEqual(files, ['file-1.txt']); 40 | }); 41 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/empty-files.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should include empty files by default', async t => { 13 | mockFs({ 14 | 'source-dir': { 15 | 'empty-file': '', 16 | 'file-1.txt': 'Hi!' 17 | } 18 | }); 19 | 20 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }); 21 | 22 | const files = fs.readdirSync('./artifact-dir/source-dir'); 23 | 24 | t.deepEqual(files, ['empty-file', 'file-1.txt']); 25 | }); 26 | 27 | test('should ignore empty files', async t => { 28 | mockFs({ 29 | 'source-dir': { 30 | 'empty-file': '', 31 | 'file-1.txt': 'Hi!' 32 | } 33 | }); 34 | 35 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }, { emptyFiles: false }); 36 | 37 | const files = fs.readdirSync('artifact-dir/source-dir'); 38 | 39 | t.deepEqual(files, ['file-1.txt']); 40 | }); 41 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/errors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const mockFs = require('mock-fs'); 5 | 6 | const writeArtifacts = require('../../../lib'); 7 | 8 | test.afterEach(() => mockFs.restore()); 9 | 10 | test('should throw error if artifact task has error', t => { 11 | t.throws( 12 | () => writeArtifacts({}), 13 | /Option "dest" or "name" must be specified for each artifact/ 14 | ); 15 | }); 16 | 17 | test('should throw error if include file does not exist', t => { 18 | mockFs({ 19 | 'source-dir': {} 20 | }); 21 | 22 | t.throws( 23 | writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/no-file.txt' }), 24 | /File not found/ 25 | ); 26 | }); 27 | 28 | test('should handle error from transform function', t => { 29 | mockFs({ 30 | 'source-dir': { 31 | 'file.txt': 'hello!' 32 | } 33 | }); 34 | 35 | const brokenTransform = () => { throw new Error('some error') }; 36 | 37 | t.throws( 38 | writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/file.txt', transform: brokenTransform }), 39 | /some error/ 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/patterns.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should include files', async t => { 13 | mockFs({ 14 | 'source-dir': { 15 | 'file-1.txt': 'Hi!', 16 | 'file-2.txt': 'Hello!' 17 | } 18 | }); 19 | 20 | await writeArtifacts({ name: 'artifact-dir', patterns: ['source-dir/**'] }); 21 | 22 | const files = fs.readdirSync('artifact-dir/source-dir'); 23 | 24 | t.deepEqual(files, ['file-1.txt', 'file-2.txt']); 25 | }); 26 | 27 | test('should exclude files', async t => { 28 | mockFs({ 29 | 'source-dir': { 30 | 'file-1.txt': 'Hi!', 31 | 'file-2.txt': 'Hello!' 32 | } 33 | }); 34 | 35 | await writeArtifacts({ name: 'artifact-dir', patterns: ['source-dir/**', '!source-dir/file-2.txt'] }); 36 | 37 | const files = fs.readdirSync('artifact-dir/source-dir'); 38 | 39 | t.deepEqual(files, ['file-1.txt']); 40 | }); 41 | 42 | test('should override negative pattern', async t => { 43 | mockFs({ 44 | 'source-dir': { 45 | 'file-1.txt': 'Hi!', 46 | 'file-2.txt': 'Hello!' 47 | } 48 | }); 49 | 50 | await writeArtifacts({ 51 | name: 'artifact-dir', 52 | patterns: [ 53 | 'source-dir/**', 54 | '!source-dir/file-2.txt', 55 | 'source-dir/file-2.txt' 56 | ] 57 | }); 58 | 59 | const files = fs.readdirSync('artifact-dir/source-dir'); 60 | 61 | t.deepEqual(files, ['file-1.txt', 'file-2.txt']); 62 | }); 63 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/symlink.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const test = require('ava'); 7 | const mockFs = require('mock-fs'); 8 | 9 | // const writeArtifacts = require('../../../lib/artifacts').writeArtifact; 10 | const writeArtifacts = require('../../../lib'); 11 | 12 | test.afterEach(() => mockFs.restore()); 13 | 14 | test('should include symlink to file by default', async t => { 15 | mockFs({ 16 | 'file.txt': 'Hi!', 17 | 'source-dir': { 18 | 'symlink.txt': mockFs.symlink({ 19 | path: path.join('..', 'file.txt') 20 | }) 21 | } 22 | }); 23 | 24 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }); 25 | 26 | const link = fs.readlinkSync(path.join('artifact-dir', 'source-dir', 'symlink.txt')); 27 | 28 | t.is(link, path.join('..', 'file.txt')); 29 | }); 30 | 31 | test('should include symlink to dir by default', async t => { 32 | mockFs({ 33 | 'dir': { 34 | 'file.txt': 'Hi!' 35 | }, 36 | 'source-dir': { 37 | 'symdir': mockFs.symlink({ 38 | path: path.join('..', 'dir') 39 | }) 40 | } 41 | }); 42 | 43 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }); 44 | 45 | const link = fs.readlinkSync(path.join('artifact-dir', 'source-dir', 'symdir')); 46 | 47 | t.is(link, path.join('..', 'dir')); 48 | }); 49 | 50 | test('should follow symlink to file', async t => { 51 | mockFs({ 52 | 'file.txt': 'Hi!', 53 | 'source-dir': { 54 | 'symlink.txt': mockFs.symlink({ 55 | path: path.join('..', 'file.txt') 56 | }) 57 | } 58 | }); 59 | 60 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }, { followSymlinks: true }); 61 | 62 | const contents = fs.readFileSync(path.join('artifact-dir', 'source-dir', 'symlink.txt'), 'utf-8'); 63 | 64 | t.is(contents, 'Hi!'); 65 | }); 66 | 67 | test('should follow symlink to dir', async t => { 68 | mockFs({ 69 | 'dir': { 70 | 'file.txt': 'Hi!' 71 | }, 72 | 'source-dir': { 73 | 'symdir': mockFs.symlink({ 74 | path: path.join('..', 'dir') 75 | }) 76 | } 77 | }); 78 | 79 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/**' }, { followSymlinks: true }); 80 | 81 | const contents = fs.readFileSync(path.join('artifact-dir', 'source-dir', 'symdir', 'file.txt'), 'utf-8'); 82 | 83 | t.is(contents, 'Hi!'); 84 | }); 85 | -------------------------------------------------------------------------------- /test/artifacts/write-artifact/tarball.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | const isTar = require('is-tar'); 8 | const isGzip = require('is-gzip'); 9 | 10 | const writeArtifacts = require('../../../lib'); 11 | 12 | test.afterEach(() => mockFs.restore()); 13 | 14 | test('should pack to tarball', async t => { 15 | mockFs({ 16 | 'source-dir': { 17 | 'file-1.txt': 'Hi!', 18 | 'file-2.txt': 'Hello!' 19 | } 20 | }); 21 | 22 | await writeArtifacts({ name: 'artifact.tar', patterns: 'source-dir/**' }, { tar: true }); 23 | 24 | const archive = fs.readFileSync('artifact.tar'); 25 | 26 | t.true(isTar(archive)); 27 | }); 28 | 29 | test('should pack to tarball with gzip', async t => { 30 | mockFs({ 31 | 'source-dir': { 32 | 'file-1.txt': 'Hi!', 33 | 'file-2.txt': 'Hello!' 34 | } 35 | }); 36 | 37 | await writeArtifacts({ name: 'artifact.tar.gz', patterns: 'source-dir/**'}, { tar: true, gzip: true }); 38 | 39 | const archive = fs.readFileSync('artifact.tar.gz'); 40 | 41 | t.true(isGzip(archive)); 42 | }); 43 | 44 | test('should pack to tarball with gzip using gzip options', async t => { 45 | mockFs({ 46 | 'source-dir': { 47 | 'file-1.txt': 'Hi!', 48 | 'file-2.txt': 'Hello!' 49 | } 50 | }); 51 | 52 | await writeArtifacts({ name: 'artifact.tar.gz', patterns: 'source-dir/**' }, { tar: true, gzip: { level: 1 } }); 53 | 54 | const gz = fs.readFileSync('artifact.tar.gz'); 55 | 56 | t.true(isGzip(gz)); 57 | }); 58 | -------------------------------------------------------------------------------- /test/artifacts/write-artifacts.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const test = require('ava'); 6 | const mockFs = require('mock-fs'); 7 | 8 | const writeArtifacts = require('../../lib'); 9 | 10 | test.afterEach(() => mockFs.restore()); 11 | 12 | test('should not create artifact', async () => { 13 | mockFs({ 14 | 'source-dir': {} 15 | }); 16 | 17 | await writeArtifacts(); 18 | }); 19 | 20 | test('should create artifact', async t => { 21 | mockFs({ 22 | 'source-dir': {} 23 | }); 24 | 25 | await writeArtifacts({ name: 'artifact-dir', patterns: 'source-dir/' }); 26 | 27 | const files = fs.readdirSync('artifact-dir'); 28 | 29 | t.deepEqual(files, ['source-dir']); 30 | }); 31 | 32 | test('should create artifacts', async t => { 33 | mockFs({ 34 | 'source-dir1': {}, 35 | 'source-dir2': {} 36 | }); 37 | 38 | await writeArtifacts([ 39 | { name: 'artifact-dir1', patterns: 'source-dir1/' }, 40 | { name: 'artifact-dir2', patterns: 'source-dir2/' } 41 | ]); 42 | 43 | const files1 = fs.readdirSync('artifact-dir1'); 44 | const files2 = fs.readdirSync('artifact-dir2'); 45 | 46 | t.deepEqual(files1, ['source-dir1']); 47 | t.deepEqual(files2, ['source-dir2']); 48 | }); 49 | 50 | test('should create artifacts consider options', async t => { 51 | mockFs({ 52 | 'source-dir1': { 53 | '.dot-file': 'bla', 54 | 'file-1.txt': 'Hi!' 55 | }, 56 | 'source-dir2': { 57 | '.dot-file': 'bla', 58 | 'file-2.txt': 'Hi!' 59 | } 60 | }); 61 | 62 | await writeArtifacts([ 63 | { name: 'artifact-dir1', patterns: 'source-dir1/**' }, 64 | { name: 'artifact-dir2', patterns: 'source-dir2/**' } 65 | ], { dotFiles: false }); 66 | 67 | const files1 = fs.readdirSync('./artifact-dir1/source-dir1'); 68 | const files2 = fs.readdirSync('./artifact-dir2/source-dir2'); 69 | 70 | t.deepEqual(files1, ['file-1.txt']); 71 | t.deepEqual(files2, ['file-2.txt']); 72 | }); 73 | -------------------------------------------------------------------------------- /test/fs/copy-symlink.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const test = require('ava'); 7 | const mockFs = require('mock-fs'); 8 | 9 | const copySymlink = require('../../lib/fs').copySymlink; 10 | 11 | test.afterEach(() => mockFs.restore()); 12 | 13 | test('should throw error if file is not found', t => { 14 | mockFs({}); 15 | 16 | t.throws(copySymlink('file.txt', 'symlink2.txt')); 17 | }); 18 | 19 | test('should throw error if file is not symlink', async t => { 20 | mockFs({ 21 | 'file.txt': '' 22 | }); 23 | 24 | t.throws(copySymlink('file.txt', 'symlink2.txt')); 25 | }); 26 | 27 | test('should create symlink', async t => { 28 | mockFs({ 29 | 'file.txt': '', 30 | 'symlink.txt': mockFs.symlink({ 31 | path: 'file.txt' 32 | }) 33 | }); 34 | 35 | await copySymlink('symlink.txt', 'symlink2.txt'); 36 | 37 | t.is(fs.readlinkSync('symlink2.txt'), 'file.txt'); 38 | }); 39 | 40 | test('should create dir', async t => { 41 | mockFs({ 42 | 'file.txt': '', 43 | 'symlink.txt': mockFs.symlink({ 44 | path: 'file.txt' 45 | }) 46 | }); 47 | 48 | await copySymlink('symlink.txt', path.join('dir', 'symlink2.txt')); 49 | 50 | t.deepEqual(fs.readdirSync('dir'), ['symlink2.txt']); 51 | }); 52 | 53 | test('should not create dir if already exists', async t => { 54 | mockFs({ 55 | 'file.txt': '', 56 | 'symlink.txt': mockFs.symlink({ 57 | path: 'file.txt' 58 | }), 59 | 'dir': {} 60 | }); 61 | 62 | await copySymlink('symlink.txt', path.join('dir', 'symlink2.txt')); 63 | 64 | t.deepEqual(fs.readdirSync('dir'), ['symlink2.txt']); 65 | }); 66 | 67 | test('should not change target path', async t => { 68 | mockFs({ 69 | 'file.txt': '', 70 | 'symlink.txt': mockFs.symlink({ 71 | path: 'file.txt' 72 | }) 73 | }); 74 | 75 | await copySymlink('symlink.txt', path.join('dir', 'symlink2.txt')); 76 | 77 | t.is(fs.readlinkSync(path.join('dir','symlink2.txt')), 'file.txt'); 78 | }); 79 | 80 | test('should use specified fs', async t => { 81 | mockFs({ 82 | 'file.txt': '', 83 | 'symlink.txt': mockFs.symlink({ 84 | path: 'file.txt' 85 | }) 86 | }); 87 | 88 | const userFs = { 89 | readlink: (file, cb) => cb(null, 'fake_target'), 90 | symlink: fs.symlink, 91 | mkdir: fs.mkdir, 92 | stat: fs.stat 93 | }; 94 | 95 | await copySymlink('symlink.txt', 'symlink2.txt', { fs: userFs }); 96 | 97 | t.is(fs.readlinkSync('symlink2.txt'), 'fake_target'); 98 | }); 99 | -------------------------------------------------------------------------------- /test/options/defaults.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const config = require('../../lib/config'); 6 | 7 | const formatOptions = (options) => config.format(Object.assign({ name: 'tartifact', patterns: ['lib/**'] }, options)); 8 | 9 | test('should format options with falsey "tar" option by default', t => { 10 | const options = formatOptions(); 11 | 12 | t.false(options.tar); 13 | }); 14 | 15 | test('should format options with falsey "gzip" option by default', t => { 16 | const options = formatOptions({ tar: true }); 17 | 18 | t.false(options.gzip); 19 | }); 20 | 21 | test('should format options with default "gzipOptions" option', t => { 22 | const options = formatOptions({ tar: true, gzip: true }); 23 | 24 | t.true(options.gzip); 25 | t.deepEqual(options.gzipOptions, { level: 1 }); 26 | }); 27 | 28 | test('should format options with falsey "followSymlinks" option by default', t => { 29 | const options = formatOptions(); 30 | 31 | t.false(options.followSymlinks); 32 | }); 33 | 34 | test('should format options with truthy "dotFiles" option by default', t => { 35 | const options = formatOptions(); 36 | 37 | t.true(options.dotFiles); 38 | }); 39 | 40 | test('should format options with truthy "emptyFiles" option by default', t => { 41 | const options = formatOptions(); 42 | 43 | t.true(options.emptyFiles); 44 | }); 45 | 46 | test('should format options with truthy "emptyDirs" option by default', t => { 47 | const options = formatOptions(); 48 | 49 | t.true(options.emptyDirs); 50 | }); 51 | 52 | test('should format options without "transform" option by default', t => { 53 | const options = formatOptions(); 54 | 55 | t.is(options.transform, null); 56 | }); 57 | 58 | test('should format options with falsey "watch" options by default', t => { 59 | const options = formatOptions(); 60 | 61 | t.false(options.watch); 62 | }); 63 | -------------------------------------------------------------------------------- /test/options/handle-errors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const config = require('../../lib/config'); 6 | 7 | test('should throw error if dest path is not specified', t => { 8 | t.throws( 9 | () => config.format({ patterns: ['lib/**'] }), 10 | 'Option "dest" or "name" must be specified for each artifact' 11 | ); 12 | }); 13 | 14 | test('should not throw error if name is specified instead of dest', t => { 15 | t.notThrows( 16 | () => config.format({ name: 'tartifact', patterns: ['lib/**'] }) 17 | ); 18 | }); 19 | 20 | test('should throw error if patterns is not specified', t => { 21 | t.throws( 22 | () => config.format({ dest: './dest-dir' }), 23 | 'you should specify the includes or patterns parameters for artifact.' 24 | ); 25 | }); 26 | 27 | test('should not throw error if includes is specified instead of patterns', t => { 28 | t.notThrows( 29 | () => config.format({ dest: './dest-dir', includes: ['lib/**'] }) 30 | ); 31 | }); 32 | 33 | test('should throw error if patterns have errors', t => { 34 | t.throws( 35 | () => config.format({ dest: './dest-dir', patterns: ['!exlib/**'] }), 36 | 'the first pattern of artifact should not be negative.' 37 | ); 38 | }); 39 | 40 | test('should throw error if not archive with gzip', t => { 41 | t.throws( 42 | () => config.format({ dest: './dest-dir', patterns: ['lib/**'] }, { gzip: true }), 43 | 'Option "gzip" must be used only with option "tar"' 44 | ); 45 | }); 46 | 47 | test('should throw error if transform has non-function type', t => { 48 | t.throws( 49 | () => config.format({ dest: './dest-dir', patterns: ['lib/**'], transform: [() => { }, 'hello'] }), 50 | 'Option "transform" must be a function' 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /test/options/options.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const test = require('ava'); 5 | 6 | const config = require('../../lib/config'); 7 | 8 | const root = path.resolve('/root'); 9 | 10 | const formatOptions = (options) => config.format(Object.assign({ name: 'tartifact', patterns: ['lib/**'] }, options)); 11 | 12 | test('should format options with absolute root', t => { 13 | const options = formatOptions({ name: 'tartifact', patterns: ['lib/**'], root }); 14 | 15 | t.is(options.root, path.resolve('/root')); 16 | }); 17 | 18 | test('should format options with relative root', t => { 19 | const options = formatOptions({ name: 'tartifact', patterns: ['lib/**'], root: './root' }); 20 | 21 | t.is(options.root, path.join(process.cwd(), './root')); 22 | }); 23 | 24 | test('should format options with specified name', t => { 25 | const options = formatOptions({ name: 'tartifact', patterns: ['lib/**'], root }); 26 | 27 | t.is(options.name, 'tartifact'); 28 | }); 29 | 30 | test('should build path of artifact by name', t => { 31 | const options = formatOptions({ name: 'tartifact', patterns: ['lib/**'], root }); 32 | 33 | t.is(options.path, path.resolve('/root/tartifact')); 34 | }); 35 | 36 | test('should build path by relative dest', t => { 37 | const options = formatOptions({ dest: '../tartifact', patterns: ['lib/**'], root }); 38 | 39 | t.is(options.path, path.resolve('/tartifact')); 40 | }); 41 | 42 | test('should build path by absolute dest', t => { 43 | const options = formatOptions({ dest: path.resolve('/tartifact'), patterns: ['lib/**'], root }); 44 | 45 | t.is(options.path, path.resolve('/tartifact')); 46 | }); 47 | 48 | test('should build path of artifact by name and relative dest dir', t => { 49 | const options = formatOptions({ name: 'tartifact', destDir: 'dest', patterns: ['lib/**'], root }); 50 | 51 | t.is(options.path, path.resolve('/root/dest/tartifact')); 52 | }); 53 | 54 | test('should build path of artifact by name and absolute dest dir', t => { 55 | const options = formatOptions({ name: 'tartifact', destDir: path.resolve('/dest'), patterns: ['lib/**'], root }); 56 | 57 | t.is(options.path, path.resolve('/dest/tartifact')); 58 | }); 59 | 60 | test('should build path by relative dest and dest dir', t => { 61 | const options = formatOptions({ dest: '../tartifact', destDir: 'dest', patterns: ['lib/**'], root }); 62 | 63 | t.is(options.path, path.resolve('/root/tartifact')); 64 | }); 65 | 66 | test('should build path by absolute dest and dest dir', t => { 67 | const options = formatOptions({ dest: path.resolve('/tartifact'), destDir: 'dest', patterns: ['lib/**'], root }); 68 | 69 | t.is(options.path, path.resolve('/tartifact')); 70 | }); 71 | 72 | test('should build patterns', t => { 73 | const options = formatOptions({ name: 'tartifact', patterns: 'lib/**', includes: ['test/**'], excludes: ['exlib/**'] }); 74 | 75 | t.deepEqual(options.patterns, { '.': ['lib/**', 'test/**', '!exlib/**'] }); 76 | }); 77 | 78 | test('should format options with falsey "tar" option', t => { 79 | const options = formatOptions({ tar: false }); 80 | 81 | t.false(options.tar); 82 | }); 83 | 84 | test('should format options with falsey "gzip" option', t => { 85 | const options = formatOptions({ tar: true, gzip: false }); 86 | 87 | t.false(options.gzip); 88 | }); 89 | 90 | test('should format options with falsey "followSymlinks" option', t => { 91 | const options = formatOptions({ followSymlinks: false }); 92 | 93 | t.false(options.followSymlinks); 94 | }); 95 | 96 | test('should format options with falsey "dotFiles" option', t => { 97 | const options = formatOptions({ dotFiles: false }); 98 | 99 | t.false(options.dotFiles); 100 | }); 101 | 102 | test('should format options with falsey "emptyFiles" option', t => { 103 | const options = formatOptions({ emptyFiles: false }); 104 | 105 | t.false(options.emptyFiles); 106 | }); 107 | 108 | test('should format options with falsey "emptyDirs" option', t => { 109 | const options = formatOptions({ emptyDirs: false }); 110 | 111 | t.false(options.emptyDirs); 112 | }); 113 | 114 | test('should format options with falsey "watch" options', t => { 115 | const options = formatOptions({ watch: false }); 116 | 117 | t.false(options.watch); 118 | }); 119 | 120 | test('should format options with truthy "tar" option', t => { 121 | const options = formatOptions({ tar: true }); 122 | 123 | t.true(options.tar); 124 | }); 125 | 126 | test('should format options with truthy "gzip" option', t => { 127 | const options = formatOptions({ tar: true, gzip: true }); 128 | 129 | t.true(options.gzip); 130 | }); 131 | 132 | test('should format options with "gzipOptions option"', t => { 133 | const options = formatOptions({ tar: true, gzip: { level: 10 } }); 134 | 135 | t.true(options.gzip); 136 | t.deepEqual(options.gzipOptions, { level: 10 }); 137 | }); 138 | 139 | test('should format options with truthy "followSymlinks" option', t => { 140 | const options = formatOptions({ followSymlinks: true }); 141 | 142 | t.true(options.followSymlinks); 143 | }); 144 | 145 | test('should format options with truthy "dotFiles" option', t => { 146 | const options = formatOptions({ dotFiles: true }); 147 | 148 | t.true(options.dotFiles); 149 | }); 150 | 151 | test('should format options with falsey "emptyFiles" option', t => { 152 | const options = formatOptions({ emptyFiles: true }); 153 | 154 | t.true(options.emptyFiles); 155 | }); 156 | 157 | test('should format options with falsey "emptyDirs" option', t => { 158 | const options = formatOptions({ emptyDirs: true }); 159 | 160 | t.true(options.emptyDirs); 161 | }); 162 | 163 | test('should format options with "transform" option', t => { 164 | const func = () => {}; 165 | const options = formatOptions({ transform: func }); 166 | 167 | t.is(options.transform, func); 168 | }); 169 | 170 | test('should format options with falsey "watch" options', t => { 171 | const options = formatOptions({ watch: true }); 172 | 173 | t.true(options.watch); 174 | }); 175 | -------------------------------------------------------------------------------- /test/options/override-options.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const config = require('../../lib/config'); 6 | 7 | const formatOptions = (globalOptions, customOptions) => { 8 | return config.format(globalOptions, Object.assign({ name: 'tartifact', patterns: ['lib/**'] }, customOptions)); 9 | }; 10 | 11 | test('should override global "tar" options by custom one', t => { 12 | const options = formatOptions({ tar: true }, { tar: false }); 13 | 14 | t.true(options.tar); 15 | }); 16 | 17 | test('should override global "gzip" option by custom one', t => { 18 | const options = formatOptions({ tar: true, gzip: true }, { tar: false, gzip: false }); 19 | 20 | t.true(options.gzip); 21 | }); 22 | 23 | test('should use custom "gzipOptions"', t => { 24 | const options = formatOptions({ tar: true, gzip: { level: 10 } }, { tar: true, gzip: true }); 25 | 26 | t.true(options.gzip); 27 | t.deepEqual(options.gzipOptions, { level: 10 }); 28 | }); 29 | 30 | test('should override global "followSymlinks" option by custom one', t => { 31 | const options = formatOptions({ followSymlinks: true }, { followSymlinks: false }); 32 | 33 | t.true(options.followSymlinks); 34 | }); 35 | 36 | test('should override global "dotFiles" option by custom one', t => { 37 | const options = formatOptions({ dotFiles: true }, { dotFiles: false }); 38 | 39 | t.true(options.dotFiles); 40 | }); 41 | 42 | test('should override global "emptyFiles" option by custom one', t => { 43 | const options = formatOptions({ emptyFiles: true }, { emptyFiles: false }); 44 | 45 | t.true(options.emptyFiles); 46 | }); 47 | 48 | test('should override global "emptyDirs" option by custom one', t => { 49 | const options = formatOptions({ emptyDirs: true }, { emptyDirs: false }); 50 | 51 | t.true(options.emptyDirs); 52 | }); 53 | 54 | test('should override global "watch" option by custom one', t => { 55 | const options = formatOptions({ watch: true }, { watch: false }); 56 | 57 | t.true(options.watch); 58 | }); 59 | -------------------------------------------------------------------------------- /test/patterns/compose/compose-array-notation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const composePatterns = require('../../../lib/patterns').compose; 6 | 7 | test('should merge includes with source patterns', t => { 8 | const patterns = composePatterns(['source-dir/**'], { include: ['sources/exlib/**'] }); 9 | 10 | t.deepEqual(patterns, { '.': ['source-dir/**', 'sources/exlib/**'] }); 11 | }); 12 | 13 | test('should merge excludes with patterns', t => { 14 | const patterns = composePatterns(['source-dir/**'], { exclude: ['sources/exlib/**'] }); 15 | 16 | t.deepEqual(patterns, { '.': ['source-dir/**', '!sources/exlib/**'] }); 17 | }); 18 | 19 | test('should support negative patterns', t => { 20 | const patterns = composePatterns(['source-dir/**', '!sources/exlib/**']); 21 | 22 | t.deepEqual(patterns, { '.': ['source-dir/**', '!sources/exlib/**'] }); 23 | }); 24 | 25 | test('should support patterns as string', t => { 26 | const patterns = composePatterns('source-dir/**'); 27 | 28 | t.deepEqual(patterns, { '.': ['source-dir/**'] }); 29 | }); 30 | 31 | test('should support includes as string', async t => { 32 | const patterns = composePatterns([], { include: 'source-dir/**' }); 33 | 34 | t.deepEqual(patterns, { '.': ['source-dir/**'] }); 35 | }); 36 | 37 | test('should support excludes as string', async t => { 38 | const patterns = composePatterns(['source-dir/**'], { exclude: 'source-dir/file-2.txt' }); 39 | 40 | t.deepEqual(patterns, { '.': ['source-dir/**', '!source-dir/file-2.txt'] }); 41 | }); 42 | 43 | test('should support excludes with negative patterns', async t => { 44 | const patterns = composePatterns(['source-dir/**'], { exclude: '!source-dir/file-2.txt' }); 45 | 46 | t.deepEqual(patterns, { '.': ['source-dir/**', '!source-dir/file-2.txt'] }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/patterns/compose/compose-object-notation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const composePatterns = require('../../../lib/patterns').compose; 6 | 7 | test('should merge includes with source patterns', t => { 8 | const patterns = composePatterns({ 9 | './subdir1': ['source-dir/**'], 10 | './subdir2': ['source-dir/**'] 11 | }, { include: ['sources/exlib/**'] }); 12 | 13 | t.deepEqual(patterns, { 14 | './subdir1': ['source-dir/**', 'sources/exlib/**'], 15 | './subdir2': ['source-dir/**', 'sources/exlib/**'] 16 | }); 17 | }); 18 | 19 | test('should merge excludes with patterns', t => { 20 | const patterns = composePatterns({ 21 | './subdir1': ['source-dir/**'], 22 | './subdir2': ['source-dir/**'] 23 | }, { exclude: ['sources/exlib/**'] }); 24 | 25 | t.deepEqual(patterns, { 26 | './subdir1': ['source-dir/**', '!sources/exlib/**'], 27 | './subdir2': ['source-dir/**', '!sources/exlib/**'] 28 | }); 29 | }); 30 | 31 | test('should support negative patterns', t => { 32 | const patterns = composePatterns({ 33 | './subdir1': ['source-dir/**', '!sources/exlib/**'], 34 | './subdir2': ['source-dir/**', '!sources/exlib/**'] 35 | }); 36 | 37 | t.deepEqual(patterns, { 38 | './subdir1': ['source-dir/**', '!sources/exlib/**'], 39 | './subdir2': ['source-dir/**', '!sources/exlib/**'] 40 | }); 41 | }); 42 | 43 | test('should support includes as string', async t => { 44 | const patterns = composePatterns({ 45 | './subdir1': [], 46 | './subdir2': [] 47 | }, { include: 'source-dir/**' }); 48 | 49 | t.deepEqual(patterns, { './subdir1': ['source-dir/**'], './subdir2': ['source-dir/**'] }); 50 | }); 51 | 52 | test('should support excludes as string', async t => { 53 | const patterns = composePatterns({ 54 | './subdir1': ['source-dir/**'], 55 | './subdir2': ['source-dir/**'] 56 | }, { exclude: 'source-dir/file-2.txt' }); 57 | 58 | t.deepEqual(patterns, { 59 | './subdir1': ['source-dir/**', '!source-dir/file-2.txt'], 60 | './subdir2': ['source-dir/**', '!source-dir/file-2.txt'] 61 | }); 62 | }); 63 | 64 | test('should support excludes with negative patterns', async t => { 65 | const patterns = composePatterns({ 66 | './subdir1': ['source-dir/**'], 67 | './subdir2': ['source-dir/**'] 68 | }, { exclude: '!source-dir/file-2.txt' }); 69 | 70 | t.deepEqual(patterns, { 71 | './subdir1': ['source-dir/**', '!source-dir/file-2.txt'], 72 | './subdir2': ['source-dir/**', '!source-dir/file-2.txt'] 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/patterns/compose/validate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const composePatterns = require('../../../lib/patterns').compose; 6 | 7 | test('should throw error if include or patterns param is not specified', t => { 8 | t.throws( 9 | () => composePatterns(), 10 | 'you should specify the includes or patterns parameters for artifact.' 11 | ); 12 | }); 13 | 14 | test('should throw error if includes param is negative patterns', t => { 15 | t.throws( 16 | () => composePatterns([], { include: '!source-dir/**' }), 17 | 'the includes parameter of artifact should not contains negative patterns.' 18 | ); 19 | }); 20 | 21 | test('should throw error if includes has negative patterns', t => { 22 | t.throws( 23 | () => composePatterns([], { include: ['source-dir/file-1.txt', '!source-dir/file-2.txt'] }), 24 | 'the includes parameter of artifact should not contains negative patterns.' 25 | ); 26 | }); 27 | 28 | test('should throw error if patterns param is negative patterns', t => { 29 | t.throws( 30 | () => composePatterns('!source-dir/**'), 31 | 'the first pattern of artifact should not be negative.' 32 | ); 33 | }); 34 | 35 | test('should throw error if first pattern is negative glob', t => { 36 | t.throws( 37 | () => composePatterns(['!source-dir/**', 'source-dir/file-1.txt']), 38 | 'the first pattern of artifact should not be negative.' 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /test/streams/copy-stream.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const test = require('ava'); 7 | const mockFs = require('mock-fs'); 8 | const streamify = require('stream-array'); 9 | 10 | const CopyStream = require('../../lib/streams').CopyStream; 11 | 12 | const root = path.resolve('source-dir'); 13 | const dest = path.resolve('dest-dir'); 14 | 15 | test.afterEach(() => mockFs.restore()); 16 | 17 | test('should not copy empty dir', async t => { 18 | mockFs({ 19 | 'source-dir': {} 20 | }); 21 | 22 | await copyFiles([]); 23 | 24 | const dirs = fs.readdirSync('./'); 25 | 26 | t.deepEqual(dirs, ['source-dir']); 27 | }); 28 | 29 | test('should copy files', async t => { 30 | mockFs({ 31 | 'source-dir': { 32 | 'file-1.txt': 'Hi!', 33 | 'file-2.txt': 'Hello!' 34 | } 35 | }); 36 | 37 | await copyFiles(['file-1.txt', 'file-2.txt']); 38 | 39 | const files = fs.readdirSync(path.join(dest)); 40 | 41 | t.deepEqual(files, ['file-1.txt', 'file-2.txt']); 42 | }); 43 | 44 | test('should copy file with contents', async t => { 45 | mockFs({ 46 | 'source-dir': { 47 | 'file-1.txt': 'Hi!' 48 | } 49 | }); 50 | 51 | await copyFiles(['file-1.txt']); 52 | 53 | const str = fs.readFileSync(path.join(dest, 'file-1.txt'), 'utf-8'); 54 | 55 | t.is(str, 'Hi!'); 56 | }); 57 | 58 | test('should copy dir with subdirs', async t => { 59 | mockFs({ 60 | 'source-dir': { 61 | 'sub-dir': { 62 | 'file-1.txt': 'Hi!', 63 | 'file-2.txt': 'Hello!' 64 | } 65 | } 66 | }); 67 | 68 | await copyFiles(['sub-dir/file-1.txt', 'sub-dir/file-2.txt']); 69 | 70 | const files = fs.readdirSync(path.join(dest, 'sub-dir')); 71 | 72 | t.deepEqual(files, ['file-1.txt', 'file-2.txt']); 73 | }); 74 | 75 | test('should copy directory without files', async t => { 76 | mockFs({ 77 | 'source-dir': { 78 | 'sub-dir': { 'file-2.txt': 'Hello!' } 79 | } 80 | }); 81 | 82 | await copyFiles(['sub-dir/']); 83 | 84 | const files = fs.readdirSync(path.join(dest)); 85 | 86 | t.deepEqual(files, ['sub-dir']); 87 | }); 88 | 89 | test('should ignore directory without files', async t => { 90 | mockFs({ 91 | 'source-dir': { 92 | 'file-1.txt': 'Hi!', 93 | 'sub-dir': { 'file-2.txt': 'Hello!' } 94 | } 95 | }); 96 | 97 | await copyFiles(['file-1.txt', 'sub-dir/'], { emptyDirs: false }); 98 | 99 | const files = fs.readdirSync(path.join(dest)); 100 | 101 | t.deepEqual(files, ['file-1.txt']); 102 | }); 103 | 104 | test('should copy symlink to file', async t => { 105 | mockFs({ 106 | 'file-1.txt': 'Hi!', 107 | 'source-dir': { 108 | 'symlink.txt': mockFs.symlink({ 109 | path: path.join('..', 'file-1.txt') 110 | }) 111 | } 112 | }); 113 | 114 | await copyFiles(['symlink.txt']); 115 | 116 | const link = fs.readlinkSync(path.join(dest, 'symlink.txt')); 117 | 118 | t.is(link, path.join('..', 'file-1.txt')); 119 | }); 120 | 121 | test('should copy symlink to dir if emptyDirs is false', async t => { 122 | mockFs({ 123 | 'dir': { 124 | 'file-1.txt': 'Hi!' 125 | }, 126 | 'source-dir': { 127 | 'symdir': mockFs.symlink({ 128 | path: path.join('..', 'dir') 129 | }) 130 | } 131 | }); 132 | 133 | await copyFiles(['symdir/'], { emptyDirs: false }); 134 | 135 | const link = fs.readlinkSync(path.join(dest, 'symdir')); 136 | 137 | t.is(link, path.join('..', 'dir')); 138 | }); 139 | 140 | test('should copy symlink to dir if emptyDirs is true', async t => { 141 | mockFs({ 142 | 'dir': { 143 | 'file-1.txt': 'Hi!' 144 | }, 145 | 'source-dir': { 146 | 'symdir': mockFs.symlink({ 147 | path: path.join('..', 'dir') 148 | }) 149 | } 150 | }); 151 | 152 | await copyFiles(['symdir/'], { emptyDirs: true }); 153 | 154 | const link = fs.readlinkSync(path.join(dest, 'symdir')); 155 | 156 | t.is(link, path.join('..', 'dir')); 157 | }); 158 | 159 | test('should copy broken symlinks', async t => { 160 | mockFs({ 161 | 'source-dir': { 162 | 'symlink.txt': mockFs.symlink({ 163 | path: 'no-file' 164 | }) 165 | } 166 | }); 167 | 168 | await copyFiles(['symlink.txt']); 169 | 170 | const link = fs.readlinkSync(path.join(dest, 'symlink.txt')); 171 | 172 | t.is(link, 'no-file'); 173 | }); 174 | 175 | test('should copy empty file', async t => { 176 | mockFs({ 177 | 'source-dir': { 178 | 'empty-file.txt': mockFs.file(), 179 | 'file-1.txt': 'Hi!' 180 | } 181 | }); 182 | 183 | await copyFiles(['empty-file.txt', 'file-1.txt']); 184 | 185 | const files = fs.readdirSync(path.join(dest)); 186 | 187 | t.deepEqual(files, ['empty-file.txt', 'file-1.txt']); 188 | }); 189 | 190 | test('should ignore empty file', async t => { 191 | mockFs({ 192 | 'source-dir': { 193 | 'file-1.txt': 'Hi!', 194 | 'empty-file.txt': mockFs.file() 195 | } 196 | }); 197 | 198 | await copyFiles(['file-1.txt', 'empty-file.txt'], { emptyFiles: false }); 199 | 200 | const files = fs.readdirSync(path.join(dest)); 201 | 202 | t.deepEqual(files, ['file-1.txt']); 203 | }); 204 | 205 | test('should emit error if file file does not exist', t => { 206 | mockFs({ 207 | 'source-dir': {} 208 | }); 209 | 210 | t.throws(copyFiles(['no-file.txt']), /no such file or directory/); 211 | }); 212 | 213 | function copyFiles(filenames, options) { 214 | return new Promise((resolve, reject) => { 215 | findFiles(filenames) 216 | .on('error', reject) 217 | .pipe(new CopyStream(dest, options)) 218 | .on('error', reject) 219 | .on('finish', resolve); 220 | }); 221 | } 222 | 223 | function findFiles(filenames) { 224 | const files = filenames.map(basename => { 225 | const filePath = path.join(root, basename); 226 | 227 | return { 228 | path: filePath, 229 | base: root, 230 | cwd: root, 231 | history: [filePath], 232 | lstats: fs.lstatSync(filePath), 233 | subdir: '.' 234 | }; 235 | }); 236 | 237 | return streamify(files); 238 | } 239 | -------------------------------------------------------------------------------- /test/streams/tar-stream.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const os = require('os'); 6 | 7 | const test = require('ava'); 8 | const mockFs = require('mock-fs'); 9 | const streamify = require('stream-array'); 10 | const tar = require('tar'); 11 | 12 | const unixOnly = os.platform() !== 'win32'; 13 | 14 | const TarStream = require('../../lib/streams').TarStream; 15 | 16 | const root = path.resolve('source-dir'); 17 | const dest = path.resolve('dest.tar'); 18 | const resdir = path.resolve('res-dir'); 19 | 20 | test.afterEach(() => mockFs.restore()); 21 | 22 | test('should create tarball with files', async t => { 23 | mockFs({ 24 | 'source-dir': { 25 | 'file-1.txt': 'Hi!', 26 | 'file-2.txt': 'Hello!' 27 | } 28 | }); 29 | 30 | await packFiles(['file-1.txt', 'file-2.txt']); 31 | 32 | const files = await parseFiles(); 33 | 34 | t.deepEqual(files.map(file => file.path), ['file-1.txt', 'file-2.txt']); 35 | }); 36 | 37 | test('should take into account contents of file', async t => { 38 | mockFs({ 39 | 'source-dir': { 40 | 'file-1.txt': 'Hi!' 41 | } 42 | }); 43 | 44 | await packFiles(['file-1.txt']); 45 | 46 | const files = await parseFiles(); 47 | 48 | t.deepEqual(files.map(file => file.contents), ['Hi!']); 49 | }); 50 | 51 | test('should create tarball with subdirs', async t => { 52 | mockFs({ 53 | 'source-dir': { 54 | 'sub-dir': { 55 | 'file-1.txt': 'Hi!', 56 | 'file-2.txt': 'Hello!' 57 | } 58 | } 59 | }); 60 | 61 | await packFiles(['sub-dir/file-1.txt', 'sub-dir/file-2.txt']); 62 | 63 | const files = await parseFiles(); 64 | 65 | t.deepEqual(files.map(file => file.path), ['sub-dir/file-1.txt', 'sub-dir/file-2.txt']); 66 | }); 67 | 68 | test('should include directory without files', async t => { 69 | mockFs({ 70 | 'source-dir': { 71 | 'sub-dir': { 'file-2.txt': 'Hello!' } 72 | } 73 | }); 74 | 75 | await packFiles(['sub-dir/']); 76 | 77 | const files = await parseFiles(); 78 | 79 | t.deepEqual(files.map(file => file.path), ['sub-dir/']); 80 | }); 81 | 82 | test('should ignore directory without files', async t => { 83 | mockFs({ 84 | 'source-dir': { 85 | 'sub-dir': { 'file-2.txt': 'Hello!' } 86 | } 87 | }); 88 | 89 | await packFiles(['sub-dir/'], { emptyDirs: false }); 90 | 91 | const files = await parseFiles(); 92 | 93 | t.deepEqual(files.map(file => file.path), []); 94 | }); 95 | 96 | test('should take into account symlink to file', async t => { 97 | mockFs({ 98 | 'file-1.txt': 'Hi!', 99 | 'source-dir': { 100 | 'symlink.txt': mockFs.symlink({ 101 | path: '../file-1.txt' 102 | }) 103 | } 104 | }); 105 | 106 | await packFiles(['symlink.txt']); 107 | 108 | const files = await parseFiles(resdir); 109 | 110 | t.deepEqual(files, [{ path: 'symlink.txt', mode: 0o644, contents: null, linkpath: '../file-1.txt' }]); 111 | }); 112 | 113 | test('should take into account symlink to dir if emptyDirs is true', async t => { 114 | mockFs({ 115 | 'dir': { 116 | 'file-1.txt': 'Hi!' 117 | }, 118 | 'source-dir': { 119 | 'symdir': mockFs.symlink({ 120 | path: '../dir' 121 | }) 122 | } 123 | }); 124 | 125 | await packFiles(['symdir/'], { emptyDirs: true }); 126 | 127 | const files = await parseFiles(resdir); 128 | 129 | t.deepEqual(files, [{ path: 'symdir', mode: 0o644, contents: null, linkpath: '../dir' }]); 130 | }); 131 | 132 | test('should take into account symlink to dir if emptyDirs is false', async t => { 133 | mockFs({ 134 | 'dir': { 135 | 'file-1.txt': 'Hi!' 136 | }, 137 | 'source-dir': { 138 | 'symdir': mockFs.symlink({ 139 | path: '../dir' 140 | }) 141 | } 142 | }); 143 | 144 | await packFiles(['symdir/'], { emptyDirs: false }); 145 | 146 | const files = await parseFiles(resdir); 147 | 148 | t.deepEqual(files, [{ path: 'symdir', mode: 0o644, contents: null, linkpath: '../dir' }]); 149 | }); 150 | 151 | test('should take into broken symlinks', async t => { 152 | mockFs({ 153 | 'file-1.txt': 'Hi!', 154 | 'source-dir': { 155 | 'symlink.txt': mockFs.symlink({ 156 | path: '../no-file' 157 | }) 158 | } 159 | }); 160 | 161 | await packFiles(['symlink.txt']); 162 | 163 | const files = await parseFiles(); 164 | 165 | t.deepEqual(files, [{ path: 'symlink.txt', mode: 0o644, contents: null, linkpath: '../no-file' }]); 166 | }); 167 | 168 | test('should follow symlink', async t => { 169 | mockFs({ 170 | 'file-1.txt': 'Hi!', 171 | 'source-dir': { 172 | 'symlink.txt': mockFs.symlink({ 173 | path: '../file-1.txt' 174 | }) 175 | } 176 | }); 177 | 178 | await packFiles(['symlink.txt'], { followSymlinks: true }); 179 | 180 | const files = await parseFiles(resdir); 181 | 182 | t.deepEqual(files, [{ path: 'symlink.txt', mode: 0o644, contents: 'Hi!' }]); 183 | }); 184 | 185 | test('should include empty file', async t => { 186 | mockFs({ 187 | 'source-dir': { 188 | 'empty-file.txt': mockFs.file(), 189 | 'file-1.txt': 'Hi!' 190 | } 191 | }); 192 | 193 | await packFiles(['empty-file.txt', 'file-1.txt']); 194 | 195 | const files = await parseFiles(); 196 | 197 | t.deepEqual(files.map(file => file.path), ['empty-file.txt', 'file-1.txt']); 198 | }); 199 | 200 | unixOnly && test('should keep x flag in mode and skip the rest', async t => { 201 | mockFs({ 202 | 'source-dir': { 203 | 'bash': mockFs.file({ mode: 0o764 }), 204 | 'broodwar.exe': mockFs.file({ mode: 0o677 }), 205 | 'wth.sh': mockFs.file({ mode: 0o711 }) 206 | } 207 | }); 208 | 209 | await packFiles(['bash', 'broodwar.exe', 'wth.sh']); 210 | 211 | const files = await parseFiles(); 212 | 213 | t.deepEqual(files.map(file => file.mode), [0o744, 0o655, 0o755]); 214 | }); 215 | 216 | test('should ignore empty file', async t => { 217 | mockFs({ 218 | 'source-dir': { 219 | 'empty-file.txt': mockFs.file(), 220 | 'file-1.txt': 'Hi!' 221 | } 222 | }); 223 | 224 | await packFiles(['empty-file.txt', 'file-1.txt'], { emptyFiles: false }); 225 | 226 | const files = await parseFiles(); 227 | 228 | t.deepEqual(files.map(file => file.path), ['file-1.txt']); 229 | }); 230 | 231 | test('should emit error if file file does not exist', t => { 232 | mockFs({ 233 | 'source-dir': {} 234 | }); 235 | 236 | t.throws(packFiles(['no-file.txt']), /no such file or directory/); 237 | }); 238 | 239 | function packFiles(filenames, options) { 240 | return new Promise((resolve, reject) => { 241 | findFiles(filenames) 242 | .on('error', reject) 243 | .pipe(new TarStream(dest, options)) 244 | .on('error', reject) 245 | .on('close', resolve); 246 | }); 247 | } 248 | 249 | function parseFiles() { 250 | const files = []; 251 | 252 | return tar.list({ 253 | file: dest, 254 | onentry: entry => { 255 | if (entry.size === 0) { 256 | files.push({ path: entry.path, mode: entry.mode, contents: null, linkpath: entry.linkpath }); 257 | } else { 258 | entry.on('data', buf => { 259 | files.push({ path: entry.path, mode: entry.mode, contents: buf.toString('utf-8') }); 260 | }); 261 | } 262 | } 263 | }).then(() => files); 264 | } 265 | 266 | function findFiles(filenames) { 267 | const files = filenames.map(basename => { 268 | const filePath = path.join(root, basename); 269 | 270 | return { 271 | path: filePath, 272 | base: root, 273 | cwd: root, 274 | history: [filePath], 275 | lstats: fs.lstatSync(filePath), 276 | subdir: '.' 277 | }; 278 | }); 279 | 280 | return streamify(files); 281 | } 282 | -------------------------------------------------------------------------------- /test/streams/transform-stream.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const test = require('ava'); 7 | const mockFs = require('mock-fs'); 8 | 9 | const writeArtifact = require('../../lib'); 10 | 11 | test.afterEach(() => mockFs.restore()); 12 | 13 | const transform = (dir) => { 14 | return (chunk) => { 15 | const f = path.parse(chunk.path); 16 | 17 | if (f.ext === '.txt') { 18 | chunk.path = path.join(f.dir, dir, f.base); 19 | } 20 | 21 | return chunk; 22 | }; 23 | }; 24 | 25 | const transformWithAddChunk = (dir) => { 26 | return (chunk) => { 27 | const chunks = []; 28 | const f = path.parse(chunk.path); 29 | 30 | if (f.ext === '.txt') { 31 | chunk.path = path.join(f.dir, dir, f.base); 32 | 33 | chunks.push(chunk, Object.assign({}, chunk, { 34 | path: chunk.path.replace('txt', 'md') 35 | })); 36 | } 37 | 38 | return chunks.length ? chunks : chunk; 39 | }; 40 | }; 41 | 42 | test('should transform chunks by transform function', async t => { 43 | mockFs({ 44 | 'source-dir': { 45 | 'file-1.txt': 'Hi!', 46 | 'file-2.txt': 'Hello!', 47 | 'file-3.js': 'good bye!' 48 | } 49 | }); 50 | 51 | await writeArtifact({ 52 | dest: 'artifact-dir', 53 | patterns: 'source-dir/**', 54 | transform: transform('new-dir') 55 | }); 56 | 57 | const newDir = fs.readdirSync('artifact-dir/source-dir/new-dir'); 58 | const dir = fs.readdirSync('artifact-dir/source-dir'); 59 | 60 | t.deepEqual(newDir, ['file-1.txt', 'file-2.txt']); 61 | t.deepEqual(dir, ['file-3.js', 'new-dir']); 62 | }); 63 | 64 | test('should handle one more chunk from transform function', async t => { 65 | mockFs({ 66 | 'source-dir': { 67 | 'file-1.txt': 'Hi!', 68 | 'file-2.txt': 'Hello!', 69 | 'file-3.js': 'good bye!' 70 | } 71 | }); 72 | 73 | await writeArtifact({ 74 | dest: 'artifact-dir', 75 | patterns: 'source-dir/**', 76 | transform: transformWithAddChunk('new-dir') 77 | }); 78 | 79 | const newDir = fs.readdirSync('artifact-dir/source-dir/new-dir'); 80 | const dir = fs.readdirSync('artifact-dir/source-dir'); 81 | 82 | t.deepEqual(newDir, ['file-1.md', 'file-1.txt', 'file-2.md', 'file-2.txt']); 83 | t.deepEqual(dir, ['file-3.js', 'new-dir']); 84 | }); 85 | --------------------------------------------------------------------------------