├── .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 |
--------------------------------------------------------------------------------