├── .babelrc.js ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── index.d.ts ├── jest.config.js ├── package.json ├── readme.md ├── rollup.config.js ├── src └── index.js ├── tests ├── fixtures │ └── src │ │ ├── assets │ │ ├── asset-1.js │ │ ├── asset-2.js │ │ ├── css │ │ │ ├── css-1.css │ │ │ └── css-2.css │ │ └── scss │ │ │ ├── nested │ │ │ └── scss-3.scss │ │ │ ├── scss-1.scss │ │ │ └── scss-2.scss │ │ └── index.js └── index.test.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | rules: { 4 | 'comma-dangle': ['error', 'never'], 5 | 'object-curly-newline': ['error', { multiline: true, consistent: true }], 6 | semi: ['error', 'never'] 7 | }, 8 | env: { 9 | jest: true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | - 8 6 | script: 7 | - jest --ci --coverage && codecov 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import rollup from 'rollup'; 2 | import fs from 'fs-extra'; 3 | import globby from 'globby'; 4 | 5 | interface Target extends globby.GlobbyOptions { 6 | /** 7 | * Path or glob of what to copy. 8 | */ 9 | readonly src: string | readonly string[]; 10 | 11 | /** 12 | * One or more destinations where to copy. 13 | */ 14 | readonly dest: string | readonly string[]; 15 | 16 | /** 17 | * Change destination file or folder name. 18 | */ 19 | readonly rename?: string | ((name: string, extension: string, fullPath: string) => string); 20 | 21 | /** 22 | * Modify file contents. 23 | */ 24 | readonly transform?: (contents: Buffer, name: string) => any; 25 | } 26 | 27 | interface CopyOptions extends globby.GlobbyOptions, fs.WriteFileOptions, fs.CopyOptions { 28 | /** 29 | * Copy items once. Useful in watch mode. 30 | * @default false 31 | */ 32 | readonly copyOnce?: boolean; 33 | 34 | /** 35 | * Copy items synchronous. 36 | * @default false 37 | */ 38 | readonly copySync?: boolean; 39 | 40 | /** 41 | * Remove the directory structure of copied files. 42 | * @default true 43 | */ 44 | readonly flatten?: boolean; 45 | 46 | /** 47 | * Rollup hook the plugin should use. 48 | * @default 'buildEnd' 49 | */ 50 | readonly hook?: string; 51 | 52 | /** 53 | * Array of targets to copy. 54 | * @default [] 55 | */ 56 | readonly targets?: readonly Target[]; 57 | 58 | /** 59 | * Output copied items to console. 60 | * @default false 61 | */ 62 | readonly verbose?: boolean; 63 | } 64 | 65 | /** 66 | * Copy files and folders using Rollup 67 | */ 68 | export default function copy(options?: CopyOptions): rollup.Plugin; 69 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | '/src', 4 | '/tests' 5 | ], 6 | watchPathIgnorePatterns: [ 7 | '/tests/fixtures/build', 8 | '/tests/fixtures/dist', 9 | '/tests/fixtures/src/index.js' 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-copy", 3 | "description": "Copy files and folders using Rollup", 4 | "version": "3.5.0", 5 | "author": "Vlad Shcherbin ", 6 | "repository": "vladshcherbin/rollup-plugin-copy", 7 | "main": "dist/index.commonjs.js", 8 | "module": "dist/index.module.js", 9 | "types": "index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf coverage dist", 12 | "build": "rollup -c", 13 | "lint": "eslint src tests", 14 | "postpublish": "yarn clean", 15 | "prepublishOnly": "yarn lint && yarn test && yarn clean && yarn build", 16 | "test": "jest" 17 | }, 18 | "dependencies": { 19 | "@types/fs-extra": "^8.0.1", 20 | "colorette": "^1.1.0", 21 | "fs-extra": "^8.1.0", 22 | "globby": "10.0.1", 23 | "is-plain-object": "^3.0.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.12.17", 27 | "@babel/preset-env": "^7.12.17", 28 | "babel-jest": "^25.5.1", 29 | "codecov": "^3.6.1", 30 | "eslint": "6.5.1", 31 | "eslint-config-airbnb-base": "^14.0.0", 32 | "eslint-plugin-import": "^2.20.0", 33 | "jest": "^25.5.4", 34 | "replace-in-file": "^5.0.2", 35 | "rimraf": "^3.0.0", 36 | "rollup": "^1.29.0", 37 | "rollup-plugin-auto-external": "^2.0.0", 38 | "rollup-plugin-babel": "^4.3.3" 39 | }, 40 | "files": [ 41 | "dist", 42 | "index.d.ts", 43 | "readme.md" 44 | ], 45 | "keywords": [ 46 | "rollup", 47 | "rollup-plugin", 48 | "copy", 49 | "cp", 50 | "asset", 51 | "assets", 52 | "file", 53 | "files", 54 | "folder", 55 | "folders", 56 | "glob" 57 | ], 58 | "engines": { 59 | "node": ">=8.3" 60 | }, 61 | "license": "MIT" 62 | } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rollup-plugin-copy 2 | 3 | [![Build Status](https://travis-ci.com/vladshcherbin/rollup-plugin-copy.svg?branch=master)](https://travis-ci.com/vladshcherbin/rollup-plugin-copy) 4 | [![Codecov](https://codecov.io/gh/vladshcherbin/rollup-plugin-copy/branch/master/graph/badge.svg)](https://codecov.io/gh/vladshcherbin/rollup-plugin-copy) 5 | 6 | Copy files and folders, with glob support. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | # yarn 12 | yarn add rollup-plugin-copy -D 13 | 14 | # npm 15 | npm install rollup-plugin-copy -D 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | // rollup.config.js 22 | import copy from 'rollup-plugin-copy' 23 | 24 | export default { 25 | input: 'src/index.js', 26 | output: { 27 | file: 'dist/app.js', 28 | format: 'cjs' 29 | }, 30 | plugins: [ 31 | copy({ 32 | targets: [ 33 | { src: 'src/index.html', dest: 'dist/public' }, 34 | { src: ['assets/fonts/arial.woff', 'assets/fonts/arial.woff2'], dest: 'dist/public/fonts' }, 35 | { src: 'assets/images/**/*', dest: 'dist/public/images' } 36 | ] 37 | }) 38 | ] 39 | } 40 | ``` 41 | 42 | ### Configuration 43 | 44 | There are some useful options: 45 | 46 | #### targets 47 | 48 | Type: `Array` | Default: `[]` 49 | 50 | Array of targets to copy. A target is an object with properties: 51 | 52 | - **src** (`string` `Array`): Path or glob of what to copy 53 | - **dest** (`string` `Array`): One or more destinations where to copy 54 | - **rename** (`string` `Function`): Change destination file or folder name 55 | - **transform** (`Function`): Modify file contents 56 | 57 | Each object should have **src** and **dest** properties, **rename** and **transform** are optional. [globby](https://github.com/sindresorhus/globby) is used inside, check it for [glob pattern](https://github.com/sindresorhus/globby#globbing-patterns) examples. 58 | 59 | ##### File 60 | 61 | ```js 62 | copy({ 63 | targets: [{ src: 'src/index.html', dest: 'dist/public' }] 64 | }) 65 | ``` 66 | 67 | ##### Folder 68 | 69 | ```js 70 | copy({ 71 | targets: [{ src: 'assets/images', dest: 'dist/public' }] 72 | }) 73 | ``` 74 | 75 | ##### Glob 76 | 77 | ```js 78 | copy({ 79 | targets: [{ src: 'assets/*', dest: 'dist/public' }] 80 | }) 81 | ``` 82 | 83 | ##### Glob: multiple items 84 | 85 | ```js 86 | copy({ 87 | targets: [{ src: ['src/index.html', 'src/styles.css', 'assets/images'], dest: 'dist/public' }] 88 | }) 89 | ``` 90 | 91 | ##### Glob: negated patterns 92 | 93 | ```js 94 | copy({ 95 | targets: [{ src: ['assets/images/**/*', '!**/*.gif'], dest: 'dist/public/images' }] 96 | }) 97 | ``` 98 | 99 | ##### Multiple targets 100 | 101 | ```js 102 | copy({ 103 | targets: [ 104 | { src: 'src/index.html', dest: 'dist/public' }, 105 | { src: 'assets/images/**/*', dest: 'dist/public/images' } 106 | ] 107 | }) 108 | ``` 109 | 110 | ##### Multiple destinations 111 | 112 | ```js 113 | copy({ 114 | targets: [{ src: 'src/index.html', dest: ['dist/public', 'build/public'] }] 115 | }) 116 | ``` 117 | 118 | ##### Rename with a string 119 | 120 | ```js 121 | copy({ 122 | targets: [{ src: 'src/app.html', dest: 'dist/public', rename: 'index.html' }] 123 | }) 124 | ``` 125 | 126 | ##### Rename with a function 127 | 128 | ```js 129 | copy({ 130 | targets: [{ 131 | src: 'assets/docs/*', 132 | dest: 'dist/public/docs', 133 | rename: (name, extension, fullPath) => `${name}-v1.${extension}` 134 | }] 135 | }) 136 | ``` 137 | 138 | ##### Transform file contents 139 | 140 | ```js 141 | copy({ 142 | targets: [{ 143 | src: 'src/index.html', 144 | dest: 'dist/public', 145 | transform: (contents, filename) => contents.toString().replace('__SCRIPT__', 'app.js') 146 | }] 147 | }) 148 | ``` 149 | 150 | #### verbose 151 | 152 | Type: `boolean` | Default: `false` 153 | 154 | Output copied items to console. 155 | 156 | ```js 157 | copy({ 158 | targets: [{ src: 'assets/*', dest: 'dist/public' }], 159 | verbose: true 160 | }) 161 | ``` 162 | 163 | #### hook 164 | 165 | Type: `string` | Default: `buildEnd` 166 | 167 | [Rollup hook](https://rollupjs.org/guide/en/#hooks) the plugin should use. By default, plugin runs when rollup has finished bundling, before bundle is written to disk. 168 | 169 | ```js 170 | copy({ 171 | targets: [{ src: 'assets/*', dest: 'dist/public' }], 172 | hook: 'writeBundle' 173 | }) 174 | ``` 175 | 176 | #### copyOnce 177 | 178 | Type: `boolean` | Default: `false` 179 | 180 | Copy items once. Useful in watch mode. 181 | 182 | ```js 183 | copy({ 184 | targets: [{ src: 'assets/*', dest: 'dist/public' }], 185 | copyOnce: true 186 | }) 187 | 188 | ``` 189 | #### copySync 190 | 191 | Type: `boolean` | Default: `false` 192 | 193 | Copy items synchronous. 194 | 195 | ```js 196 | copy({ 197 | targets: [{ src: 'assets/*', dest: 'dist/public' }], 198 | copySync: true 199 | }) 200 | ``` 201 | 202 | #### flatten 203 | 204 | Type: `boolean` | Default: `true` 205 | 206 | Remove the directory structure of copied files. 207 | 208 | ```js 209 | copy({ 210 | targets: [{ src: 'assets/**/*', dest: 'dist/public' }], 211 | flatten: false 212 | }) 213 | ``` 214 | 215 | All other options are passed to packages, used inside: 216 | - [globby](https://github.com/sindresorhus/globby) 217 | - [fs-extra copy function](https://github.com/jprichardson/node-fs-extra/blob/7.0.0/docs/copy.md) 218 | 219 | ## Original Author 220 | 221 | [Cédric Meuter](https://github.com/meuter) 222 | 223 | ## License 224 | 225 | MIT 226 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import autoExternal from 'rollup-plugin-auto-external' 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: [ 7 | { 8 | file: 'dist/index.commonjs.js', 9 | format: 'commonjs' 10 | }, 11 | { 12 | file: 'dist/index.module.js', 13 | format: 'module' 14 | } 15 | ], 16 | plugins: [ 17 | babel({ 18 | presets: [['@babel/preset-env', { targets: { node: '8.3' } }]], 19 | comments: false 20 | }), 21 | autoExternal() 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop, no-console, no-restricted-syntax */ 2 | import path from 'path' 3 | import util from 'util' 4 | import fs from 'fs-extra' 5 | import isObject from 'is-plain-object' 6 | import globby from 'globby' 7 | import { bold, green, yellow } from 'colorette' 8 | 9 | function stringify(value) { 10 | return util.inspect(value, { breakLength: Infinity }) 11 | } 12 | 13 | async function isFile(filePath) { 14 | const fileStats = await fs.stat(filePath) 15 | 16 | return fileStats.isFile() 17 | } 18 | 19 | function renameTarget(target, rename, src) { 20 | const parsedPath = path.parse(target) 21 | 22 | return typeof rename === 'string' 23 | ? rename 24 | : rename(parsedPath.name, parsedPath.ext.replace('.', ''), src) 25 | } 26 | 27 | async function generateCopyTarget(src, dest, { flatten, rename, transform }) { 28 | if (transform && !await isFile(src)) { 29 | throw new Error(`"transform" option works only on files: '${src}' must be a file`) 30 | } 31 | 32 | const { base, dir } = path.parse(src) 33 | const destinationFolder = (flatten || (!flatten && !dir)) 34 | ? dest 35 | : dir.replace(dir.split('/')[0], dest) 36 | 37 | return { 38 | src, 39 | dest: path.join(destinationFolder, rename ? renameTarget(base, rename, src) : base), 40 | ...(transform && { contents: await transform(await fs.readFile(src), base) }), 41 | renamed: rename, 42 | transformed: transform 43 | } 44 | } 45 | 46 | export default function copy(options = {}) { 47 | const { 48 | copyOnce = false, 49 | copySync = false, 50 | flatten = true, 51 | hook = 'buildEnd', 52 | targets = [], 53 | verbose = false, 54 | ...restPluginOptions 55 | } = options 56 | 57 | let copied = false 58 | 59 | return { 60 | name: 'copy', 61 | [hook]: async () => { 62 | if (copyOnce && copied) { 63 | return 64 | } 65 | 66 | const copyTargets = [] 67 | 68 | if (Array.isArray(targets) && targets.length) { 69 | for (const target of targets) { 70 | if (!isObject(target)) { 71 | throw new Error(`${stringify(target)} target must be an object`) 72 | } 73 | 74 | const { dest, rename, src, transform, ...restTargetOptions } = target 75 | 76 | if (!src || !dest) { 77 | throw new Error(`${stringify(target)} target must have "src" and "dest" properties`) 78 | } 79 | 80 | if (rename && typeof rename !== 'string' && typeof rename !== 'function') { 81 | throw new Error(`${stringify(target)} target's "rename" property must be a string or a function`) 82 | } 83 | 84 | const matchedPaths = await globby(src, { 85 | expandDirectories: false, 86 | onlyFiles: false, 87 | ...restPluginOptions, 88 | ...restTargetOptions 89 | }) 90 | 91 | if (matchedPaths.length) { 92 | for (const matchedPath of matchedPaths) { 93 | const generatedCopyTargets = Array.isArray(dest) 94 | ? await Promise.all(dest.map((destination) => generateCopyTarget( 95 | matchedPath, 96 | destination, 97 | { flatten, rename, transform } 98 | ))) 99 | : [await generateCopyTarget(matchedPath, dest, { flatten, rename, transform })] 100 | 101 | copyTargets.push(...generatedCopyTargets) 102 | } 103 | } 104 | } 105 | } 106 | 107 | if (copyTargets.length) { 108 | if (verbose) { 109 | console.log(green('copied:')) 110 | } 111 | 112 | for (const copyTarget of copyTargets) { 113 | const { contents, dest, src, transformed } = copyTarget 114 | 115 | if (transformed) { 116 | await fs.outputFile(dest, contents, restPluginOptions) 117 | } else if (!copySync) { 118 | await fs.copy(src, dest, restPluginOptions) 119 | } else { 120 | fs.copySync(src, dest, restPluginOptions) 121 | } 122 | 123 | if (verbose) { 124 | let message = green(` ${bold(src)} → ${bold(dest)}`) 125 | const flags = Object.entries(copyTarget) 126 | .filter(([key, value]) => ['renamed', 'transformed'].includes(key) && value) 127 | .map(([key]) => key.charAt(0).toUpperCase()) 128 | 129 | if (flags.length) { 130 | message = `${message} ${yellow(`[${flags.join(', ')}]`)}` 131 | } 132 | 133 | console.log(message) 134 | } 135 | } 136 | } else if (verbose) { 137 | console.log(yellow('no items to copy')) 138 | } 139 | 140 | copied = true 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/asset-1.js: -------------------------------------------------------------------------------- 1 | export default function asset1() { 2 | return 'asset 1' 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/asset-2.js: -------------------------------------------------------------------------------- 1 | export default function asset2() { 2 | return 'asset 2' 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/css/css-1.css: -------------------------------------------------------------------------------- 1 | .css-1 { 2 | background-color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/css/css-2.css: -------------------------------------------------------------------------------- 1 | .css-2 { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/scss/nested/scss-3.scss: -------------------------------------------------------------------------------- 1 | .scss-3 { 2 | background-color: green; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/scss/scss-1.scss: -------------------------------------------------------------------------------- 1 | .scss-1 { 2 | background-color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/assets/scss/scss-2.scss: -------------------------------------------------------------------------------- 1 | .scss-2 { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/index.js: -------------------------------------------------------------------------------- 1 | export default function test() { 2 | return 'hey' 3 | } 4 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { rollup, watch } from 'rollup' 3 | import fs from 'fs-extra' 4 | import replace from 'replace-in-file' 5 | import { bold, green, yellow, options } from 'colorette' 6 | import copy from '../src' 7 | 8 | process.chdir(`${__dirname}/fixtures`) 9 | 10 | options.enabled = true 11 | 12 | function sleep(ms) { 13 | return new Promise((resolve) => setTimeout(resolve, ms)) 14 | } 15 | 16 | function readFile(filePath) { 17 | return fs.readFile(filePath, 'utf-8') 18 | } 19 | 20 | async function build(pluginOptions) { 21 | await rollup({ 22 | input: 'src/index.js', 23 | plugins: [ 24 | copy(pluginOptions) 25 | ] 26 | }) 27 | } 28 | 29 | afterEach(async () => { 30 | await fs.remove('build') 31 | await fs.remove('dist') 32 | }) 33 | 34 | describe('Copy', () => { 35 | test('No config passed', async () => { 36 | await build() 37 | 38 | expect(await fs.pathExists('dist/asset-1.js')).toBe(false) 39 | }) 40 | 41 | test('Empty array as target', async () => { 42 | await build({ 43 | targets: [] 44 | }) 45 | 46 | expect(await fs.pathExists('dist/asset-1.js')).toBe(false) 47 | }) 48 | 49 | test('Files', async () => { 50 | await build({ 51 | targets: [{ 52 | src: [ 53 | 'src/assets/asset-1.js', 54 | 'src/assets/asset-2.js' 55 | ], 56 | dest: 'dist' 57 | }] 58 | }) 59 | 60 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 61 | expect(await fs.pathExists('dist/asset-2.js')).toBe(true) 62 | }) 63 | 64 | test('Folders', async () => { 65 | await build({ 66 | targets: [{ 67 | src: [ 68 | 'src/assets/css', 69 | 'src/assets/scss' 70 | ], 71 | dest: 'dist' 72 | }] 73 | }) 74 | 75 | expect(await fs.pathExists('dist/css')).toBe(true) 76 | expect(await fs.pathExists('dist/css/css-1.css')).toBe(true) 77 | expect(await fs.pathExists('dist/css/css-2.css')).toBe(true) 78 | expect(await fs.pathExists('dist/scss')).toBe(true) 79 | expect(await fs.pathExists('dist/scss/scss-1.scss')).toBe(true) 80 | expect(await fs.pathExists('dist/scss/scss-2.scss')).toBe(true) 81 | expect(await fs.pathExists('dist/scss/nested')).toBe(true) 82 | expect(await fs.pathExists('dist/scss/nested/scss-3.scss')).toBe(true) 83 | }) 84 | 85 | test('Glob', async () => { 86 | await build({ 87 | targets: [{ 88 | src: [ 89 | 'src/assets/asset-{1,2}.js', 90 | 'src/assets/css/*.css', 91 | '!**/css-1.css', 92 | 'src/assets/scss/scss-?(1).scss' 93 | ], 94 | dest: 'dist' 95 | }] 96 | }) 97 | 98 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 99 | expect(await fs.pathExists('dist/asset-2.js')).toBe(true) 100 | expect(await fs.pathExists('dist/css-1.css')).toBe(false) 101 | expect(await fs.pathExists('dist/css-2.css')).toBe(true) 102 | expect(await fs.pathExists('dist/scss-1.scss')).toBe(true) 103 | expect(await fs.pathExists('dist/scss-2.scss')).toBe(false) 104 | }) 105 | 106 | test('Multiple objects as targets', async () => { 107 | await build({ 108 | targets: [ 109 | { src: ['src/assets/*', 'src/assets/css'], dest: 'dist' }, 110 | { src: 'src/assets/css/*.css', dest: 'build' } 111 | ] 112 | }) 113 | 114 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 115 | expect(await fs.pathExists('dist/asset-2.js')).toBe(true) 116 | expect(await fs.pathExists('dist/css')).toBe(true) 117 | expect(await fs.pathExists('dist/css/css-1.css')).toBe(true) 118 | expect(await fs.pathExists('dist/css/css-2.css')).toBe(true) 119 | expect(await fs.pathExists('build/css-1.css')).toBe(true) 120 | expect(await fs.pathExists('build/css-2.css')).toBe(true) 121 | }) 122 | 123 | test('Multiple destinations', async () => { 124 | await build({ 125 | targets: [{ 126 | src: [ 127 | 'src/assets/asset-1.js', 128 | 'src/assets/css', 129 | 'src/assets/scss/scss-?(1).scss' 130 | ], 131 | dest: ['dist', 'build'] 132 | }] 133 | }) 134 | 135 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 136 | expect(await fs.pathExists('dist/css')).toBe(true) 137 | expect(await fs.pathExists('dist/css/css-1.css')).toBe(true) 138 | expect(await fs.pathExists('dist/css/css-2.css')).toBe(true) 139 | expect(await fs.pathExists('dist/scss-1.scss')).toBe(true) 140 | expect(await fs.pathExists('build/asset-1.js')).toBe(true) 141 | expect(await fs.pathExists('build/css')).toBe(true) 142 | expect(await fs.pathExists('build/css/css-1.css')).toBe(true) 143 | expect(await fs.pathExists('build/css/css-2.css')).toBe(true) 144 | expect(await fs.pathExists('build/scss-1.scss')).toBe(true) 145 | }) 146 | 147 | test('Same target', async () => { 148 | await build({ 149 | targets: [ 150 | { src: 'src/assets/css', dest: 'dist' }, 151 | { src: 'src/assets/css', dest: 'dist' }, 152 | { src: ['src/assets/asset-1.js', 'src/assets/asset-1.js'], dest: 'build' } 153 | ] 154 | }) 155 | 156 | expect(await fs.pathExists('dist/css')).toBe(true) 157 | expect(await fs.pathExists('dist/css/css-1.css')).toBe(true) 158 | expect(await fs.pathExists('dist/css/css-2.css')).toBe(true) 159 | expect(await fs.pathExists('build/asset-1.js')).toBe(true) 160 | }) 161 | 162 | test('Throw if target is not an object', async () => { 163 | await expect(build({ 164 | targets: [ 165 | 'src/assets/asset-1.js' 166 | ] 167 | })).rejects.toThrow('\'src/assets/asset-1.js\' target must be an object') 168 | }) 169 | 170 | test('Throw if target object doesn\'t have required properties', async () => { 171 | await expect(build({ 172 | targets: [ 173 | { src: 'src/assets/asset-1.js' } 174 | ] 175 | })) 176 | .rejects 177 | .toThrow('{ src: \'src/assets/asset-1.js\' } target must have "src" and "dest" properties') 178 | }) 179 | 180 | test('Throw if target object "rename" property is of wrong type', async () => { 181 | await expect(build({ 182 | targets: [ 183 | { src: 'src/assets/asset-1.js', dest: 'dist', rename: [] } 184 | ] 185 | })) 186 | .rejects 187 | .toThrow( 188 | '{ src: \'src/assets/asset-1.js\', dest: \'dist\', rename: [] }' 189 | + ' target\'s "rename" property must be a string or a function' 190 | ) 191 | }) 192 | 193 | test('Rename target', async () => { 194 | await build({ 195 | targets: [ 196 | { src: 'src/assets/asset-1.js', dest: 'dist', rename: 'asset-1-renamed.js' }, 197 | { src: 'src/assets/css', dest: 'dist', rename: 'css-renamed' }, 198 | { src: 'src/assets/css/*', dest: 'dist/css-multiple', rename: 'css-1.css' }, 199 | { 200 | src: 'src/assets/asset-2.js', 201 | dest: 'dist', 202 | rename: (name, extension) => `${name}-renamed.${extension}` 203 | }, 204 | { 205 | src: 'src/assets/scss', 206 | dest: 'dist', 207 | rename: (name) => `${name}-renamed` 208 | }, 209 | { 210 | src: 'src/assets/scss/*', 211 | dest: 'dist/scss-multiple', 212 | rename: (name, extension) => ( 213 | extension 214 | ? `${name}-renamed.${extension}` 215 | : `${name}-renamed` 216 | ) 217 | }, 218 | { 219 | src: 'src/assets/asset-1.js', 220 | dest: 'dist', 221 | rename: (_, __, fullPath) => path.basename(fullPath).replace(1, 3) 222 | } 223 | ] 224 | }) 225 | 226 | expect(await fs.pathExists('dist/asset-1-renamed.js')).toBe(true) 227 | expect(await fs.pathExists('dist/css-renamed')).toBe(true) 228 | expect(await fs.pathExists('dist/css-renamed/css-1.css')).toBe(true) 229 | expect(await fs.pathExists('dist/css-renamed/css-2.css')).toBe(true) 230 | expect(await fs.pathExists('dist/css-multiple/css-1.css')).toBe(true) 231 | expect(await fs.pathExists('dist/css-multiple/css-2.css')).toBe(false) 232 | expect(await fs.pathExists('dist/asset-2-renamed.js')).toBe(true) 233 | expect(await fs.pathExists('dist/scss-renamed')).toBe(true) 234 | expect(await fs.pathExists('dist/scss-renamed/scss-1.scss')).toBe(true) 235 | expect(await fs.pathExists('dist/scss-renamed/scss-2.scss')).toBe(true) 236 | expect(await fs.pathExists('dist/scss-renamed/nested')).toBe(true) 237 | expect(await fs.pathExists('dist/scss-renamed/nested/scss-3.scss')).toBe(true) 238 | expect(await fs.pathExists('dist/scss-multiple/scss-1-renamed.scss')).toBe(true) 239 | expect(await fs.pathExists('dist/scss-multiple/scss-2-renamed.scss')).toBe(true) 240 | expect(await fs.pathExists('dist/scss-multiple/nested-renamed')).toBe(true) 241 | expect(await fs.pathExists('dist/scss-multiple/nested-renamed/scss-3.scss')).toBe(true) 242 | expect(await fs.pathExists('dist/asset-3.js')).toBe(true) 243 | }) 244 | 245 | test('Throw if transform target is not a file', async () => { 246 | await expect(build({ 247 | targets: [{ 248 | src: 'src/assets/css', 249 | dest: 'dist', 250 | transform: (contents) => contents.toString().replace('blue', 'red') 251 | }] 252 | })).rejects.toThrow('"transform" option works only on files: \'src/assets/css\' must be a file') 253 | }) 254 | 255 | test('Transform target', async () => { 256 | await build({ 257 | targets: [{ 258 | src: 'src/assets/css/css-1.css', 259 | dest: ['dist', 'build'], 260 | transform: (contents) => contents.toString().replace('blue', 'red') 261 | }, { 262 | src: 'src/assets/scss/**/*.scss', 263 | dest: 'dist', 264 | transform: (contents) => contents.toString().replace('background-color', 'color') 265 | }, { 266 | src: 'src/assets/css/css-1.css', 267 | dest: 'dist/css', 268 | transform: (contents, filename) => ( 269 | contents.toString().replace('blue', filename.replace('ss-1.css', 'oral')) 270 | ) 271 | }] 272 | }) 273 | 274 | expect(await fs.pathExists('dist/css-1.css')).toBe(true) 275 | expect(await readFile('dist/css-1.css')).toEqual(expect.stringContaining('red')) 276 | expect(await fs.pathExists('build/css-1.css')).toBe(true) 277 | expect(await readFile('build/css-1.css')).toEqual(expect.stringContaining('red')) 278 | expect(await fs.pathExists('dist/scss-1.scss')).toBe(true) 279 | expect(await readFile('dist/scss-1.scss')).toEqual(expect.not.stringContaining('background-color')) 280 | expect(await fs.pathExists('dist/scss-2.scss')).toBe(true) 281 | expect(await readFile('dist/scss-2.scss')).toEqual(expect.not.stringContaining('background-color')) 282 | expect(await fs.pathExists('dist/scss-3.scss')).toBe(true) 283 | expect(await readFile('dist/scss-3.scss')).toEqual(expect.not.stringContaining('background-color')) 284 | expect(await fs.pathExists('dist/css/css-1.css')).toBe(true) 285 | expect(await readFile('dist/css/css-1.css')).toEqual(expect.stringContaining('coral')) 286 | }) 287 | }) 288 | 289 | describe('Options', () => { 290 | /* eslint-disable no-console */ 291 | test('Verbose, copy files', async () => { 292 | console.log = jest.fn() 293 | 294 | await build({ 295 | targets: [{ 296 | src: [ 297 | 'src/assets/asset-1.js', 298 | 'src/assets/css/*', 299 | 'src/assets/scss', 300 | 'src/not-exist' 301 | ], 302 | dest: 'dist' 303 | }], 304 | verbose: true 305 | }) 306 | 307 | expect(console.log).toHaveBeenCalledTimes(5) 308 | expect(console.log).toHaveBeenCalledWith(green('copied:')) 309 | expect(console.log).toHaveBeenCalledWith(green(` ${bold('src/assets/asset-1.js')} → ${bold('dist/asset-1.js')}`)) 310 | expect(console.log).toHaveBeenCalledWith(green(` ${bold('src/assets/css/css-1.css')} → ${bold('dist/css-1.css')}`)) 311 | expect(console.log).toHaveBeenCalledWith(green(` ${bold('src/assets/css/css-2.css')} → ${bold('dist/css-2.css')}`)) 312 | expect(console.log).toHaveBeenCalledWith(green(` ${bold('src/assets/scss')} → ${bold('dist/scss')}`)) 313 | }) 314 | 315 | test('Verbose, no files to copy', async () => { 316 | console.log = jest.fn() 317 | 318 | await build({ 319 | targets: [ 320 | { src: 'src/not-exist', dest: 'dist' } 321 | ], 322 | verbose: true 323 | }) 324 | 325 | expect(console.log).toHaveBeenCalledTimes(1) 326 | expect(console.log).toHaveBeenCalledWith(yellow('no items to copy')) 327 | }) 328 | 329 | test('Verbose, rename files', async () => { 330 | console.log = jest.fn() 331 | 332 | await build({ 333 | targets: [ 334 | { src: 'src/assets/asset-1.js', dest: 'dist', rename: 'asset-1-renamed.js' }, 335 | { 336 | src: 'src/assets/scss/*', 337 | dest: 'dist/scss-multiple', 338 | rename: (name, extension) => ( 339 | extension 340 | ? `${name}-renamed.${extension}` 341 | : `${name}-renamed` 342 | ) 343 | } 344 | ], 345 | verbose: true 346 | }) 347 | 348 | expect(console.log).toHaveBeenCalledTimes(5) 349 | expect(console.log).toHaveBeenCalledWith(green('copied:')) 350 | expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/asset-1.js')} → ${bold('dist/asset-1-renamed.js')}`)} ${yellow('[R]')}`) 351 | expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/scss/scss-1.scss')} → ${bold('dist/scss-multiple/scss-1-renamed.scss')}`)} ${yellow('[R]')}`) 352 | expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/scss/scss-2.scss')} → ${bold('dist/scss-multiple/scss-2-renamed.scss')}`)} ${yellow('[R]')}`) 353 | expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/scss/nested')} → ${bold('dist/scss-multiple/nested-renamed')}`)} ${yellow('[R]')}`) 354 | }) 355 | 356 | test('Verbose, transform files', async () => { 357 | console.log = jest.fn() 358 | 359 | await build({ 360 | targets: [{ 361 | src: 'src/assets/css/css-*.css', 362 | dest: 'dist', 363 | transform: (contents) => contents.toString().replace('background-color', 'color') 364 | }], 365 | verbose: true 366 | }) 367 | 368 | expect(console.log).toHaveBeenCalledTimes(3) 369 | expect(console.log).toHaveBeenCalledWith(green('copied:')) 370 | expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/css/css-1.css')} → ${bold('dist/css-1.css')}`)} ${yellow('[T]')}`) 371 | expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/css/css-2.css')} → ${bold('dist/css-2.css')}`)} ${yellow('[T]')}`) 372 | }) 373 | /* eslint-enable no-console */ 374 | 375 | test('Hook', async () => { 376 | await build({ 377 | targets: [{ 378 | src: ['src/assets/asset-1.js', 'src/assets/css'], 379 | dest: 'dist' 380 | }], 381 | hook: 'buildStart' 382 | }) 383 | 384 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 385 | expect(await fs.pathExists('dist/css')).toBe(true) 386 | expect(await fs.pathExists('dist/css/css-1.css')).toBe(true) 387 | expect(await fs.pathExists('dist/css/css-2.css')).toBe(true) 388 | }) 389 | 390 | test('Copy once', async () => { 391 | const watcher = watch({ 392 | input: 'src/index.js', 393 | output: { 394 | dir: 'build', 395 | format: 'esm' 396 | }, 397 | plugins: [ 398 | copy({ 399 | targets: [ 400 | { src: 'src/assets/asset-1.js', dest: 'dist' } 401 | ], 402 | copyOnce: true 403 | }) 404 | ] 405 | }) 406 | 407 | await sleep(1000) 408 | 409 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 410 | 411 | await fs.remove('dist') 412 | 413 | expect(await fs.pathExists('dist/asset-1.js')).toBe(false) 414 | 415 | await replace({ 416 | files: 'src/index.js', 417 | from: 'hey', 418 | to: 'ho' 419 | }) 420 | 421 | await sleep(1000) 422 | 423 | expect(await fs.pathExists('dist/asset-1.js')).toBe(false) 424 | 425 | watcher.close() 426 | 427 | await replace({ 428 | files: 'src/index.js', 429 | from: 'ho', 430 | to: 'hey' 431 | }) 432 | }) 433 | 434 | test('Copy sync', async () => { 435 | await build({ 436 | targets: [{ 437 | src: [ 438 | 'src/assets/asset-1.js', 439 | 'src/assets/asset-2.js' 440 | ], 441 | dest: 'dist' 442 | }], 443 | copySync: true 444 | }) 445 | 446 | expect(await fs.pathExists('dist/asset-1.js')).toBe(true) 447 | expect(await fs.pathExists('dist/asset-2.js')).toBe(true) 448 | }) 449 | 450 | test('Flatten', async () => { 451 | await build({ 452 | targets: [{ 453 | src: [ 454 | 'src/assets/asset-1.js', 455 | 'src/assets/asset-2.js' 456 | ], 457 | dest: 'dist' 458 | }, 459 | { 460 | src: 'src/**/*.css', 461 | dest: 'dist' 462 | }, 463 | { 464 | src: '**/*.scss', 465 | dest: 'dist', 466 | rename: (name, extension) => `${name}-renamed.${extension}` 467 | }], 468 | flatten: false 469 | }) 470 | 471 | expect(await fs.pathExists('dist/assets/asset-1.js')).toBe(true) 472 | expect(await fs.pathExists('dist/assets/asset-2.js')).toBe(true) 473 | expect(await fs.pathExists('dist/assets/css/css-1.css')).toBe(true) 474 | expect(await fs.pathExists('dist/assets/css/css-2.css')).toBe(true) 475 | expect(await fs.pathExists('dist/assets/scss/scss-1-renamed.scss')).toBe(true) 476 | expect(await fs.pathExists('dist/assets/scss/scss-2-renamed.scss')).toBe(true) 477 | expect(await fs.pathExists('dist/assets/scss/nested/scss-3-renamed.scss')).toBe(true) 478 | }) 479 | 480 | test('Rest options', async () => { 481 | await build({ 482 | targets: [ 483 | { src: 'src/assets/asset-1.js', dest: 'dist' } 484 | ], 485 | ignore: ['**/asset-1.js'] 486 | }) 487 | 488 | expect(await fs.pathExists('dist/asset-1.js')).toBe(false) 489 | }) 490 | 491 | test('Rest target options', async () => { 492 | await build({ 493 | targets: [ 494 | { src: 'src/assets/asset-1.js', dest: 'dist', ignore: ['**/asset-1.js'] } 495 | ] 496 | }) 497 | 498 | expect(await fs.pathExists('dist/asset-1.js')).toBe(false) 499 | }) 500 | }) 501 | --------------------------------------------------------------------------------