├── .commitlintrc.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── workflows │ └── tests.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── filemanager.png ├── ava.config.js ├── package-lock.json ├── package.json ├── prettier.config.cjs ├── rollup.config.js ├── src ├── actions │ ├── archive.js │ ├── copy.js │ ├── delete.js │ ├── index.js │ ├── mkdir.js │ └── move.js ├── index.js ├── options-schema.js └── utils │ ├── glob-copy.js │ └── p-exec.js ├── tests ├── archive.test.js ├── copy.test.js ├── delete.test.js ├── execution-order.test.js ├── fixtures │ ├── index.html │ └── index.js ├── mkdir.test.js ├── move.test.js ├── multi-actions.test.js ├── other-options.test.js └── utils │ ├── compile.js │ ├── getCompiler.js │ ├── getFixturesDir.js │ └── tempy.js └── types.d.ts /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: non-conventional 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sibiraj-s] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## Config 6 | 7 | 10 | 11 | ## Issue 12 | 13 | 17 | 18 | ## Your Environment 19 | 20 | 21 | 22 | | Tech | Version | 23 | | -------------------------- | ------- | 24 | | filemanager-plugin-webpack | | 25 | | node | | 26 | | OS | | 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | branches: master 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | node-version: [16, 18, lts/*, current] 15 | 16 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }} 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: npm 28 | 29 | - run: npm --version 30 | - run: node --version 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - run: npm run build 36 | - run: npm test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,node,yarn,sass 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,node,yarn,sass 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### Node ### 48 | # Logs 49 | logs 50 | *.log 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | lerna-debug.log* 55 | 56 | # Diagnostic reports (https://nodejs.org/api/report.html) 57 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 58 | 59 | # Runtime data 60 | pids 61 | *.pid 62 | *.seed 63 | *.pid.lock 64 | 65 | # Directory for instrumented libs generated by jscoverage/JSCover 66 | lib-cov 67 | 68 | # Coverage directory used by tools like istanbul 69 | coverage 70 | *.lcov 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Microbundle cache 104 | .rpt2_cache/ 105 | .rts2_cache_cjs/ 106 | .rts2_cache_es/ 107 | .rts2_cache_umd/ 108 | 109 | # Optional REPL history 110 | .node_repl_history 111 | 112 | # Output of 'npm pack' 113 | *.tgz 114 | 115 | # Yarn Integrity file 116 | .yarn-integrity 117 | 118 | # dotenv environment variables file 119 | .env 120 | .env.test 121 | .env*.local 122 | 123 | # parcel-bundler cache (https://parceljs.org/) 124 | .cache 125 | .parcel-cache 126 | 127 | # Next.js build output 128 | .next 129 | 130 | # Nuxt.js build / generate output 131 | .nuxt 132 | dist 133 | 134 | # Gatsby files 135 | .cache/ 136 | # Comment in the public line in if your project uses Gatsby and not Next.js 137 | # https://nextjs.org/blog/next-9-1#public-directory-support 138 | # public 139 | 140 | # vuepress build output 141 | .vuepress/dist 142 | 143 | # Serverless directories 144 | .serverless/ 145 | 146 | # FuseBox cache 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | .dynamodb/ 151 | 152 | # TernJS port file 153 | .tern-port 154 | 155 | # Stores VSCode versions used for testing VSCode extensions 156 | .vscode-test 157 | 158 | ### Sass ### 159 | .sass-cache/ 160 | *.css.map 161 | *.sass.map 162 | *.scss.map 163 | 164 | ### Windows ### 165 | # Windows thumbnail cache files 166 | Thumbs.db 167 | Thumbs.db:encryptable 168 | ehthumbs.db 169 | ehthumbs_vista.db 170 | 171 | # Dump file 172 | *.stackdump 173 | 174 | # Folder config file 175 | [Dd]esktop.ini 176 | 177 | # Recycle Bin used on file shares 178 | $RECYCLE.BIN/ 179 | 180 | # Windows Installer files 181 | *.cab 182 | *.msi 183 | *.msix 184 | *.msm 185 | *.msp 186 | 187 | # Windows shortcuts 188 | *.lnk 189 | 190 | ### yarn ### 191 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored 192 | 193 | .yarn/* 194 | !.yarn/releases 195 | !.yarn/plugins 196 | !.yarn/sdks 197 | !.yarn/versions 198 | 199 | # if you are NOT using Zero-installs, then: 200 | # comment the following lines 201 | !.yarn/cache 202 | 203 | # and uncomment the following lines 204 | # .pnp.* 205 | 206 | # Artifcats 207 | dist/ 208 | lib/ 209 | testing/ 210 | testing*/ 211 | tests/**/tmp-* 212 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run build 5 | npm run test 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v8.0.0 (2022-11-16) 6 | 7 | - drop nodejs 12 support, requires node 14.13.1+ ([0f334f0](https://github.com/gregnb/filemanager-webpack-plugin/commit/0f334f0)) 8 | - fix export syntax for typings ([59b17ca](https://github.com/gregnb/filemanager-webpack-plugin/commit/59b17ca)) 9 | - update to rollup 3 ([23e332c](https://github.com/gregnb/filemanager-webpack-plugin/commit/23e332c)) 10 | 11 | ## v7.0.0 (2022-06-11) 12 | 13 | - no notable changes since v7.0.0-beta.0 14 | 15 | ## v7.0.0-beta.0 (2022-04-09) 16 | 17 | - no notable changes since v7.0.0-alpha.2 18 | 19 | ## v7.0.0-alpha.2 (2022-02-01) 20 | 21 | #### Bug Fixes 22 | 23 | - fix typings for copy actions ([100686f](https://github.com/gregnb/filemanager-webpack-plugin/commit/100686f)) 24 | 25 | #### Dependency Updates 26 | 27 | - bump all dependencies to latest ([1dc902b](https://github.com/gregnb/filemanager-webpack-plugin/commit/1dc902b)) 28 | 29 | #### Internal 30 | 31 | - update LICENSE ([aafd652](https://github.com/gregnb/filemanager-webpack-plugin/commit/aafd652)) 32 | 33 | ## v7.0.0-alpha.1 (2021-12-04) 34 | 35 | #### Features 36 | 37 | - support flat copy with glob ([83bf2dd](https://github.com/gregnb/filemanager-webpack-plugin/commit/83bf2dd)) 38 | - add copy options `overwrite` and `preserveTimestamps` ([83bf2dd](https://github.com/gregnb/filemanager-webpack-plugin/commit/83bf2dd)) 39 | 40 | ## v7.0.0-alpha.0 (2021-12-03) 41 | 42 | #### Bug Fixes 43 | 44 | - remove cpy dependency, fixing vulnerabilities ([9dcfbd8](https://github.com/gregnb/filemanager-webpack-plugin/commit/9dcfbd8)) 45 | 46 | #### Breaking Changes 47 | 48 | - copy with glob by default maintains directory structure ([9dcfbd8](https://github.com/gregnb/filemanager-webpack-plugin/commit/9dcfbd8)) 49 | 50 | ## v6.1.7 (2021-09-10) 51 | 52 | #### Bug Fixes 53 | 54 | - Revert npm engines requirement ([a794942](https://github.com/gregnb/filemanager-webpack-plugin/commit/a794942)) 55 | 56 | ## v6.1.6 (2021-08-21) 57 | 58 | #### Bug Fixes 59 | 60 | - fix archiver creating invalid archives ([9204617](https://github.com/gregnb/filemanager-webpack-plugin/commit/9204617)) 61 | 62 | ## v6.1.5 (2021-08-14) 63 | 64 | #### Features 65 | 66 | - add type definitions ([1888ce4](https://github.com/gregnb/filemanager-webpack-plugin/commit/1888ce4)) 67 | 68 | ## v6.1.4 (2021-06-29) 69 | 70 | #### Bug Fixes 71 | 72 | - add `runOnceInWatchMode` option to validation schema ([6fe28f1](https://github.com/gregnb/filemanager-webpack-plugin/commit/6fe28f1)) 73 | 74 | ## v6.1.3 (2021-06-27) 75 | 76 | #### Bug Fixes 77 | 78 | - consume archiver ignore options correctly ([5c6f84b](https://github.com/gregnb/filemanager-webpack-plugin/commit/5c6f84b)), ([019f8da](https://github.com/gregnb/filemanager-webpack-plugin/commit/019f8da)) 79 | 80 | ## v6.1.2 (2021-06-25) 81 | 82 | #### Bug Fixes 83 | 84 | - remove `node:` protocol for CJS support ([da97771](https://github.com/gregnb/filemanager-webpack-plugin/commit/da97771)) 85 | 86 | ## v6.1.1 (2021-06-24) 87 | 88 | #### Bug Fixes 89 | 90 | - fix incompatibility with CJS ([e0f92a4](https://github.com/gregnb/filemanager-webpack-plugin/commit/e0f92a4)) 91 | 92 | ## v6.1.0 (2021-06-24) 93 | 94 | #### Features 95 | 96 | - add option `runOnceInWatchMode` ([50f4912](https://github.com/gregnb/filemanager-webpack-plugin/commit/50f4912)) 97 | 98 | ## v6.0.0 (2021-06-24) 99 | 100 | #### Features 101 | 102 | - support ES Modules ([ab86f31](https://github.com/gregnb/filemanager-webpack-plugin/commit/ab86f31)) 103 | 104 | #### Breaking Changes 105 | 106 | - required node versions `^12.20.0 || ^14.13.1 || >=16.0.0` ([ab86f31](https://github.com/gregnb/filemanager-webpack-plugin/commit/ab86f31)) 107 | 108 | ## v5.0.0 (2021-05-10) 109 | 110 | #### Dependency Updates 111 | 112 | - update to fs-extras v10 ([3cebee5](https://github.com/gregnb/filemanager-webpack-plugin/commit/3cebee5)) 113 | 114 | #### Breaking Changes 115 | 116 | - drop nodejs 10 support, requires node 12+ 117 | 118 | ## v4.0.0 (2021-03-07) 119 | 120 | #### Breaking Changes 121 | 122 | - drop webpack 4 ([4354e86](https://github.com/gregnb/filemanager-webpack-plugin/commit/4354e86)) 123 | 124 | ## v3.1.1 (2021-03-07) 125 | 126 | #### Bug Fixes 127 | 128 | - fix error while copying absolute glob source ([fe20026](https://github.com/gregnb/filemanager-webpack-plugin/commit/fe20026)) 129 | 130 | ## v3.1.0 (2020-01-16) 131 | 132 | #### Features 133 | 134 | - support options to forward to `del` ([fe20026](https://github.com/gregnb/filemanager-webpack-plugin/commit/fe20026)) 135 | 136 | ## v3.0.0 (2020-12-26) 137 | 138 | No notable changes since v3.0.0-beta.0 139 | 140 | ## v3.0.0-beta.0 (2020-11-19) 141 | 142 | #### Enhancements 143 | 144 | - add logs and handle errors ([1fadb46](https://github.com/gregnb/filemanager-webpack-plugin/commit/1fadb46)) 145 | 146 | ## v3.0.0-alpha.7 (2020-11-08) 147 | 148 | #### Bug Fixes 149 | 150 | - preserve timestamps while copying ([00de8e6](https://github.com/gregnb/filemanager-webpack-plugin/commit/00de8e6)), closes [#48](https://github.com/gregnb/filemanager-webpack-plugin/issues/48) 151 | 152 | ## v3.0.0-alpha.6 (2020-11-06) 153 | 154 | #### Features 155 | 156 | - add option to specify `context` ([fac605e](https://github.com/gregnb/filemanager-webpack-plugin/commit/fac605e)) 157 | 158 | ## v3.0.0-alpha.5 (2020-11-04) 159 | 160 | #### Bug Fixes 161 | 162 | - Fix copy action execution in series ([7837f41](https://github.com/gregnb/filemanager-webpack-plugin/commit/7837f41)), closes [#84](https://github.com/gregnb/filemanager-webpack-plugin/issues/84) 163 | 164 | ## v3.0.0-alpha.4 (2020-10-27) 165 | 166 | #### Bug Fixes 167 | 168 | - run all actions in the event ([8dc5bab](https://github.com/gregnb/filemanager-webpack-plugin/commit/8dc5bab)) 169 | - fix ignore behaviour in archiver action ([30aa49a](https://github.com/gregnb/filemanager-webpack-plugin/commit/30aa49a)) 170 | 171 | #### Features 172 | 173 | - add option to run tasks in series ([034f645](https://github.com/gregnb/filemanager-webpack-plugin/commit/034f645)) 174 | 175 | ## v3.0.0-alpha.3 (2020-10-26) 176 | 177 | - use native mkdir ([13720b3](https://github.com/gregnb/filemanager-webpack-plugin/commit/13720b3)) 178 | - fix onStart Event not executed in watch mode. ([cb0c180](https://github.com/gregnb/filemanager-webpack-plugin/commit/cb0c180)) 179 | - execute all tasks in series ([5fd2f16](https://github.com/gregnb/filemanager-webpack-plugin/commit/5fd2f16)) 180 | - support execution order ([cb0c180](https://github.com/gregnb/filemanager-webpack-plugin/commit/cb0c180)) 181 | 182 | #### Breaking Changes 183 | 184 | - minimum required node version v10.13 ([5884440](https://github.com/gregnb/filemanager-webpack-plugin/commit/5884440)) 185 | - file events moved into event object. See [README](https://github.com/gregnb/filemanager-webpack-plugin/blob/master/README.md) ([525f35d](https://github.com/gregnb/filemanager-webpack-plugin/commit/525f35d)) 186 | 187 | ## v3.0.0-alpha.2 (2020-10-22) 188 | 189 | #### Features 190 | 191 | - support webpack 5 192 | 193 | #### Enhancements 194 | 195 | - removed dependency `mv` and `mkdir`, use `fs-extra` instead 196 | - run tests in `ubuntu`, `windows` and `mac` 197 | - added more tests for all actions 198 | 199 | #### Breaking Changes 200 | 201 | - requires node 10.13 or above 202 | - removed verbose option, will be added in upcoming releases 203 | 204 | ## v3.0.0-alpha.1 (2020-10-04) 205 | 206 | #### Enhancements 207 | 208 | - reduce build size ([1765520](https://github.com/gregnb/filemanager-webpack-plugin/commit/1765520)) 209 | 210 | ## v3.0.0-alpha.0 (2020-10-04) 211 | 212 | #### Enhancements 213 | 214 | - replace [cpx](https://www.npmjs.com/cpx) with [cpy](https://www.npmjs.com/cpy) ([9c8eff9](https://github.com/gregnb/filemanager-webpack-plugin/commit/9c8eff9)) 215 | - remove fs-extra dependency ([9c8eff9](https://github.com/gregnb/filemanager-webpack-plugin/commit/9c8eff9)) 216 | - add schema validation for options ([fec1785](https://github.com/gregnb/filemanager-webpack-plugin/commit/fec1785)) 217 | 218 | #### Breaking Changes 219 | 220 | - drop webpack 3 support ([6d994a6](https://github.com/gregnb/filemanager-webpack-plugin/commit/6d994a6)) 221 | - update archiver to v5, refer [archiver changelog](https://github.com/archiverjs/node-archiver/blob/master/CHANGELOG.md) for more details ([f584a83](https://github.com/gregnb/filemanager-webpack-plugin/commit/f584a83)) 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2022 gregn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

FileManager Webpack Plugin

4 |

This Webpack plugin allows you to copy, archive (.zip/.tar/.tar.gz), move, delete files and directories before and after builds

5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 |
23 | 24 | ## Install 25 | 26 | ```bash 27 | npm install filemanager-webpack-plugin --save-dev 28 | # or 29 | yarn add filemanager-webpack-plugin --dev 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```js 35 | // webpack.config.js: 36 | 37 | const FileManagerPlugin = require('filemanager-webpack-plugin'); 38 | 39 | export default { 40 | // ...rest of the config 41 | plugins: [ 42 | new FileManagerPlugin({ 43 | events: { 44 | onEnd: { 45 | copy: [ 46 | { source: '/path/fromfile.txt', destination: '/path/tofile.txt' }, 47 | { source: '/path/**/*.js', destination: '/path' }, 48 | ], 49 | move: [ 50 | { source: '/path/from', destination: '/path/to' }, 51 | { source: '/path/fromfile.txt', destination: '/path/tofile.txt' }, 52 | ], 53 | delete: ['/path/to/file.txt', '/path/to/directory/'], 54 | mkdir: ['/path/to/directory/', '/another/directory/'], 55 | archive: [ 56 | { source: '/path/from', destination: '/path/to.zip' }, 57 | { source: '/path/**/*.js', destination: '/path/to.zip' }, 58 | { source: '/path/fromfile.txt', destination: '/path/to.zip' }, 59 | { source: '/path/fromfile.txt', destination: '/path/to.zip', format: 'tar' }, 60 | { 61 | source: '/path/fromfile.txt', 62 | destination: '/path/to.tar.gz', 63 | format: 'tar', 64 | options: { 65 | gzip: true, 66 | gzipOptions: { 67 | level: 1, 68 | }, 69 | globOptions: { 70 | // https://github.com/Yqnn/node-readdir-glob#options 71 | dot: true, 72 | }, 73 | }, 74 | }, 75 | ], 76 | }, 77 | }, 78 | }), 79 | ], 80 | }; 81 | ``` 82 | 83 | # Options 84 | 85 | ```js 86 | new FileManagerPlugin({ 87 | events: { 88 | onStart: {}, 89 | onEnd: {}, 90 | }, 91 | runTasksInSeries: false, 92 | runOnceInWatchMode: false, 93 | }); 94 | ``` 95 | 96 | ## File Events 97 | 98 | - `onStart`: Commands to execute before Webpack begins the bundling process 99 | 100 | **Note:** 101 | 102 | OnStart might execute twice for file changes in webpack context. 103 | 104 | ```js 105 | new webpack.WatchIgnorePlugin({ 106 | paths: [/copied-directory/], 107 | }); 108 | ``` 109 | 110 | - `onEnd`: Commands to execute after Webpack has finished the bundling process 111 | 112 | ## File Actions 113 | 114 | ### Copy 115 | 116 | Copy individual files or entire directories from a source folder to a destination folder. Also supports glob pattern. 117 | 118 | ```js 119 | [ 120 | { source: '/path/from', destination: '/path/to' }, 121 | { 122 | source: '/path/**/*.js', 123 | destination: '/path', 124 | options: { 125 | flat: false, 126 | preserveTimestamps: true, 127 | overwrite: true, 128 | }, 129 | globOptions: { 130 | dot: true, 131 | }, 132 | }, 133 | { source: '/path/fromfile.txt', destination: '/path/tofile.txt' }, 134 | { source: '/path/**/*.{html,js}', destination: '/path/to' }, 135 | { source: '/path/{file1,file2}.js', destination: '/path/to' }, 136 | ]; 137 | ``` 138 | 139 | **Options** 140 | 141 | - source[`string`] - a file or a directory or a glob 142 | - destination[`string`] - a file or a directory. 143 | - options [`object`] - copy options 144 | - globOptions [`object`] - options to forward to glob options([See available options here](https://github.com/Yqnn/node-readdir-glob#options)) 145 | 146 | **Caveats** 147 | 148 | - if source is a `glob`, destination must be a directory 149 | - if source is a `file` and destination is a directory, the file will be copied into the directory 150 | 151 | ### Delete 152 | 153 | Delete individual files or entire directories. Also supports glob pattern 154 | 155 | ```js 156 | ['/path/to/file.txt', '/path/to/directory/', '/another-path/to/directory/**.js']; 157 | ``` 158 | 159 | or 160 | 161 | ```js 162 | [ 163 | { 164 | source: '/path/to/file.txt', 165 | options: { 166 | force: true, 167 | }, 168 | }, 169 | ]; 170 | ``` 171 | 172 | ### Move 173 | 174 | Move individual files or entire directories. 175 | 176 | ```js 177 | [ 178 | { source: '/path/from', destination: '/path/to' }, 179 | { source: '/path/fromfile.txt', destination: '/path/tofile.txt' }, 180 | ]; 181 | ``` 182 | 183 | **Options** 184 | 185 | - source[`string`] - a file or a directory or a glob 186 | - destination[`string`] - a file or a directory. 187 | 188 | ### Mkdir 189 | 190 | Create a directory path with given path 191 | 192 | ```js 193 | ['/path/to/directory/', '/another/directory/']; 194 | ``` 195 | 196 | ### Archive 197 | 198 | Archive individual files or entire directories. Defaults to .zip unless 'format' and 'options' provided. Uses [node-archiver](https://github.com/archiverjs/node-archiver) 199 | 200 | ```js 201 | [ 202 | { source: '/path/from', destination: '/path/to.zip' }, 203 | { source: '/path/**/*.js', destination: '/path/to.zip' }, 204 | { source: '/path/fromfile.txt', destination: '/path/to.zip' }, 205 | { source: '/path/fromfile.txt', destination: '/path/to.zip', format: 'tar' }, 206 | { 207 | source: '/path/fromfile.txt', 208 | destination: '/path/to.tar.gz', 209 | format: 'tar', // optional 210 | options: { 211 | // see https://www.archiverjs.com/docs/archiver 212 | gzip: true, 213 | gzipOptions: { 214 | level: 1, 215 | }, 216 | globOptions: { 217 | // https://github.com/Yqnn/node-readdir-glob#options 218 | dot: true, 219 | }, 220 | }, 221 | }, 222 | ]; 223 | ``` 224 | 225 | - source[`string`] - a file or a directory or a glob 226 | - destination[`string`] - a file. 227 | - format[`string`] - Optional. Defaults to extension in destination filename. 228 | - options[`object`] - Refer https://www.archiverjs.com/archiver 229 | 230 | ### Order of execution 231 | 232 | If you need to preserve the order in which operations will run you can set the onStart and onEnd events to be Arrays. In this example below, in the onEnd event the copy action will run first, and then the delete after: 233 | 234 | ```js 235 | { 236 | onEnd: [ 237 | { 238 | copy: [{ source: './dist/bundle.js', destination: './newfile.js' }], 239 | }, 240 | { 241 | delete: ['./dist/bundle.js'], 242 | }, 243 | ]; 244 | } 245 | ``` 246 | 247 | ## Other Options 248 | 249 | - **runTasksInSeries** [`boolean`] - Run tasks in series. Defaults to false 250 | - **runOnceInWatchMode** [`boolean`] - The `onStart` event will be run only once in watch mode. Defaults to false 251 | 252 | For Example, the following will run one after the other 253 | 254 | ```js 255 | copy: [ 256 | { source: 'dist/index.html', destination: 'dir1/' }, 257 | { source: 'dir1/index.html', destination: 'dir2/' }, 258 | ]; 259 | ``` 260 | 261 | - **context** [`string`] - The directory, an absolute path, for resolving files. Defaults to [webpack context](https://webpack.js.org/configuration/entry-context/#context). 262 | 263 | ## Related plugins 264 | 265 | - [copy-asset-in-memory-webpack-plugin](https://github.com/sibiraj-s/copy-asset-in-memory-webpack-plugin) 266 | - [replace-asset-name-webpack-plugin](https://github.com/sibiraj-s/replace-asset-name-webpack-plugin) 267 | -------------------------------------------------------------------------------- /assets/filemanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregnb/filemanager-webpack-plugin/3332ff31b8434df1471137b7b4242c6a4b11653f/assets/filemanager.png -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | files: ['tests/*.test.js'], 3 | serial: true, 4 | verbose: true, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filemanager-webpack-plugin", 3 | "version": "8.0.0", 4 | "description": "Webpack plugin to copy, archive (.zip), move, delete files and directories before and after builds", 5 | "author": "gregnb", 6 | "license": "MIT", 7 | "repository": "https://github.com/gregnb/filemanager-webpack-plugin.git", 8 | "bugs": "https://github.com/gregnb/filemanager-webpack-plugin/issues", 9 | "homepage": "https://github.com/gregnb/filemanager-webpack-plugin#readme", 10 | "type": "module", 11 | "main": "./dist/index.cjs", 12 | "types": "types.d.ts", 13 | "exports": { 14 | "types": "./types.d.ts", 15 | "require": "./dist/index.cjs", 16 | "import": "./src/index.js", 17 | "default": "./src/index.js" 18 | }, 19 | "engines": { 20 | "node": ">=16.0.0" 21 | }, 22 | "contributors": [ 23 | "sibiraj-s" 24 | ], 25 | "keywords": [ 26 | "webpack", 27 | "webpack-copy-plugin", 28 | "webpack-archive-plugin", 29 | "filemanager-plugin" 30 | ], 31 | "files": [ 32 | "dist", 33 | "src", 34 | "types.d.ts" 35 | ], 36 | "scripts": { 37 | "dev": "rollup -c -w", 38 | "build": "rollup -c", 39 | "test": "ava", 40 | "prettier": "prettier . --write --ignore-path .gitignore", 41 | "prepublishOnly": "npm run build && npm run test", 42 | "prepare": "is-ci || husky install" 43 | }, 44 | "peerDependencies": { 45 | "webpack": "^5.0.0" 46 | }, 47 | "dependencies": { 48 | "@types/archiver": "^5.3.2", 49 | "archiver": "^5.3.1", 50 | "del": "^6.1.1", 51 | "fast-glob": "^3.3.0", 52 | "fs-extra": "^10.1.0", 53 | "is-glob": "^4.0.3", 54 | "normalize-path": "^3.0.0", 55 | "schema-utils": "^4.0.0" 56 | }, 57 | "devDependencies": { 58 | "@commitlint/cli": "^17.6.6", 59 | "@rollup/plugin-json": "^6.0.0", 60 | "@rollup/plugin-node-resolve": "^15.1.0", 61 | "ava": "^5.3.1", 62 | "commitlint-config-non-conventional": "^1.0.1", 63 | "html-webpack-plugin": "^5.5.3", 64 | "husky": "^8.0.3", 65 | "is-ci": "^3.0.1", 66 | "jszip": "^3.10.1", 67 | "prettier": "^2.8.8", 68 | "pretty-quick": "^3.1.3", 69 | "rollup": "^3.26.0", 70 | "webpack": "^5.88.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true, 4 | trailingComma: 'es5', 5 | bracketSpacing: true, 6 | bracketSameLine: true, 7 | arrowParens: 'always', 8 | semi: true, 9 | }; 10 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup'; 2 | import nodeResolve from '@rollup/plugin-node-resolve'; 3 | import json from '@rollup/plugin-json'; 4 | 5 | const config = defineConfig({ 6 | input: 'src/index.js', 7 | plugins: [ 8 | json(), 9 | nodeResolve({ 10 | preferBuiltins: true, 11 | }), 12 | ], 13 | external: ['normalize-path', 'archiver', 'fast-glob', 'del', 'fs-extra', 'is-glob', 'schema-utils', 'fs', 'path'], 14 | output: { 15 | file: 'dist/index.cjs', 16 | format: 'cjs', 17 | sourcemap: true, 18 | exports: 'auto', 19 | }, 20 | }); 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /src/actions/archive.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import archiver from 'archiver'; 4 | import isGlob from 'is-glob'; 5 | import fsExtra from 'fs-extra'; 6 | 7 | import pExec from '../utils/p-exec.js'; 8 | 9 | const archive = async (task, { logger }) => { 10 | const { source, absoluteSource, absoluteDestination, options = {}, context } = task; 11 | const format = task.format || path.extname(absoluteDestination).replace('.', ''); 12 | 13 | // Exclude destination file from archive 14 | const destFile = path.basename(absoluteDestination); 15 | const destDir = path.dirname(absoluteDestination); 16 | 17 | const inputGlobOptions = options.globOptions || {}; 18 | 19 | const ignore = Array.isArray(inputGlobOptions.ignore) ? [...inputGlobOptions.ignore, destFile] : [destFile]; 20 | const fileToIgnore = typeof inputGlobOptions.ignore === 'string' ? [...ignore, inputGlobOptions.ignore] : ignore; 21 | const globOptions = { ...inputGlobOptions, ignore: fileToIgnore }; 22 | 23 | await fsExtra.ensureDir(destDir); 24 | 25 | const output = fs.createWriteStream(absoluteDestination); 26 | const archive = archiver(format, options); 27 | 28 | const streamClose = () => new Promise((resolve) => output.on('close', resolve)); 29 | 30 | archive.pipe(output); 31 | 32 | logger.log(`archiving src ${source}`); 33 | 34 | if (isGlob(source)) { 35 | const opts = { 36 | ...globOptions, 37 | cwd: context, 38 | }; 39 | 40 | await archive.glob(source, opts).finalize(); 41 | } else { 42 | const sStat = fs.lstatSync(absoluteSource); 43 | 44 | if (sStat.isDirectory()) { 45 | const opts = { 46 | ...globOptions, 47 | cwd: absoluteSource, 48 | }; 49 | 50 | await archive.glob('**/*', opts).finalize(); 51 | } 52 | 53 | if (sStat.isFile()) { 54 | const opts = { 55 | name: path.basename(source), 56 | }; 57 | 58 | await archive.file(absoluteSource, opts).finalize(); 59 | } 60 | } 61 | 62 | await streamClose(); 63 | 64 | logger.info(`archive created at "${absoluteDestination}"`); 65 | }; 66 | 67 | const archiveAction = async (tasks, options) => { 68 | const { runTasksInSeries, logger } = options; 69 | 70 | const taskOptions = { 71 | logger, 72 | }; 73 | 74 | logger.debug(`processing archive tasks. tasks: ${tasks}`); 75 | await pExec(runTasksInSeries, tasks, async (task) => { 76 | try { 77 | await archive(task, taskOptions); 78 | } catch (err) { 79 | logger.error(`error while creating archive. task ${task}`); 80 | } 81 | }); 82 | logger.debug(`archive tasks complete. tasks: ${tasks}`); 83 | }; 84 | 85 | export default archiveAction; 86 | -------------------------------------------------------------------------------- /src/actions/copy.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import fsExtra from 'fs-extra'; 4 | import isGlob from 'is-glob'; 5 | 6 | import pExec from '../utils/p-exec.js'; 7 | import globCopy from '../utils/glob-copy.js'; 8 | 9 | const fsExtraDefaultOptions = { 10 | preserveTimestamps: true, 11 | }; 12 | 13 | const copy = async (task, { logger }) => { 14 | const { 15 | source, 16 | absoluteSource, 17 | destination, 18 | absoluteDestination, 19 | context, 20 | toType, 21 | options = {}, 22 | globOptions = {}, 23 | } = task; 24 | 25 | logger.log(`copying from ${source} to ${destination}`); 26 | 27 | if (isGlob(source)) { 28 | const cpOptions = { 29 | ...options, 30 | cwd: context, 31 | }; 32 | 33 | await globCopy(source, absoluteDestination, cpOptions, globOptions); 34 | } else { 35 | const isSourceFile = fs.lstatSync(absoluteSource).isFile(); 36 | 37 | // if source is a file and target is a directory 38 | // create the directory and copy the file into that directory 39 | if (isSourceFile && toType === 'dir') { 40 | await fsExtra.ensureDir(absoluteDestination); 41 | 42 | const sourceFileName = path.posix.basename(absoluteSource); 43 | const filePath = path.resolve(absoluteDestination, sourceFileName); 44 | 45 | await fsExtra.copy(absoluteSource, filePath, fsExtraDefaultOptions); 46 | return; 47 | } 48 | 49 | await fsExtra.copy(absoluteSource, absoluteDestination, fsExtraDefaultOptions); 50 | } 51 | 52 | logger.info(`copied "${source}" to "${destination}`); 53 | }; 54 | 55 | const copyAction = async (tasks, options) => { 56 | const { runTasksInSeries, logger } = options; 57 | 58 | const taskOptions = { 59 | logger, 60 | }; 61 | 62 | logger.debug(`processing copy tasks. tasks: ${tasks}`); 63 | 64 | await pExec(runTasksInSeries, tasks, async (task) => { 65 | try { 66 | await copy(task, taskOptions); 67 | } catch (err) { 68 | logger.error(`error while copying. task ${err}`); 69 | } 70 | }); 71 | logger.debug(`copy tasks complete. tasks: ${tasks}`); 72 | }; 73 | 74 | export default copyAction; 75 | -------------------------------------------------------------------------------- /src/actions/delete.js: -------------------------------------------------------------------------------- 1 | import del from 'del'; 2 | 3 | import pExec from '../utils/p-exec.js'; 4 | 5 | const deleteAction = async (tasks, taskOptions) => { 6 | const { runTasksInSeries, logger } = taskOptions; 7 | 8 | logger.debug(`processing delete tasks. tasks: ${tasks}`); 9 | 10 | await pExec(runTasksInSeries, tasks, async (task) => { 11 | const { options = {} } = task; 12 | 13 | try { 14 | await del(task.absoluteSource, options); 15 | logger.info(`deleted ${task.source}`); 16 | } catch (err) { 17 | logger.error(`unable to delete ${task.source}. ${err}`); 18 | } 19 | }); 20 | 21 | logger.debug(`delete tasks complete. tasks: ${tasks}`); 22 | }; 23 | 24 | export default deleteAction; 25 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export { default as copyAction } from './copy.js'; 2 | export { default as moveAction } from './move.js'; 3 | export { default as deleteAction } from './delete.js'; 4 | export { default as mkdirAction } from './mkdir.js'; 5 | export { default as archiveAction } from './archive.js'; 6 | -------------------------------------------------------------------------------- /src/actions/mkdir.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import pExec from '../utils/p-exec.js'; 4 | 5 | const mkdirAction = async (tasks, options) => { 6 | const { runTasksInSeries, logger } = options; 7 | 8 | logger.debug(`processing mkdir tasks. tasks: ${tasks}`); 9 | 10 | await pExec(runTasksInSeries, tasks, async (task) => { 11 | try { 12 | await fs.promises.mkdir(task.absoluteSource, { recursive: true }); 13 | logger.info(`created directory. ${task.source}`); 14 | } catch (err) { 15 | logger.error(`unable to create direcotry: ${task.source}. ${err}`); 16 | } 17 | }); 18 | 19 | logger.debug(`mkdir tasks complete. tasks: ${tasks}`); 20 | }; 21 | 22 | export default mkdirAction; 23 | -------------------------------------------------------------------------------- /src/actions/move.js: -------------------------------------------------------------------------------- 1 | import fsExtra from 'fs-extra'; 2 | 3 | import pExec from '../utils/p-exec.js'; 4 | 5 | const moveAction = async (tasks, options) => { 6 | const { runTasksInSeries, logger } = options; 7 | 8 | logger.debug(`processing move tasks. tasks: ${tasks}`); 9 | 10 | await pExec(runTasksInSeries, tasks, async (task) => { 11 | try { 12 | await fsExtra.move(task.absoluteSource, task.absoluteDestination); 13 | logger.info(`moved ${task.source} to ${task.destination}`); 14 | } catch (err) { 15 | logger.error(`unable to move ${task.source}, ${err}`); 16 | } 17 | }); 18 | 19 | logger.debug(`move tasks complete. tasks: ${tasks}`); 20 | }; 21 | 22 | export default moveAction; 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { validate } from 'schema-utils'; 3 | import normalizePath from 'normalize-path'; 4 | 5 | import optionsSchema from './options-schema.js'; 6 | import pExec from './utils/p-exec.js'; 7 | import { copyAction, moveAction, mkdirAction, archiveAction, deleteAction } from './actions/index.js'; 8 | 9 | const PLUGIN_NAME = 'FileManagerPlugin'; 10 | 11 | const defaultOptions = { 12 | events: { 13 | onStart: [], 14 | onEnd: [], 15 | }, 16 | runTasksInSeries: false, 17 | context: null, 18 | runOnceInWatchMode: false, 19 | }; 20 | 21 | const resolvePaths = (action, context) => { 22 | return action.map((task) => { 23 | if (typeof task === 'string') { 24 | const source = task; 25 | return { 26 | source, 27 | absoluteSource: path.isAbsolute(source) ? source : path.join(context, source), 28 | }; 29 | } 30 | 31 | const { source, destination } = task; 32 | 33 | if (!destination) { 34 | return { 35 | ...task, 36 | source, 37 | absoluteSource: path.isAbsolute(source) ? source : path.join(context, source), 38 | }; 39 | } 40 | 41 | const toType = /(?:\\|\/)$/.test(destination) ? 'dir' : 'file'; 42 | 43 | const absoluteSource = path.isAbsolute(source) ? source : path.join(context, source); 44 | const absoluteDestination = path.isAbsolute(destination) ? destination : path.join(context, destination); 45 | 46 | return { 47 | ...task, 48 | source: normalizePath(source), 49 | absoluteSource: normalizePath(absoluteSource), 50 | destination: normalizePath(destination), 51 | absoluteDestination: normalizePath(absoluteDestination), 52 | toType, 53 | context, 54 | }; 55 | }); 56 | }; 57 | 58 | class FileManagerPlugin { 59 | constructor(options) { 60 | validate(optionsSchema, options, { 61 | name: PLUGIN_NAME, 62 | baseDataPath: 'actions', 63 | }); 64 | 65 | this.options = { ...defaultOptions, ...options }; 66 | } 67 | 68 | async applyAction(action, actionParams) { 69 | const opts = { 70 | runTasksInSeries: this.options.runTasksInSeries, 71 | logger: this.logger, 72 | }; 73 | 74 | await action(resolvePaths(actionParams, this.context), opts); 75 | } 76 | 77 | async run(event) { 78 | for (const actionType in event) { 79 | const action = event[actionType]; 80 | 81 | switch (actionType) { 82 | case 'delete': 83 | await this.applyAction(deleteAction, action); 84 | break; 85 | 86 | case 'mkdir': 87 | await this.applyAction(mkdirAction, action); 88 | break; 89 | 90 | case 'copy': 91 | await this.applyAction(copyAction, action); 92 | break; 93 | 94 | case 'move': 95 | await this.applyAction(moveAction, action); 96 | break; 97 | 98 | case 'archive': 99 | await this.applyAction(archiveAction, action); 100 | break; 101 | 102 | default: 103 | throw Error('Unknown action'); 104 | } 105 | } 106 | } 107 | 108 | async execute(eventName) { 109 | const { events } = this.options; 110 | 111 | if (Array.isArray(events[eventName])) { 112 | const eventsArr = events[eventName]; 113 | 114 | await pExec(true, eventsArr, async (event) => await this.run(event)); 115 | return; 116 | } 117 | 118 | const event = events[eventName]; 119 | await this.run(event); 120 | } 121 | 122 | apply(compiler) { 123 | this.context = this.options.context || compiler.options.context; 124 | this.logger = compiler.getInfrastructureLogger(PLUGIN_NAME); 125 | 126 | const onStart = async () => { 127 | await this.execute('onStart'); 128 | }; 129 | 130 | const onEnd = async () => { 131 | await this.execute('onEnd'); 132 | }; 133 | 134 | compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, onStart); 135 | compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, onEnd); 136 | 137 | let watchRunCount = 0; 138 | compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async () => { 139 | if (this.options.runOnceInWatchMode && watchRunCount > 0) { 140 | return; 141 | } 142 | 143 | ++watchRunCount; 144 | await onStart(); 145 | }); 146 | } 147 | } 148 | 149 | export default FileManagerPlugin; 150 | -------------------------------------------------------------------------------- /src/options-schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'FileManagerPluginOptions', 3 | type: 'object', 4 | additionalProperties: false, 5 | definitions: { 6 | Copy: { 7 | description: 'Copy individual files or entire directories from a source folder to a destination folder', 8 | type: 'array', 9 | minItems: 1, 10 | additionalItems: true, 11 | itmes: [ 12 | { 13 | type: 'object', 14 | additionalProperties: false, 15 | properties: { 16 | source: { 17 | description: 'Copy source. A file or directory or a glob', 18 | type: 'string', 19 | minLength: 1, 20 | }, 21 | destination: { 22 | description: 'Copy destination', 23 | type: 'string', 24 | minLength: 1, 25 | }, 26 | options: { 27 | additionalProperties: false, 28 | type: 'object', 29 | description: 'Options to forward to archiver', 30 | properties: { 31 | flat: { 32 | description: 'Flatten the directory structure. All copied files will be put in the same directory', 33 | type: 'boolean', 34 | default: false, 35 | }, 36 | overwrite: { 37 | description: 'overwrite existing file or directory', 38 | type: 'boolean', 39 | default: true, 40 | }, 41 | preserveTimestamps: { 42 | description: 'Set last modification and access times to the ones of the original source files', 43 | type: 'boolean', 44 | default: false, 45 | }, 46 | }, 47 | }, 48 | globOptions: { 49 | additionalProperties: true, 50 | type: 'object', 51 | description: 'Options to forward to fast-glob', 52 | }, 53 | }, 54 | }, 55 | ], 56 | }, 57 | Delete: { 58 | description: 'Delete individual files or entire directories', 59 | type: 'array', 60 | minItems: 1, 61 | additionalItems: true, 62 | items: { 63 | anyOf: [ 64 | { 65 | type: 'object', 66 | additionalProperties: false, 67 | properties: { 68 | source: { 69 | type: 'string', 70 | minLength: 1, 71 | }, 72 | options: { 73 | additionalProperties: true, 74 | type: 'object', 75 | description: 'Options to forward to del', 76 | }, 77 | }, 78 | }, 79 | { 80 | type: 'string', 81 | minLength: 1, 82 | }, 83 | ], 84 | }, 85 | }, 86 | Move: { 87 | description: 'Move individual files or entire directories from a source folder to a destination folder', 88 | type: 'array', 89 | additionalItems: true, 90 | items: [ 91 | { 92 | type: 'object', 93 | additionalProperties: false, 94 | properties: { 95 | source: { 96 | description: 'Move source. A file or directory or a glob', 97 | type: 'string', 98 | minLength: 1, 99 | }, 100 | destination: { 101 | description: 'Move destination', 102 | type: 'string', 103 | minLength: 1, 104 | }, 105 | }, 106 | }, 107 | ], 108 | }, 109 | Mkdir: { 110 | description: 'Create Directories', 111 | type: 'array', 112 | minItems: 1, 113 | additionalItems: true, 114 | items: { 115 | type: 'string', 116 | }, 117 | }, 118 | Archive: { 119 | description: 'Archive individual files or entire directories.', 120 | type: 'array', 121 | additionalItems: true, 122 | items: [ 123 | { 124 | type: 'object', 125 | additionalProperties: false, 126 | properties: { 127 | source: { 128 | description: 'Source. A file or directory or a glob', 129 | type: 'string', 130 | minLength: 1, 131 | }, 132 | destination: { 133 | description: 'Archive destination', 134 | type: 'string', 135 | minLength: 1, 136 | }, 137 | format: { 138 | type: 'string', 139 | enum: ['zip', 'tar'], 140 | }, 141 | options: { 142 | additionalProperties: true, 143 | type: 'object', 144 | description: 'Options to forward to archiver', 145 | }, 146 | }, 147 | }, 148 | ], 149 | }, 150 | Actions: { 151 | type: 'object', 152 | additionalProperties: false, 153 | properties: { 154 | copy: { 155 | $ref: '#/definitions/Copy', 156 | }, 157 | delete: { 158 | $ref: '#/definitions/Delete', 159 | }, 160 | move: { 161 | $ref: '#/definitions/Move', 162 | }, 163 | mkdir: { 164 | $ref: '#/definitions/Mkdir', 165 | }, 166 | archive: { 167 | $ref: '#/definitions/Archive', 168 | }, 169 | }, 170 | }, 171 | }, 172 | properties: { 173 | events: { 174 | type: 'object', 175 | additionalProperties: false, 176 | properties: { 177 | onStart: { 178 | oneOf: [ 179 | { 180 | $ref: '#/definitions/Actions', 181 | }, 182 | { 183 | type: 'array', 184 | items: { 185 | $ref: '#/definitions/Actions', 186 | }, 187 | }, 188 | ], 189 | }, 190 | onEnd: { 191 | oneOf: [ 192 | { 193 | $ref: '#/definitions/Actions', 194 | }, 195 | { 196 | type: 'array', 197 | items: { 198 | $ref: '#/definitions/Actions', 199 | }, 200 | }, 201 | ], 202 | }, 203 | }, 204 | }, 205 | runTasksInSeries: { 206 | type: 'boolean', 207 | default: false, 208 | description: 'Run tasks in an action in series', 209 | }, 210 | context: { 211 | type: 'string', 212 | description: 'The directory, an absolute path, for resolving files. Defaults to webpack context', 213 | }, 214 | runOnceInWatchMode: { 215 | type: 'boolean', 216 | default: false, 217 | description: 'Run tasks only at first compilation in watch mode', 218 | }, 219 | }, 220 | }; 221 | -------------------------------------------------------------------------------- /src/utils/glob-copy.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fse from 'fs-extra'; 3 | import fg from 'fast-glob'; 4 | 5 | const defaultOptions = { 6 | flat: false, 7 | cwd: process.cwd(), 8 | }; 9 | 10 | const destPath = (pattern, file, options = defaultOptions) => { 11 | if (options.flat) { 12 | return path.posix.basename(file); 13 | } 14 | 15 | const pathArr = pattern.split('/'); 16 | const globIndex = pathArr.findIndex((item) => (item ? fg.isDynamicPattern(item) : false)); 17 | const normalized = pathArr.slice(0, globIndex).join('/'); 18 | 19 | const absolutePath = path.isAbsolute(normalized) ? normalized : path.posix.join(options.cwd, normalized); 20 | 21 | return path.relative(absolutePath, file); 22 | }; 23 | 24 | const globCopy = async (pattern, destination, options = defaultOptions, globOptions = {}) => { 25 | await fse.ensureDir(destination); 26 | 27 | const matches = await fg(pattern, { dot: true, ...globOptions, absolute: true, cwd: options.cwd }); 28 | 29 | const entries = matches.map((file) => { 30 | const destDir = path.isAbsolute(destination) ? destination : path.posix.join(options.cwd, destination); 31 | const destFileName = destPath(pattern, file, options); 32 | 33 | return { 34 | from: file, 35 | destDir, 36 | destFileName, 37 | to: path.posix.join(destDir, destFileName), 38 | }; 39 | }); 40 | 41 | const cpPromises = entries.map(async (entry) => { 42 | const copyOptions = { 43 | overwrite: true, 44 | preserveTimestamps: true, 45 | }; 46 | 47 | return fse.copy(entry.from, entry.to, copyOptions); 48 | }); 49 | 50 | return Promise.all(cpPromises); 51 | }; 52 | 53 | export default globCopy; 54 | -------------------------------------------------------------------------------- /src/utils/p-exec.js: -------------------------------------------------------------------------------- 1 | const defaultTask = async () => {}; 2 | 3 | const pExec = async (series = false, arr = [], task = defaultTask) => { 4 | if (series) { 5 | await arr.reduce(async (p, spec) => { 6 | await p; 7 | return task(spec); 8 | }, Promise.resolve(null)); 9 | return; 10 | } 11 | 12 | const pMap = arr.map(async (spec) => await task(spec)); 13 | await Promise.all(pMap); 14 | }; 15 | 16 | export default pExec; 17 | -------------------------------------------------------------------------------- /tests/archive.test.js: -------------------------------------------------------------------------------- 1 | import fs, { existsSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | 4 | import test from 'ava'; 5 | import del from 'del'; 6 | import JSZip from 'jszip'; 7 | 8 | import compile from './utils/compile.js'; 9 | import getCompiler from './utils/getCompiler.js'; 10 | import tempy from './utils/tempy.js'; 11 | 12 | import FileManagerPlugin from '../src/index.js'; 13 | 14 | const zipHasFile = async (zipPath, fileName) => { 15 | const data = await fs.promises.readFile(zipPath); 16 | const zip = await JSZip.loadAsync(data); 17 | return Object.keys(zip.files).includes(fileName); 18 | }; 19 | 20 | test.beforeEach(async (t) => { 21 | t.context.tmpdir = await tempy.dir({ suffix: 'archive-action' }); 22 | }); 23 | 24 | test.afterEach(async (t) => { 25 | await del(t.context.tmpdir); 26 | }); 27 | 28 | test('should archive(ZIP) a directory to a destination ZIP', async (t) => { 29 | const { tmpdir } = t.context; 30 | await tempy.file(tmpdir, 'file'); 31 | 32 | const zipName = tempy.getZipName(); 33 | 34 | const config = { 35 | context: tmpdir, 36 | events: { 37 | onEnd: { 38 | archive: [{ source: './', destination: zipName }], 39 | }, 40 | }, 41 | }; 42 | 43 | const compiler = getCompiler(); 44 | new FileManagerPlugin(config).apply(compiler); 45 | await compile(compiler); 46 | 47 | const zipPath = join(tmpdir, zipName); 48 | t.true(existsSync(zipPath)); 49 | }); 50 | 51 | test('should archive(ZIP) a single file to a destination ZIP', async (t) => { 52 | const { tmpdir } = t.context; 53 | await tempy.file(tmpdir, 'file'); 54 | 55 | const zipName = tempy.getZipName(); 56 | 57 | const config = { 58 | context: tmpdir, 59 | events: { 60 | onEnd: { 61 | archive: [{ source: './', destination: zipName }], 62 | }, 63 | }, 64 | }; 65 | 66 | const compiler = getCompiler(); 67 | new FileManagerPlugin(config).apply(compiler); 68 | await compile(compiler); 69 | 70 | const zipPath = join(tmpdir, zipName); 71 | t.true(existsSync(zipPath)); 72 | t.true(await zipHasFile(zipPath, 'file')); 73 | }); 74 | 75 | test('should archive(ZIP) a directory glob to destination ZIP', async (t) => { 76 | const { tmpdir } = t.context; 77 | await tempy.file(tmpdir, 'file'); 78 | 79 | const zipName = tempy.getZipName(); 80 | 81 | const config = { 82 | context: tmpdir, 83 | events: { 84 | onEnd: { 85 | archive: [{ source: '**/*', destination: zipName }], 86 | }, 87 | }, 88 | }; 89 | 90 | const compiler = getCompiler(); 91 | new FileManagerPlugin(config).apply(compiler); 92 | await compile(compiler); 93 | 94 | const zipPath = join(tmpdir, zipName); 95 | t.true(existsSync(zipPath)); 96 | t.true(await zipHasFile(zipPath, 'file')); 97 | }); 98 | 99 | test('should archive(TAR) a directory glob to destination TAR when format is provided', async (t) => { 100 | const { tmpdir } = t.context; 101 | await tempy.file(tmpdir, 'file'); 102 | 103 | const zipName = tempy.getZipName('.tar'); 104 | 105 | const config = { 106 | context: tmpdir, 107 | events: { 108 | onEnd: { 109 | archive: [{ source: '**/*', destination: zipName, format: 'tar' }], 110 | }, 111 | }, 112 | }; 113 | 114 | const compiler = getCompiler(); 115 | new FileManagerPlugin(config).apply(compiler); 116 | await compile(compiler); 117 | 118 | const zipPath = join(tmpdir, zipName); 119 | t.true(existsSync(zipPath)); 120 | }); 121 | 122 | test('should archive(TAR.GZ) a directory glob to destination TAR.GZ', async (t) => { 123 | const { tmpdir } = t.context; 124 | await tempy.file(tmpdir, 'file'); 125 | 126 | const zipName = tempy.getZipName('.tar.gz'); 127 | 128 | const config = { 129 | context: tmpdir, 130 | events: { 131 | onEnd: { 132 | archive: [ 133 | { 134 | source: '**/*', 135 | destination: zipName, 136 | format: 'tar', 137 | options: { 138 | gzip: true, 139 | gzipOptions: { 140 | level: 1, 141 | }, 142 | }, 143 | }, 144 | ], 145 | }, 146 | }, 147 | }; 148 | 149 | const compiler = getCompiler(); 150 | new FileManagerPlugin(config).apply(compiler); 151 | await compile(compiler); 152 | 153 | const zipPath = join(tmpdir, zipName); 154 | t.true(existsSync(zipPath)); 155 | }); 156 | 157 | // https://github.com/gregnb/filemanager-webpack-plugin/issues/37 158 | test('should not include the output zip into compression', async (t) => { 159 | const { tmpdir } = t.context; 160 | await tempy.file(tmpdir, 'file'); 161 | 162 | const zipName = tempy.getZipName(); 163 | 164 | const config = { 165 | context: tmpdir, 166 | events: { 167 | onEnd: { 168 | archive: [{ source: './', destination: zipName }], 169 | }, 170 | }, 171 | }; 172 | 173 | const compiler = getCompiler(); 174 | new FileManagerPlugin(config).apply(compiler); 175 | await compile(compiler); 176 | 177 | const zipPath = join(tmpdir, zipName); 178 | t.false(await zipHasFile(zipPath, zipName)); 179 | }); 180 | 181 | test('should include files in the archive', async (t) => { 182 | const { tmpdir } = t.context; 183 | await tempy.file(tmpdir, 'file1'); 184 | await tempy.file(tmpdir, 'file2'); 185 | 186 | const zipName = tempy.getZipName(); 187 | 188 | const config = { 189 | context: tmpdir, 190 | events: { 191 | onEnd: { 192 | archive: [{ source: './', destination: zipName }], 193 | }, 194 | }, 195 | }; 196 | 197 | const compiler = getCompiler(); 198 | new FileManagerPlugin(config).apply(compiler); 199 | await compile(compiler); 200 | 201 | const zipPath = join(tmpdir, zipName); 202 | 203 | t.true(await zipHasFile(zipPath, 'file1')); 204 | t.true(await zipHasFile(zipPath, 'file2')); 205 | }); 206 | 207 | test('should ignore files in the archive correclty if ignore is an array', async (t) => { 208 | const { tmpdir } = t.context; 209 | await tempy.file(tmpdir, 'file1'); 210 | await tempy.file(tmpdir, 'file2'); 211 | 212 | const zipName = tempy.getZipName(); 213 | 214 | const config = { 215 | context: tmpdir, 216 | events: { 217 | onEnd: { 218 | archive: [ 219 | { 220 | source: './', 221 | destination: zipName, 222 | options: { 223 | globOptions: { 224 | ignore: ['**/**/file2'], 225 | }, 226 | }, 227 | }, 228 | ], 229 | }, 230 | }, 231 | }; 232 | 233 | const compiler = getCompiler(); 234 | new FileManagerPlugin(config).apply(compiler); 235 | await compile(compiler); 236 | 237 | const zipPath = join(tmpdir, zipName); 238 | 239 | t.true(await zipHasFile(zipPath, 'file1')); 240 | t.false(await zipHasFile(zipPath, 'file2')); 241 | }); 242 | 243 | test('should ignore files in the archive correclty if ignore is a string', async (t) => { 244 | const { tmpdir } = t.context; 245 | await tempy.file(tmpdir, 'file1'); 246 | await tempy.file(tmpdir, 'file2'); 247 | 248 | const zipName = tempy.getZipName(); 249 | 250 | const config = { 251 | context: tmpdir, 252 | events: { 253 | onEnd: { 254 | archive: [ 255 | { 256 | source: './', 257 | destination: zipName, 258 | options: { 259 | globOptions: { 260 | ignore: ['**/**/file2'], 261 | }, 262 | }, 263 | }, 264 | ], 265 | }, 266 | }, 267 | }; 268 | 269 | const compiler = getCompiler(); 270 | new FileManagerPlugin(config).apply(compiler); 271 | await compile(compiler); 272 | 273 | const zipPath = join(tmpdir, zipName); 274 | 275 | t.true(await zipHasFile(zipPath, 'file1')); 276 | t.false(await zipHasFile(zipPath, 'file2')); 277 | }); 278 | -------------------------------------------------------------------------------- /tests/copy.test.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs'; 2 | import { basename, join, sep } from 'node:path'; 3 | 4 | import test from 'ava'; 5 | import del from 'del'; 6 | 7 | import compile from './utils/compile.js'; 8 | import getCompiler from './utils/getCompiler.js'; 9 | import tempy from './utils/tempy.js'; 10 | 11 | import FileManagerPlugin from '../src/index.js'; 12 | 13 | test.beforeEach(async (t) => { 14 | t.context.tmpdir = await tempy.dir({ suffix: 'copy-action' }); 15 | }); 16 | 17 | test.afterEach(async (t) => { 18 | await del(t.context.tmpdir); 19 | }); 20 | 21 | test('should copy files to a directory given a glob source', async (t) => { 22 | const { tmpdir } = t.context; 23 | 24 | const file1 = await tempy.file(tmpdir); 25 | const file2 = await tempy.file(tmpdir); 26 | const dirName = tempy.getDirName(); 27 | 28 | const config = { 29 | context: tmpdir, 30 | events: { 31 | onEnd: { 32 | copy: [{ source: '*', destination: dirName }], 33 | }, 34 | }, 35 | }; 36 | 37 | const compiler = getCompiler(); 38 | new FileManagerPlugin(config).apply(compiler); 39 | await compile(compiler); 40 | 41 | t.true(existsSync(join(tmpdir, dirName))); 42 | t.true(existsSync(join(tmpdir, dirName, basename(file1)))); 43 | t.true(existsSync(join(tmpdir, dirName, basename(file2)))); 44 | }); 45 | 46 | test('should copy files to a directory given a glob absolute source', async (t) => { 47 | const { tmpdir } = t.context; 48 | 49 | const file1 = await tempy.file(tmpdir); 50 | const file2 = await tempy.file(tmpdir); 51 | const dirName = tempy.getDirName(); 52 | 53 | const source = join(tmpdir, '*'); 54 | 55 | const config = { 56 | context: tmpdir, 57 | events: { 58 | onEnd: { 59 | copy: [{ source, destination: dirName }], 60 | }, 61 | }, 62 | }; 63 | 64 | const compiler = getCompiler(); 65 | new FileManagerPlugin(config).apply(compiler); 66 | await compile(compiler); 67 | 68 | t.true(existsSync(join(tmpdir, dirName))); 69 | t.true(existsSync(join(tmpdir, dirName, basename(file1)))); 70 | t.true(existsSync(join(tmpdir, dirName, basename(file2)))); 71 | }); 72 | 73 | test('should deep copy files to directory given a glob source', async (t) => { 74 | const { tmpdir } = t.context; 75 | 76 | const file1 = await tempy.file(tmpdir); 77 | const nestedDir = await tempy.dir({ root: tmpdir }); 78 | const file2 = await tempy.file(nestedDir); 79 | 80 | const dirName = tempy.getDirName(); 81 | 82 | const config = { 83 | context: tmpdir, 84 | events: { 85 | onEnd: { 86 | copy: [{ source: '**/*', destination: dirName }], 87 | }, 88 | }, 89 | }; 90 | 91 | const compiler = getCompiler(); 92 | new FileManagerPlugin(config).apply(compiler); 93 | await compile(compiler); 94 | 95 | t.true(existsSync(join(tmpdir, dirName))); 96 | t.true(existsSync(join(tmpdir, dirName, basename(file1)))); 97 | t.true(existsSync(join(tmpdir, dirName, nestedDir.split(sep).pop(), basename(file2)))); 98 | }); 99 | 100 | test('should flat copy the files to directory given a glob source', async (t) => { 101 | const { tmpdir } = t.context; 102 | 103 | const file1 = await tempy.file(tmpdir); 104 | const nestedDir = await tempy.dir({ root: tmpdir }); 105 | const file2 = await tempy.file(nestedDir); 106 | 107 | const dirName = tempy.getDirName(); 108 | 109 | const config = { 110 | context: tmpdir, 111 | events: { 112 | onEnd: { 113 | copy: [ 114 | { 115 | source: '**/*', 116 | destination: dirName, 117 | options: { 118 | flat: true, 119 | }, 120 | globOptions: {}, 121 | }, 122 | ], 123 | }, 124 | }, 125 | }; 126 | 127 | const compiler = getCompiler(); 128 | new FileManagerPlugin(config).apply(compiler); 129 | await compile(compiler); 130 | 131 | t.true(existsSync(join(tmpdir, dirName))); 132 | t.true(existsSync(join(tmpdir, dirName, basename(file1)))); 133 | t.true(existsSync(join(tmpdir, dirName, basename(file2)))); 134 | }); 135 | 136 | test(`should create destination directory if it doesn't exist and copy files`, async (t) => { 137 | const { tmpdir } = t.context; 138 | 139 | const file = await tempy.file(tmpdir); 140 | const destDir = tempy.getDirName(); 141 | 142 | const config = { 143 | context: tmpdir, 144 | events: { 145 | onEnd: { 146 | copy: [{ source: '*', destination: destDir }], 147 | }, 148 | }, 149 | }; 150 | 151 | const compiler = getCompiler(); 152 | new FileManagerPlugin(config).apply(compiler); 153 | await compile(compiler); 154 | 155 | t.true(existsSync(join(tmpdir, destDir, basename(file)))); 156 | }); 157 | 158 | test('should copy and create destination directory given a glob source with extension', async (t) => { 159 | const { tmpdir } = t.context; 160 | 161 | await tempy.file(tmpdir, 'index.html'); 162 | const destDir = tempy.getDirName(); 163 | 164 | const config = { 165 | context: tmpdir, 166 | events: { 167 | onEnd: { 168 | copy: [{ source: `**/*{fake,index}.html`, destination: destDir }], 169 | }, 170 | }, 171 | }; 172 | 173 | const compiler = getCompiler(); 174 | new FileManagerPlugin(config).apply(compiler); 175 | await compile(compiler); 176 | 177 | t.true(existsSync(join(tmpdir, destDir, 'index.html'))); 178 | t.false(existsSync(join(tmpdir, destDir, 'fake'))); 179 | }); 180 | 181 | test('should copy source file to destination file', async (t) => { 182 | const { tmpdir } = t.context; 183 | 184 | await tempy.file(tmpdir, 'index.html'); 185 | 186 | const config = { 187 | context: tmpdir, 188 | runTasksInSeries: true, 189 | events: { 190 | onEnd: { 191 | copy: [ 192 | { source: 'index.html', destination: './deep/index.html' }, 193 | { source: 'index.html', destination: './deep/deep1/index.html' }, 194 | ], 195 | }, 196 | }, 197 | }; 198 | 199 | const compiler = getCompiler(); 200 | new FileManagerPlugin(config).apply(compiler); 201 | await compile(compiler); 202 | 203 | t.true(existsSync(join(tmpdir, 'deep/index.html'))); 204 | t.true(existsSync(join(tmpdir, 'deep/deep1/index.html'))); 205 | }); 206 | 207 | test('should copy file into the directory given source is a file and destination is a directory', async (t) => { 208 | const { tmpdir } = t.context; 209 | 210 | const fileName = tempy.getFileName(); 211 | await tempy.file(tmpdir, fileName); 212 | const destDir = tempy.getDirName('/'); 213 | 214 | const config = { 215 | context: tmpdir, 216 | events: { 217 | onEnd: { 218 | copy: [{ source: fileName, destination: destDir }], 219 | }, 220 | }, 221 | }; 222 | 223 | const compiler = getCompiler(); 224 | new FileManagerPlugin(config).apply(compiler); 225 | await compile(compiler); 226 | 227 | t.true(existsSync(join(tmpdir, destDir, fileName))); 228 | }); 229 | 230 | test('should copy a file without extension to target folder', async (t) => { 231 | const { tmpdir } = t.context; 232 | 233 | const fileName = tempy.getFileName(); 234 | await tempy.file(tmpdir, fileName); 235 | const destDir = tempy.getDirName('/'); 236 | 237 | const config = { 238 | context: tmpdir, 239 | events: { 240 | onEnd: { 241 | copy: [{ source: fileName, destination: destDir }], 242 | }, 243 | }, 244 | }; 245 | 246 | const compiler = getCompiler(); 247 | new FileManagerPlugin(config).apply(compiler); 248 | await compile(compiler); 249 | 250 | t.true(existsSync(join(tmpdir, destDir, fileName))); 251 | }); 252 | 253 | test('should not copy a file that does not exist', async (t) => { 254 | const { tmpdir } = t.context; 255 | 256 | const config = { 257 | context: tmpdir, 258 | events: { 259 | onEnd: { 260 | copy: [{ source: 'doesnotexit.js', destination: 'wontexist.js' }], 261 | }, 262 | }, 263 | }; 264 | 265 | const compiler = getCompiler(); 266 | new FileManagerPlugin(config).apply(compiler); 267 | await compile(compiler); 268 | 269 | t.false(existsSync('./testing/wontexist.js')); 270 | }); 271 | -------------------------------------------------------------------------------- /tests/delete.test.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs'; 2 | 3 | import test from 'ava'; 4 | import del from 'del'; 5 | 6 | import compile from './utils/compile.js'; 7 | import getCompiler from './utils/getCompiler.js'; 8 | import tempy from './utils/tempy.js'; 9 | 10 | import FileManagerPlugin from '../src/index.js'; 11 | 12 | test.beforeEach(async (t) => { 13 | t.context.tmpdir = await tempy.dir({ suffix: 'delete-action' }); 14 | }); 15 | 16 | test.afterEach(async (t) => { 17 | await del(t.context.tmpdir); 18 | }); 19 | 20 | test('should delete file when array of strings provided in delete function', async (t) => { 21 | const { tmpdir } = t.context; 22 | 23 | const file1 = await tempy.file(tmpdir); 24 | const file2 = await tempy.file(tmpdir); 25 | const file3 = await tempy.file(tmpdir); 26 | 27 | t.true(existsSync(file1)); 28 | t.true(existsSync(file2)); 29 | t.true(existsSync(file3)); 30 | 31 | const config = { 32 | context: tmpdir, 33 | events: { 34 | onStart: { 35 | delete: [file1], 36 | }, 37 | onEnd: { 38 | delete: [file2, file3], 39 | }, 40 | }, 41 | }; 42 | 43 | const compiler = getCompiler(); 44 | new FileManagerPlugin(config).apply(compiler); 45 | await compile(compiler); 46 | 47 | t.false(existsSync(file1)); 48 | t.false(existsSync(file2)); 49 | t.false(existsSync(file3)); 50 | }); 51 | 52 | test('should support glob', async (t) => { 53 | const { tmpdir } = t.context; 54 | 55 | const file1 = await tempy.file(tmpdir); 56 | const file2 = await tempy.file(tmpdir); 57 | const file3 = await tempy.file(tmpdir); 58 | 59 | t.true(existsSync(file1)); 60 | t.true(existsSync(file2)); 61 | t.true(existsSync(file3)); 62 | 63 | const config = { 64 | context: tmpdir, 65 | events: { 66 | onEnd: { 67 | delete: ['./*'], 68 | }, 69 | }, 70 | }; 71 | 72 | const compiler = getCompiler(); 73 | new FileManagerPlugin(config).apply(compiler); 74 | 75 | await compile(compiler); 76 | 77 | t.false(existsSync(file1)); 78 | t.false(existsSync(file2)); 79 | t.false(existsSync(file3)); 80 | }); 81 | 82 | test('should accept options', async (t) => { 83 | const { tmpdir } = t.context; 84 | 85 | const file = await tempy.file(tmpdir); 86 | 87 | t.true(existsSync(file)); 88 | 89 | const config = { 90 | context: tmpdir, 91 | events: { 92 | onEnd: { 93 | delete: [{ source: './*', options: { force: true } }, './*'], 94 | }, 95 | }, 96 | }; 97 | 98 | const compiler = getCompiler(); 99 | new FileManagerPlugin(config).apply(compiler); 100 | 101 | await compile(compiler); 102 | 103 | t.false(existsSync(file)); 104 | }); 105 | -------------------------------------------------------------------------------- /tests/execution-order.test.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs'; 2 | import { join, relative } from 'node:path'; 3 | import test from 'ava'; 4 | import del from 'del'; 5 | 6 | import compile from './utils/compile.js'; 7 | import getCompiler from './utils/getCompiler.js'; 8 | import tempy from './utils/tempy.js'; 9 | 10 | import FileManagerPlugin from '../src/index.js'; 11 | 12 | test.beforeEach(async (t) => { 13 | t.context.tmpdir = await tempy.dir({ suffix: 'execution-order' }); 14 | }); 15 | 16 | test.afterEach(async (t) => { 17 | await del(t.context.tmpdir); 18 | }); 19 | 20 | test('should execute actions in a given order', async (t) => { 21 | const { tmpdir } = t.context; 22 | 23 | const mDir = await tempy.dir({ root: tmpdir }); 24 | await tempy.file(mDir, 'file'); 25 | 26 | const dirName = relative(tmpdir, mDir); 27 | 28 | const config = { 29 | context: tmpdir, 30 | events: { 31 | onStart: [ 32 | { 33 | mkdir: ['dir1', 'dir2'], 34 | }, 35 | { 36 | delete: ['dir2'], 37 | }, 38 | { 39 | copy: [{ source: `${dirName}/`, destination: 'dir-copied/' }], 40 | }, 41 | ], 42 | }, 43 | }; 44 | 45 | const compiler = getCompiler(); 46 | new FileManagerPlugin(config).apply(compiler); 47 | await compile(compiler); 48 | 49 | t.true(existsSync(join(tmpdir, 'dir1'))); 50 | t.false(existsSync(join(tmpdir, 'dir2'))); 51 | t.false(existsSync(join(tmpdir, 'dir2'))); 52 | t.true(existsSync(join(tmpdir, 'dir-copied/file'))); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | simple test 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/index.js: -------------------------------------------------------------------------------- 1 | /* simple dummy project */ 2 | 3 | const test = () => {}; 4 | 5 | export default test; 6 | -------------------------------------------------------------------------------- /tests/mkdir.test.js: -------------------------------------------------------------------------------- 1 | import { join, relative } from 'node:path'; 2 | import { existsSync } from 'node:fs'; 3 | 4 | import test from 'ava'; 5 | import del from 'del'; 6 | 7 | import compile from './utils/compile.js'; 8 | import getCompiler from './utils/getCompiler.js'; 9 | import tempy from './utils/tempy.js'; 10 | 11 | import FileManagerPlugin from '../src/index.js'; 12 | 13 | test.beforeEach(async (t) => { 14 | t.context.tmpdir = await tempy.dir({ suffix: 'mkdir-action' }); 15 | }); 16 | 17 | test.afterEach(async (t) => { 18 | await del(t.context.tmpdir); 19 | }); 20 | 21 | test('should create the given directories', async (t) => { 22 | const { tmpdir } = t.context; 23 | 24 | const config = { 25 | context: tmpdir, 26 | events: { 27 | onStart: { 28 | mkdir: ['dir1', 'dir2'], 29 | }, 30 | onEnd: { 31 | mkdir: ['dir3', 'dir4'], 32 | }, 33 | }, 34 | }; 35 | 36 | const compiler = getCompiler(); 37 | new FileManagerPlugin(config).apply(compiler); 38 | await compile(compiler); 39 | 40 | t.true(existsSync(join(tmpdir, 'dir1'))); 41 | t.true(existsSync(join(tmpdir, 'dir2'))); 42 | t.true(existsSync(join(tmpdir, 'dir3'))); 43 | t.true(existsSync(join(tmpdir, 'dir4'))); 44 | }); 45 | 46 | test('should create nested directories', async (t) => { 47 | const { tmpdir } = t.context; 48 | 49 | const config = { 50 | context: tmpdir, 51 | events: { 52 | onEnd: { 53 | mkdir: ['dir/depth1', 'dir/depth1/depth2'], 54 | }, 55 | }, 56 | runTasksInSeries: true, 57 | }; 58 | 59 | const compiler = getCompiler(); 60 | new FileManagerPlugin(config).apply(compiler); 61 | await compile(compiler); 62 | 63 | t.true(existsSync(join(tmpdir, 'dir'))); 64 | t.true(existsSync(join(tmpdir, 'dir/depth1'))); 65 | t.true(existsSync(join(tmpdir, 'dir/depth1/depth2'))); 66 | }); 67 | 68 | test('should not overwite existing directories', async (t) => { 69 | const { tmpdir } = t.context; 70 | 71 | const dir = await tempy.dir({ root: tmpdir }); 72 | const file = await tempy.file(dir, 'file'); 73 | const dirName = relative(tmpdir, dir); 74 | 75 | const config = { 76 | context: tmpdir, 77 | events: { 78 | onEnd: { 79 | mkdir: [dirName], 80 | }, 81 | }, 82 | }; 83 | 84 | const compiler = getCompiler(); 85 | new FileManagerPlugin(config).apply(compiler); 86 | await compile(compiler); 87 | 88 | t.true(existsSync(dir)); 89 | t.true(existsSync(join(file))); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/move.test.js: -------------------------------------------------------------------------------- 1 | import { join, relative, basename } from 'node:path'; 2 | import { existsSync } from 'node:fs'; 3 | 4 | import test from 'ava'; 5 | import del from 'del'; 6 | 7 | import compile from './utils/compile.js'; 8 | import getCompiler from './utils/getCompiler.js'; 9 | import tempy from './utils/tempy.js'; 10 | 11 | import FileManagerPlugin from '../src/index.js'; 12 | 13 | test.beforeEach(async (t) => { 14 | t.context.tmpdir = await tempy.dir({ suffix: 'move-action' }); 15 | }); 16 | 17 | test.afterEach(async (t) => { 18 | await del(t.context.tmpdir); 19 | }); 20 | 21 | test('should move files from source to destination', async (t) => { 22 | const { tmpdir } = t.context; 23 | 24 | const dir = await tempy.dir({ root: tmpdir }); 25 | const file = await tempy.file(dir, 'file'); 26 | 27 | const srcDir = relative(tmpdir, dir); 28 | const destDir = tempy.getDirName(); 29 | 30 | const config = { 31 | context: tmpdir, 32 | events: { 33 | onEnd: { 34 | move: [{ source: srcDir, destination: destDir }], 35 | }, 36 | }, 37 | }; 38 | 39 | const compiler = getCompiler(); 40 | new FileManagerPlugin(config).apply(compiler); 41 | await compile(compiler); 42 | 43 | t.false(existsSync(join(tmpdir, srcDir))); 44 | t.true(existsSync(join(tmpdir, destDir))); 45 | t.true(existsSync(join(tmpdir, destDir, basename(file)))); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/multi-actions.test.js: -------------------------------------------------------------------------------- 1 | import { basename, join } from 'node:path'; 2 | import { existsSync } from 'node:fs'; 3 | 4 | import test from 'ava'; 5 | import del from 'del'; 6 | 7 | import compile from './utils/compile.js'; 8 | import getCompiler from './utils/getCompiler.js'; 9 | import tempy from './utils/tempy.js'; 10 | 11 | import FileManagerPlugin from '../src/index.js'; 12 | 13 | test.beforeEach(async (t) => { 14 | t.context.tmpdir = await tempy.dir({ suffix: 'multi-action' }); 15 | }); 16 | 17 | test.afterEach(async (t) => { 18 | await del(t.context.tmpdir); 19 | }); 20 | 21 | test('should execute given actions in an event', async (t) => { 22 | const { tmpdir } = t.context; 23 | 24 | const dirName1 = tempy.getDirName(); 25 | const destDir = tempy.getDirName(); 26 | const file = await tempy.file(tmpdir, 'file'); 27 | 28 | const config = { 29 | context: tmpdir, 30 | events: { 31 | onEnd: { 32 | mkdir: [dirName1], 33 | copy: [{ source: basename(file), destination: `${destDir}/file-copied` }], 34 | }, 35 | }, 36 | }; 37 | 38 | const compiler = getCompiler(); 39 | new FileManagerPlugin(config).apply(compiler); 40 | await compile(compiler); 41 | 42 | t.true(existsSync(join(tmpdir, dirName1))); 43 | t.true(existsSync(file)); 44 | t.true(existsSync(join(tmpdir, destDir, 'file-copied'))); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/other-options.test.js: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { existsSync } from 'node:fs'; 3 | import test from 'ava'; 4 | import del from 'del'; 5 | 6 | import compile from './utils/compile.js'; 7 | import getCompiler from './utils/getCompiler.js'; 8 | import getFixtruesDir from './utils/getFixturesDir.js'; 9 | import tempy from './utils/tempy.js'; 10 | 11 | import FileManagerPlugin from '../src/index.js'; 12 | 13 | const fixturesDir = getFixtruesDir(); 14 | 15 | test.beforeEach(async (t) => { 16 | t.context.tmpdir = await tempy.dir({ suffix: 'other-options' }); 17 | }); 18 | 19 | test.afterEach(async (t) => { 20 | await del(t.context.tmpdir); 21 | }); 22 | 23 | test(`should tasks in sequence with option 'runTasksInSeries'`, async (t) => { 24 | const { tmpdir } = t.context; 25 | 26 | const dir1 = tempy.getDirName('/'); 27 | const dir2 = tempy.getDirName('/'); 28 | 29 | const config = { 30 | context: fixturesDir, 31 | runTasksInSeries: true, 32 | events: { 33 | onEnd: { 34 | copy: [ 35 | { source: 'dist/index.html', destination: join(tmpdir, dir1) }, 36 | { source: join(tmpdir, dir1, 'index.html'), destination: join(tmpdir, dir2) }, 37 | ], 38 | }, 39 | }, 40 | }; 41 | 42 | const compiler = getCompiler(fixturesDir); 43 | new FileManagerPlugin(config).apply(compiler); 44 | await compile(compiler); 45 | 46 | t.true(existsSync(join(tmpdir, dir1, 'index.html'))); 47 | t.true(existsSync(join(tmpdir, dir2, 'index.html'))); 48 | }); 49 | 50 | test(`should resolve files from given 'context'`, async (t) => { 51 | const distDir = join(fixturesDir, 'dist'); 52 | 53 | const config = { 54 | events: { 55 | onEnd: { 56 | copy: [{ source: 'index.html', destination: 'index.copied.html' }], 57 | }, 58 | }, 59 | context: distDir, 60 | }; 61 | 62 | const compiler = getCompiler(); 63 | new FileManagerPlugin(config).apply(compiler); 64 | await compile(compiler); 65 | 66 | t.true(existsSync(join(distDir, 'index.html'))); 67 | t.true(existsSync(join(distDir, 'index.copied.html'))); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/utils/compile.js: -------------------------------------------------------------------------------- 1 | const compile = (compiler) => { 2 | return new Promise((resolve, reject) => { 3 | compiler.run((err, stats) => { 4 | if (err) { 5 | return reject(err); 6 | } 7 | 8 | if (stats.hasErrors()) { 9 | return reject(new Error(stats.toString())); 10 | } 11 | 12 | return resolve(stats); 13 | }); 14 | }); 15 | }; 16 | 17 | export default compile; 18 | -------------------------------------------------------------------------------- /tests/utils/getCompiler.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import webpack from 'webpack'; 3 | import HTMLPlugin from 'html-webpack-plugin'; 4 | 5 | import getFixtruesDir from './getFixturesDir.js'; 6 | 7 | const fixturesDir = getFixtruesDir(); 8 | 9 | const getCompiler = () => { 10 | const compiler = webpack({ 11 | context: fixturesDir, 12 | mode: 'production', 13 | entry: path.resolve(fixturesDir), 14 | output: { 15 | path: path.resolve(fixturesDir, 'dist'), 16 | filename: 'js/bunlde-[contenthash].js', 17 | clean: true, 18 | }, 19 | plugins: [new HTMLPlugin()], 20 | }); 21 | 22 | return compiler; 23 | }; 24 | 25 | export default getCompiler; 26 | -------------------------------------------------------------------------------- /tests/utils/getFixturesDir.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 5 | 6 | const getFixtruesDir = () => { 7 | return path.resolve(__dirname, '..', 'fixtures'); 8 | }; 9 | 10 | export default getFixtruesDir; 11 | -------------------------------------------------------------------------------- /tests/utils/tempy.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import crypto from 'node:crypto'; 4 | 5 | import getFixtruesDir from './getFixturesDir.js'; 6 | 7 | const fixturesDir = getFixtruesDir(); 8 | 9 | const getRandomId = () => crypto.randomBytes(16).toString('hex'); 10 | const getZipName = (ext = '.zip') => `test-${getRandomId()}${ext}`; 11 | const getDirName = (last = '') => `dir-${getRandomId()}${last}`; 12 | const getFileName = (last = '') => `file-${getRandomId()}${last}`; 13 | 14 | const dir = async ({ root = fixturesDir, suffix = 'random' }) => { 15 | const tmpDir = await fs.promises.mkdtemp(path.join(root, `tmp-${suffix}-`)); 16 | return tmpDir; 17 | }; 18 | 19 | const file = async (dir, fileName = getFileName()) => { 20 | const filePath = path.join(dir, fileName); 21 | await fs.promises.writeFile(filePath, 'lorem-ipsum', 'utf-8'); 22 | return filePath; 23 | }; 24 | 25 | export default { 26 | dir, 27 | file, 28 | getRandomId, 29 | getZipName, 30 | getDirName, 31 | getFileName, 32 | }; 33 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import type { ArchiverOptions } from 'archiver'; 2 | import type { Options as DelOptions } from 'del'; 3 | import type { Compiler, WebpackPluginInstance } from 'webpack'; 4 | import type { CopyOptions } from 'fs-extra'; 5 | import type { Options as FgOptions } from 'fast-glob'; 6 | 7 | type FsCopyOptions = Pick; 8 | 9 | interface CopyActionOptions extends FsCopyOptions { 10 | /** 11 | * Flatten directory structure. All copied files will be put in the same directory. 12 | * disabled by default 13 | */ 14 | flat: boolean; 15 | } 16 | 17 | /** Copy individual files or entire directories from a source folder to a destination folder */ 18 | type Copy = { 19 | /** Copy source. A file or directory or a glob */ 20 | source: string; 21 | /** Copy destination */ 22 | destination: string; 23 | /** Copy Options */ 24 | options?: CopyActionOptions; 25 | /** Glob options */ 26 | globOptions?: Omit; 27 | }[]; 28 | 29 | /** Delete individual files or entire directories */ 30 | type Delete = ( 31 | | { 32 | /** A folder or file or a glob to delete */ 33 | source: string; 34 | /** Options to forward to del */ 35 | options: DelOptions; 36 | } 37 | | string 38 | )[]; 39 | 40 | /** Move individual files or entire directories from a source folder to a destination folder */ 41 | type Move = { 42 | /** Move source. A file or directory or a glob */ 43 | source: string; 44 | /** Move destination */ 45 | destination: string; 46 | }[]; 47 | 48 | /** Create Directories */ 49 | type Mkdir = string[]; 50 | 51 | /** Archive individual files or entire directories. */ 52 | type Archive = { 53 | /** Source. A file or directory or a glob */ 54 | source: string; 55 | /** Archive destination */ 56 | destination: string; 57 | format?: 'zip' | 'tar'; 58 | options?: ArchiverOptions | { globOptions: ReaddirGlobOptions }; 59 | }[]; 60 | 61 | /** {@link https://github.com/Yqnn/node-readdir-glob#options} */ 62 | interface ReaddirGlobOptions { 63 | /** Glob pattern or Array of Glob patterns to match the found files with. A file has to match at least one of the provided patterns to be returned. */ 64 | pattern?: string | string[]; 65 | /** Glob pattern or Array of Glob patterns to exclude matches. If a file or a folder matches at least one of the provided patterns, it's not returned. It doesn't prevent files from folder content to be returned. Note: ignore patterns are always in dot:true mode. */ 66 | ignore?: string | string[]; 67 | /** Glob pattern or Array of Glob patterns to exclude folders. If a folder matches one of the provided patterns, it's not returned, and it's not explored: this prevents any of its children to be returned. Note: skip patterns are always in dot:true mode. */ 68 | skip?: string | string[]; 69 | /** Add a / character to directory matches. */ 70 | mark?: boolean; 71 | /** Set to true to stat all results. This reduces performance. */ 72 | stat?: boolean; 73 | /** When an unusual error is encountered when attempting to read a directory, a warning will be printed to stderr. Set the silent option to true to suppress these warnings. */ 74 | silent?: boolean; 75 | /** Do not match directories, only files. */ 76 | nodir?: boolean; 77 | /** Follow symlinked directories. Note that requires to stat all results, and so reduces performance. */ 78 | follow?: boolean; 79 | /** Allow pattern to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot. */ 80 | dot?: boolean; 81 | /** Disable ** matching against multiple folder names. */ 82 | noglobstar?: boolean; 83 | /** Perform a case-insensitive match. Note: on case-insensitive filesystems, non-magic patterns will match by default, since stat and readdir will not raise errors. */ 84 | nocase?: boolean; 85 | /** Perform a basename-only match if the pattern does not contain any slash characters. That is, *.js would be treated as equivalent to ** /*.js, matching all js files in all directories. */ 86 | matchBase?: boolean; 87 | } 88 | 89 | interface Actions { 90 | copy?: Copy; 91 | delete?: Delete; 92 | move?: Move; 93 | mkdir?: Mkdir; 94 | archive?: Archive; 95 | } 96 | 97 | interface Options { 98 | events?: { 99 | /** 100 | * Commands to execute before Webpack begins the bundling process 101 | * Note: OnStart might execute twice for file changes in webpack context. 102 | */ 103 | onStart?: Actions | Actions[]; 104 | /** 105 | * Commands to execute after Webpack has finished the bundling process 106 | */ 107 | onEnd?: Actions | Actions[]; 108 | }; 109 | /** 110 | * Run tasks in an action in series 111 | */ 112 | runTasksInSeries?: boolean; 113 | /** 114 | * Run tasks only at first compilation in watch mode 115 | */ 116 | runOnceInWatchMode?: boolean; 117 | /** 118 | * The directory, an absolute path, for resolving files. Defaults to webpack context 119 | */ 120 | context?: string; 121 | } 122 | 123 | declare class FileManagerPlugin implements WebpackPluginInstance { 124 | constructor(options?: Options); 125 | apply(compiler: Compiler): void; 126 | } 127 | 128 | export default FileManagerPlugin; 129 | --------------------------------------------------------------------------------