├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── .npmignore ├── src ├── index.js ├── info.js ├── utils.js ├── logger.js ├── items.js ├── path.js ├── terminal.js └── fs.js ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature-request.md │ └── bug-report.md └── PULL_REQUEST_TEMPLATE.md ├── test ├── unit │ ├── path │ │ ├── path.js │ │ ├── type.js │ │ └── to-absolute.js │ ├── fs │ │ ├── get-stat-sync.js │ │ ├── unlink-file-sync.js │ │ ├── unlink-folder-sync.js │ │ └── get-items-sync.js │ ├── plugin │ │ ├── apply.js │ │ ├── new.js │ │ ├── apply-hook.js │ │ ├── handle-test.js │ │ ├── handle-remove.js │ │ └── get-items-for-removing.js │ ├── terminal │ │ └── generate-message.js │ └── items │ │ ├── remove-root.js │ │ └── remove-unnecessary.js ├── webpack.js ├── acceptance │ ├── large.js │ └── small.js └── setup.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── types └── index.d.ts ├── .eslintrc.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders. 2 | .nyc_output 3 | node_modules 4 | coverage 5 | 6 | # Files. 7 | *.log 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | .nyc_output 4 | node_modules 5 | coverage 6 | test 7 | .eslintrc.yaml 8 | .gitignore 9 | TODO 10 | yarn.lock 11 | *.log 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "files.insertFinalNewline": true, 6 | "files.exclude": { 7 | "**/node_modules": true, 8 | "**/.nyc_output": true 9 | }, 10 | "search.exclude": { 11 | "**/node_modules": true 12 | }, 13 | "eslint.enable": true 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * @module RemoveFilesWebpackPlugin 6 | * @version 1.5.0 7 | * @file A plugin for webpack that removes files and folders before and after compilation. 8 | * @author Sergey Kuznetsov 9 | * @license MIT 10 | * @see https://github.com/Amaimersion/remove-files-webpack-plugin 11 | */ 12 | 13 | 14 | module.exports = require('./plugin'); 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Have any questions regarding the project? 4 | --- 5 | 6 | 7 | 11 | 12 | 13 | # Question 14 | 15 | 16 | 17 | 18 | ## Summary 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Information about plugin. 6 | */ 7 | class Info { 8 | /** 9 | * Name of plugin. 10 | */ 11 | static get name() { 12 | return 'remove-files-plugin'; 13 | } 14 | 15 | /** 16 | * Version of plugin. 17 | */ 18 | static get version() { 19 | return '1.5.0'; 20 | } 21 | 22 | /** 23 | * Name with version. 24 | */ 25 | static get fullName() { 26 | return `${this.name}@${this.version}`; 27 | } 28 | } 29 | 30 | 31 | module.exports = Info; 32 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Small utils which don't deserve separate class. 6 | */ 7 | class Utils { 8 | /** 9 | * Escapes a string for pasting in RegExp. 10 | * 11 | * - means `\` will be in RegExp, like this 12 | * `/D:\/\(test/`. 13 | * - you should insert result in RegExp like 14 | * a string, not like expression. 15 | * 16 | * @param {string} string 17 | * A string for escaping. 18 | * 19 | * @returns {string} 20 | * Escaped string. 21 | */ 22 | static escapeForRegExp(string) { 23 | return string.replace( 24 | // eslint-disable-next-line no-useless-escape 25 | /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, 26 | '\\$&' 27 | ); 28 | } 29 | } 30 | 31 | 32 | module.exports = Utils; 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Want to add or change something in the project? 4 | --- 5 | 6 | 7 | 11 | 12 | 13 | # Feature Request 14 | 15 | 16 | 17 | 18 | ## Summary 19 | 20 | 21 | 22 | 23 | ## Implementation 24 | 25 | 26 | 27 | 28 | ## Description 29 | 30 | 31 | 32 | 33 | ## Examples 34 | 35 | 36 | 37 | 38 | ## Additional Information 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/unit/path/path.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const expect = require('chai').expect; 3 | const Path = require('../../../src/path'); 4 | 5 | 6 | describe('unit', function () { 7 | describe('path', function () { 8 | describe('path', function () { 9 | describe('getter', function () { 10 | const instance = new Path(); 11 | 12 | // should go before any setters usage! 13 | it('auto', function () { 14 | expect(instance.path.sep).to.equal(path.sep); 15 | }); 16 | 17 | it('win32', function () { 18 | instance.type = 'win32'; 19 | 20 | expect(instance.path.sep).to.equal(path.win32.sep); 21 | }); 22 | 23 | it('posix', function () { 24 | instance.type = 'posix'; 25 | 26 | expect(instance.path.sep).to.equal(path.posix.sep); 27 | }); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/unit/fs/get-stat-sync.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const expect = require('chai').expect; 3 | const Fs = require('../../../src/fs'); 4 | 5 | 6 | describe('unit', function () { 7 | describe('fs', function () { 8 | describe('.getStatSync()', function () { 9 | const instance = new Fs(); 10 | 11 | it('should return undefined if path not exists', function () { 12 | const result = instance.getStatSync('D:/fs_test/not exists'); 13 | 14 | expect(result).to.be.undefined; 15 | }); 16 | 17 | it('should return stats if folder exists', function () { 18 | const result = instance.getStatSync('D:/fs_test/fs-test'); 19 | 20 | expect(result).to.be.an.instanceOf(fs.Stats); 21 | }); 22 | 23 | it('should return stats if file exists', function () { 24 | const result = instance.getStatSync('D:/fs_test/test_1.txt'); 25 | 26 | expect(result).to.be.an.instanceOf(fs.Stats); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sergey Kuznetsov 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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | # Pull Request 8 | 9 | 10 | 11 | 12 | ## Type 13 | 14 | 15 | 16 | 17 | ## Description 18 | 19 | 20 | 21 | 22 | ## Checklist 23 | 24 | 25 | 26 | - [ ] this PR contains breaking changes 27 | - [ ] `yarn test` passes 28 | - [ ] `yarn eslint` passes 29 | - [ ] changelog is changed 30 | - [ ] documentation is changed 31 | - [ ] tests are changed or added 32 | 33 | 34 | ## Additional Information 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/unit/path/type.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Path = require('../../../src/path'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('path', function () { 7 | describe('type', function () { 8 | describe('getter', function () { 9 | const instance = new Path(); 10 | 11 | it('works once', function () { 12 | instance._type = 'win32'; 13 | 14 | expect(instance.type).to.equal('win32'); 15 | }); 16 | 17 | it('works twice', function () { 18 | instance._type = 'posix'; 19 | 20 | expect(instance.type).to.equal('posix'); 21 | }); 22 | }); 23 | 24 | describe('setter', function () { 25 | const instance = new Path(); 26 | 27 | it('works once', function () { 28 | instance.type = 'win32'; 29 | 30 | expect(instance.type).to.equal('win32'); 31 | }); 32 | 33 | it('works twice', function () { 34 | instance.type = 'posix'; 35 | 36 | expect(instance.type).to.equal('posix'); 37 | }); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Did something not work as expected? 4 | --- 5 | 6 | 7 | 11 | 12 | 13 | # Bug Report 14 | 15 | 16 | 17 | 18 | ## Environment 19 | 20 | 21 | 22 | - plugin version: 23 | - system and its version: 24 | - webpack version: 25 | - node version: 26 | 27 | 28 | ## Expected Behavior 29 | 30 | 31 | 32 | 33 | ## Actual Behavior 34 | 35 | 36 | 37 | 38 | ## Configuration 39 | 40 | 41 | 42 | ```javascript 43 | new RemovePlugin({ 44 | before: {}, 45 | watch: {}, 46 | after: {} 47 | }) 48 | ``` 49 | 50 | 51 | ## Debug Log 52 | 53 | 54 | 55 | ``` 56 | remove-files-plugin@1.2.2: 57 | v4 hook registered – "beforeRun" 58 | v4 hook registered – "watchRun" 59 | v4 hook registered – "afterEmit" 60 | ... 61 | ``` 62 | 63 | 64 | ## Additional Information 65 | 66 | 67 | -------------------------------------------------------------------------------- /test/unit/plugin/apply.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Webpack = require('../../webpack'); 3 | const Plugin = require('../../../src/plugin'); 4 | 5 | 6 | describe('unit', function () { 7 | describe('plugin', function () { 8 | describe('.apply()', function () { 9 | it('should work with webpack v4', function () { 10 | const test = () => { 11 | const webpackV4 = new Webpack.EmulatedWebpackCompiler.v4(); 12 | const instance = new Plugin({ 13 | before: {} 14 | }); 15 | 16 | instance.apply(webpackV4); 17 | webpackV4.runAllHooks(); 18 | }; 19 | 20 | expect(test).not.to.throw(); 21 | }); 22 | 23 | it('should work with webpack v3', function () { 24 | const test = () => { 25 | const webpackV3 = new Webpack.EmulatedWebpackCompiler.v3(); 26 | const plugin = new Plugin({ 27 | before: {} 28 | }); 29 | 30 | plugin.apply(webpackV3); 31 | webpackV3.runAllHooks(); 32 | }; 33 | 34 | expect(test).not.to.throw(); 35 | }); 36 | 37 | it('should not work with invalid webpack', function () { 38 | const test = () => { 39 | const webpackInvalid = {}; 40 | const plugin = new Plugin({ 41 | before: {} 42 | }); 43 | 44 | plugin.apply(webpackInvalid); 45 | }; 46 | 47 | expect(test).to.throw(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/plugin/new.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Plugin = require('../../../src/plugin'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('plugin', function () { 7 | describe('new()', function () { 8 | it('should throw error when calling without parameters', function () { 9 | const test = () => { 10 | new Plugin(); 11 | }; 12 | 13 | expect(test).to.throw(); 14 | }); 15 | 16 | it('should throw error when calling without either "before", "after" or "watch" parameters', function () { 17 | const test = () => { 18 | new Plugin({ 19 | test: {} 20 | }); 21 | }; 22 | 23 | expect(test).to.throw(); 24 | }); 25 | 26 | it('should create when calling with "before" parameter', function () { 27 | const test = () => { 28 | new Plugin({ 29 | before: {} 30 | }); 31 | }; 32 | 33 | expect(test).not.to.throw(); 34 | }); 35 | 36 | it('should create when calling with "after" parameter', function () { 37 | const test = () => { 38 | new Plugin({ 39 | after: {} 40 | }); 41 | }; 42 | 43 | expect(test).not.to.throw(); 44 | }); 45 | 46 | it('should create when calling with "watch" parameter', function () { 47 | const test = () => { 48 | new Plugin({ 49 | watch: {} 50 | }); 51 | }; 52 | 53 | expect(test).not.to.throw(); 54 | }); 55 | 56 | it('should create when calling with all "before", "after" and "watch" parameters', function () { 57 | const test = () => { 58 | new Plugin({ 59 | before: {}, 60 | after: {}, 61 | watch: {} 62 | }); 63 | }; 64 | 65 | expect(test).not.to.throw(); 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/unit/terminal/generate-message.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Terminal = require('../../../src/terminal'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('terminal', function () { 7 | describe('.generateMessage()', function () { 8 | it('should return white default text', function () { 9 | const text = 'Text text'; 10 | const result = Terminal.generateMessage(text, { 11 | endDot: false, 12 | color: 'white' 13 | }); 14 | const correct = Terminal.colorize(text, 'white'); 15 | 16 | expect(result).to.be.equal(correct); 17 | }); 18 | 19 | it('should return red default text', function () { 20 | const text = 'Text text'; 21 | const result = Terminal.generateMessage(text, { 22 | endDot: false, 23 | color: 'red' 24 | }); 25 | const correct = Terminal.colorize(text, 'red'); 26 | 27 | expect(result).to.be.equal(correct); 28 | }); 29 | 30 | it('should return yellow empty text', function () { 31 | const text = ''; 32 | const result = Terminal.generateMessage(text, { 33 | endDot: false, 34 | color: 'yellow' 35 | }); 36 | const correct = Terminal.colorize(text, 'yellow'); 37 | 38 | expect(result).to.be.equal(correct); 39 | }); 40 | 41 | it('should append end dot if not already presented', function () { 42 | const text = 'Text text'; 43 | const result = Terminal.generateMessage(text, { 44 | endDot: true, 45 | color: 'white' 46 | }); 47 | const correct = Terminal.colorize(text + '.', 'white'); 48 | 49 | expect(result).to.be.equal(correct); 50 | }); 51 | 52 | it('should not append end dot if already presented', function () { 53 | const text = 'Text text.'; 54 | const result = Terminal.generateMessage(text, { 55 | endDot: true, 56 | color: 'white' 57 | }); 58 | const correct = Terminal.colorize(text, 'white'); 59 | 60 | expect(result).to.be.equal(correct); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/unit/fs/unlink-file-sync.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const expect = require('chai').expect; 3 | const Fs = require('../../../src/fs'); 4 | 5 | 6 | describe('unit', function () { 7 | describe('fs', function () { 8 | describe('.unlinkFileSync()', function () { 9 | const instance = new Fs(); 10 | 11 | it('should not throw error in case of permanent removing if file not exists', function () { 12 | const method = () => instance.unlinkFileSync({ 13 | pth: 'D:/fs_test_file_remove/not exists.txt', 14 | toTrash: false 15 | }); 16 | 17 | expect(method).to.not.throw(); 18 | }); 19 | 20 | it('should not throw error in case of moving to trash if file not exists', function () { 21 | const method = () => instance.unlinkFileSync({ 22 | pth: 'D:/fs_test_file_remove/not exists.txt', 23 | toTrash: true 24 | }); 25 | 26 | expect(method).to.not.throw(); 27 | }); 28 | 29 | it('should permanently remove file', function () { 30 | instance.unlinkFileSync({ 31 | pth: 'D:/fs_test_file_remove/1 pa)t-h (t [e]s {t} + file.name.txt', 32 | toTrash: false, 33 | onError: (error) => { 34 | throw new Error(error); 35 | } 36 | }); 37 | 38 | const result = fs.existsSync('D:/fs_test_file_remove/1 pa)t-h (t [e]s {t} + file.name.txt'); 39 | const correct = false; 40 | 41 | expect(result).to.be.equal(correct); 42 | }); 43 | 44 | it('should move file to trash', function (done) { 45 | instance.unlinkFileSync({ 46 | pth: 'D:/fs_test_file_remove/2 pa)t-h (t [e]s {t} + file.name.txt', 47 | toTrash: true, 48 | rightTrashCallbacks: true, 49 | onSuccess: () => { 50 | const result = fs.existsSync('D:/fs_test_file_remove/2 pa)t-h (t [e]s {t} + file.name.txt'); 51 | const correct = false; 52 | 53 | expect(result).to.be.equal(correct); 54 | done(); 55 | }, 56 | onError: (error) => { 57 | done(error); 58 | } 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remove-files-webpack-plugin", 3 | "version": "1.5.0", 4 | "description": "A plugin for webpack that removes files and folders before and after compilation.", 5 | "main": "./src/index.js", 6 | "types": "./types/index.d.ts", 7 | "scripts": { 8 | "test": "mocha --recursive --reporter nyan --check-leaks", 9 | "test-coverage": "nyc --reporter=html --reporter=text yarn test --reporter list", 10 | "eslint": "./node_modules/.bin/eslint ./src/**/*.js", 11 | "eslint-codeframe": "yarn eslint --format codeframe", 12 | "eslint-fix": "yarn eslint --fix" 13 | }, 14 | "homepage": "https://github.com/Amaimersion/remove-files-webpack-plugin/blob/master/README.md", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Amaimersion/remove-files-webpack-plugin.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/Amaimersion/remove-files-webpack-plugin/issues" 21 | }, 22 | "author": { 23 | "name": "Sergey Kuznetsov", 24 | "url": "https://github.com/Amaimersion" 25 | }, 26 | "license": "MIT", 27 | "dependencies": { 28 | "@types/webpack": "5.28.0", 29 | "trash": "7.2.0" 30 | }, 31 | "devDependencies": { 32 | "chai": "^4.2.0", 33 | "eslint": "^6.8.0", 34 | "mocha": "^7.0.1", 35 | "nyc": "^15.0.0" 36 | }, 37 | "peerDependencies": { 38 | "webpack": ">=2.2.0" 39 | }, 40 | "engines": { 41 | "node": ">=8.3.0" 42 | }, 43 | "keywords": [ 44 | "webpack", 45 | "webpack-plugin", 46 | "webpack-compilation", 47 | "webpack-files", 48 | "compilation", 49 | "plugin", 50 | "files", 51 | "build-automation", 52 | "clean", 53 | "cleanup", 54 | "clean-plugin", 55 | "clean-webpack-plugin", 56 | "clean-files-plugin", 57 | "clean-files-webpack-plugin", 58 | "remove", 59 | "remove-plugin", 60 | "remove-webpack-plugin", 61 | "remove-files-plugin", 62 | "remove-files-webpack-plugin", 63 | "delete", 64 | "delete-plugin", 65 | "delete-webpack-plugin", 66 | "delete-files-plugin", 67 | "delete-files-webpack-plugin", 68 | "вебпак", 69 | "вебпак-автоматизация", 70 | "вебпак-файлы", 71 | "вебпак-компиляция", 72 | "компиляция", 73 | "удалить", 74 | "удаление", 75 | "очистка" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /test/unit/items/remove-root.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Items = require('../../../src/items'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('items', function () { 7 | describe('.removeRoot()', function () { 8 | const instance = new Items(); 9 | 10 | it('should remove backward slash in directories', function () { 11 | instance.directories = [ 12 | 'D:\\items_test\\', 13 | 'D:\\items_test\\test_1', 14 | 'D:\\items_test\\test_2\\test_3' 15 | ]; 16 | instance.files = []; 17 | 18 | instance.removeRoot('D:\\items_test\\'); 19 | 20 | expect(instance.directories).to.have.members([ 21 | '', 22 | 'test_1', 23 | 'test_2\\test_3' 24 | ]); 25 | expect(instance.files).to.have.members([]); 26 | }); 27 | 28 | it('should remove forward slash in files', function () { 29 | instance.directories = []; 30 | instance.files = [ 31 | '/items_test/test_1.txt', 32 | '/items_test/test_1/test_2.bin', 33 | '/items_test/test_2/test_3/test_3.txt' 34 | ]; 35 | 36 | instance.removeRoot('/items_test/'); 37 | 38 | expect(instance.directories).to.have.members([]); 39 | expect(instance.files).to.have.members([ 40 | 'test_1.txt', 41 | 'test_1/test_2.bin', 42 | 'test_2/test_3/test_3.txt' 43 | ]); 44 | }); 45 | 46 | it('should remove mixed slash in both directories and files', function () { 47 | instance.directories = [ 48 | 'D:\\items_test\\', 49 | 'D:\\items_test\\items_test', 50 | 'D:\\items_test\\items_test/test_3' 51 | ]; 52 | instance.files = [ 53 | '/items_test/items_test.txt', 54 | '/items_test/items_test/test_2.bin', 55 | '/items_test/items_test/test_3\\test_3.txt' 56 | ]; 57 | 58 | instance.removeRoot('D:\\items_test\\'); 59 | instance.removeRoot('/items_test/'); 60 | 61 | expect(instance.directories).to.have.members([ 62 | '', 63 | 'items_test', 64 | 'items_test/test_3' 65 | ]); 66 | expect(instance.files).to.have.members([ 67 | 'items_test.txt', 68 | 'items_test/test_2.bin', 69 | 'items_test/test_3\\test_3.txt' 70 | ]); 71 | }); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/webpack.js: -------------------------------------------------------------------------------- 1 | class EmulatedWebpackCompilerV4 { 2 | constructor() { 3 | this.hooks = { 4 | beforeRun: { 5 | tapAsync: (_pluginName, method) => { 6 | this._register('beforeRun', method); 7 | }, 8 | run: undefined 9 | }, 10 | watchRun: { 11 | tapAsync: (_pluginName, method) => { 12 | this._register('watchRun', method); 13 | }, 14 | run: undefined 15 | }, 16 | afterEmit: { 17 | tapAsync: (_pluginName, method) => { 18 | this._register('afterEmit', method); 19 | }, 20 | run: undefined 21 | } 22 | }; 23 | } 24 | 25 | _register(hookName, method) { 26 | this.hooks[hookName].run = () => { 27 | method({}, () => { }); 28 | }; 29 | } 30 | 31 | _run(hookName) { 32 | this.hooks[hookName].run(); 33 | } 34 | 35 | runBeforeRun() { 36 | this._run('beforeRun'); 37 | } 38 | 39 | runWatchRun() { 40 | this._run('watchRun'); 41 | } 42 | 43 | runAfterEmit() { 44 | this._run('afterEmit'); 45 | } 46 | 47 | runAllHooks() { 48 | this.runBeforeRun(); 49 | this.runWatchRun(); 50 | this.runAfterEmit(); 51 | } 52 | } 53 | 54 | 55 | class EmulatedWebpackCompilerV3 { 56 | constructor() { 57 | this.plugin = this._register; 58 | this._hooks = { 59 | 'before-run': { 60 | run: undefined 61 | }, 62 | 'watch-run': { 63 | run: undefined 64 | }, 65 | 'after-emit': { 66 | run: undefined 67 | } 68 | }; 69 | } 70 | 71 | _register(hookName, method) { 72 | if (!([ 73 | 'before-run', 74 | 'watch-run', 75 | 'after-emit' 76 | ].includes(hookName))) { 77 | throw new Error(`Invalid hook name - "${hookName}"`); 78 | } 79 | 80 | this._hooks[hookName].run = () => { 81 | method({}, () => { }); 82 | }; 83 | } 84 | 85 | _run(hookName) { 86 | this._hooks[hookName].run(); 87 | } 88 | 89 | runBeforeRun() { 90 | this._run('before-run'); 91 | } 92 | 93 | runWatchRun() { 94 | this._run('watch-run'); 95 | } 96 | 97 | runAfterEmit() { 98 | this._run('after-emit'); 99 | } 100 | 101 | runAllHooks() { 102 | this.runBeforeRun(); 103 | this.runWatchRun(); 104 | this.runAfterEmit(); 105 | } 106 | } 107 | 108 | 109 | exports.EmulatedWebpackCompiler = { 110 | v4: EmulatedWebpackCompilerV4, 111 | v3: EmulatedWebpackCompilerV3 112 | }; 113 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | //#region Types 5 | 6 | /** 7 | * @typedef {( 8 | * 'info' | 9 | * 'debug' | 10 | * 'warning' | 11 | * 'error' 12 | * )} MessageGroup 13 | * Group of message. 14 | */ 15 | 16 | /** 17 | * @typedef {Object} Message 18 | * A message object. 19 | * 20 | * @property {string} message 21 | * A message data. 22 | * 23 | * @property {MessageGroup} group 24 | * A message group. 25 | * 26 | * @property {Terminal.Items[]} [items] 27 | * An items for printing. 28 | * Optional. 29 | */ 30 | 31 | //#endregion 32 | 33 | 34 | const Terminal = require('./terminal'); 35 | 36 | 37 | /** 38 | * Logs messages of different levels. 39 | */ 40 | class Logger { 41 | /** 42 | * Creates an instance of `Logger`. 43 | */ 44 | constructor() { 45 | /** @type {Message[]} */ 46 | this.data = []; 47 | } 48 | 49 | /** 50 | * Adds message in log. 51 | * 52 | * @param {string} message 53 | * Message for printing. 54 | * **You shouldn't include new line symbols 55 | * in one message, because new line symbol 56 | * depend on the OS. Use several `add` instead.** 57 | * 58 | * @param {Terminal.Items} [items] 59 | * Optional. 60 | * An items for printing. 61 | */ 62 | add(message, items = undefined) { 63 | this.data.push({ 64 | message: message, 65 | items: items, 66 | group: this.group 67 | }); 68 | } 69 | 70 | /** 71 | * Clears a log. 72 | */ 73 | clear() { 74 | this.data = []; 75 | } 76 | 77 | /** 78 | * Prints all messages. 79 | * 80 | * @param {Object} prcss 81 | * `compiler` or `compilation` object. 82 | * 83 | * @param {boolean} newLineStart 84 | * Print new line before all messages will be printed. 85 | * Defaults to `true`. 86 | */ 87 | log(prcss, newLineStart = true) { 88 | let messages = []; 89 | 90 | for (const data of this.data) { 91 | if (data.message) { 92 | messages.push( 93 | Terminal.generateMessage( 94 | data.message, 95 | this.generateProperties 96 | ) 97 | ); 98 | } 99 | 100 | if (data.items) { 101 | messages = messages.concat( 102 | Terminal.generateItemsMessage( 103 | data.items 104 | ) 105 | ); 106 | } 107 | } 108 | 109 | Terminal.printMessages( 110 | prcss, 111 | messages, 112 | this.group, 113 | { 114 | newLineStart: newLineStart 115 | } 116 | ); 117 | } 118 | 119 | /** 120 | * @returns {boolean} 121 | * Logger is empty or not. 122 | */ 123 | isEmpty() { 124 | return !this.data.length; 125 | } 126 | } 127 | 128 | 129 | /** 130 | * Logs messages of info level. 131 | */ 132 | class InfoLogger extends Logger { 133 | /** 134 | * @returns {MessageGroup} 135 | * Group of message. 136 | */ 137 | get group() { 138 | return 'info'; 139 | } 140 | 141 | /** 142 | * @returns {Terminal.GenerateProperties} 143 | * Generate properties. 144 | */ 145 | get generateProperties() { 146 | return { 147 | color: 'white' 148 | }; 149 | } 150 | } 151 | 152 | 153 | /** 154 | * Logs messages of debug level. 155 | */ 156 | class DebugLogger extends Logger { 157 | /** 158 | * @returns {MessageGroup} 159 | * Group of message. 160 | */ 161 | get group() { 162 | return 'debug'; 163 | } 164 | 165 | /** 166 | * @returns {Terminal.GenerateProperties} 167 | * Generate properties. 168 | */ 169 | get generateProperties() { 170 | return { 171 | color: 'magenta' 172 | }; 173 | } 174 | } 175 | 176 | 177 | /** 178 | * Logs messages of warning level. 179 | */ 180 | class WarningLogger extends Logger { 181 | /** 182 | * @returns {MessageGroup} 183 | * Group of message. 184 | */ 185 | get group() { 186 | return 'warning'; 187 | } 188 | 189 | /** 190 | * @returns {Terminal.GenerateProperties} 191 | * Generate properties. 192 | */ 193 | get generateProperties() { 194 | return { 195 | color: 'yellow' 196 | }; 197 | } 198 | } 199 | 200 | 201 | /** 202 | * Logs messages of error level. 203 | */ 204 | class ErrorLogger extends Logger { 205 | /** 206 | * @returns {MessageGroup} 207 | * Group of message. 208 | */ 209 | get group() { 210 | return 'error'; 211 | } 212 | 213 | /** 214 | * @returns {Terminal.GenerateProperties} 215 | * Generate properties. 216 | */ 217 | get generateProperties() { 218 | return { 219 | color: 'red' 220 | }; 221 | } 222 | } 223 | 224 | 225 | module.exports = { 226 | Logger: Logger, 227 | InfoLogger: InfoLogger, 228 | DebugLogger: DebugLogger, 229 | WarningLogger: WarningLogger, 230 | ErrorLogger: ErrorLogger 231 | }; 232 | -------------------------------------------------------------------------------- /test/unit/plugin/apply-hook.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Webpack = require('../../webpack'); 3 | const Plugin = require('../../../src/plugin'); 4 | 5 | 6 | describe('unit', function () { 7 | describe('plugin', function () { 8 | describe('.applyHook()', function () { 9 | it('"beforeForFirstBuild" works only in "watch"', function () { 10 | const webpackV4 = new Webpack.EmulatedWebpackCompiler.v4(); 11 | let applied = false; 12 | const instance = new Plugin({ 13 | before: { 14 | include: [ 15 | './plugin_test_remove/apply-hook/test1.txt' 16 | ], 17 | beforeRemove: () => { 18 | applied = true; 19 | } 20 | }, 21 | after: { 22 | include: [ 23 | './plugin_test_remove/apply-hook/test1.txt' 24 | ], 25 | beforeForFirstBuild: true, 26 | log: false 27 | } 28 | }); 29 | 30 | instance.apply(webpackV4); 31 | webpackV4.runAfterEmit(); 32 | 33 | expect(applied).to.equal(false); 34 | }); 35 | 36 | it('"beforeForFirstBuild: true" applies "before" for first build', function () { 37 | const webpackV4 = new Webpack.EmulatedWebpackCompiler.v4(); 38 | let applied = false; 39 | const instance = new Plugin({ 40 | before: { 41 | include: [ 42 | './plugin_test_remove/apply-hook/test2.txt' 43 | ], 44 | beforeRemove: () => { 45 | applied = true; 46 | }, 47 | log: false 48 | }, 49 | watch: { 50 | beforeForFirstBuild: true 51 | } 52 | }); 53 | 54 | instance.apply(webpackV4); 55 | webpackV4.runWatchRun(); 56 | 57 | expect(applied).to.equal(true); 58 | }); 59 | 60 | it('"beforeForFirstBuild: false" not applies "before" for first build', function () { 61 | const webpackV4 = new Webpack.EmulatedWebpackCompiler.v4(); 62 | let applied = true; 63 | const instance = new Plugin({ 64 | before: { 65 | include: [ 66 | './plugin_test_remove/apply-hook/test3.txt' 67 | ] 68 | }, 69 | watch: { 70 | include: [ 71 | './plugin_test_remove/apply-hook/test3.txt' 72 | ], 73 | beforeForFirstBuild: false, 74 | beforeRemove: () => { 75 | applied = false; 76 | }, 77 | log: false 78 | } 79 | }); 80 | 81 | instance.apply(webpackV4); 82 | webpackV4.runWatchRun(); 83 | 84 | expect(applied).to.equal(false); 85 | }); 86 | 87 | it('"beforeForFirstBuild: true" applies "before" only for first build', function () { 88 | const webpackV4 = new Webpack.EmulatedWebpackCompiler.v4(); 89 | const result = []; 90 | const instance = new Plugin({ 91 | before: { 92 | include: [ 93 | './plugin_test_remove/apply-hook/test4.txt' 94 | ], 95 | beforeRemove: () => { 96 | result.push("before"); 97 | }, 98 | log: false 99 | }, 100 | watch: { 101 | include: [ 102 | './plugin_test_remove/apply-hook/test5.txt' 103 | ], 104 | beforeForFirstBuild: true, 105 | beforeRemove: () => { 106 | result.push("watch"); 107 | }, 108 | log: false 109 | } 110 | }); 111 | 112 | instance.apply(webpackV4); 113 | webpackV4.runWatchRun(); 114 | webpackV4.runWatchRun(); 115 | 116 | expect(result).to.have.ordered.members(["before", "watch"]); 117 | }); 118 | 119 | it('"skipFirstBuild: true" skips first build', function () { 120 | const webpackV4 = new Webpack.EmulatedWebpackCompiler.v4(); 121 | let removed = false; 122 | const instance = new Plugin({ 123 | watch: { 124 | include: [ 125 | './plugin_test_remove/apply-hook/test6.txt' 126 | ], 127 | skipFirstBuild: true, 128 | afterRemove: () => { 129 | removed = true; 130 | } 131 | } 132 | }); 133 | 134 | instance.apply(webpackV4); 135 | webpackV4.runWatchRun(); 136 | 137 | expect(removed).to.equal(false); 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const Path = require('./path'); 5 | 6 | 7 | /** 8 | * Contains directories and files. 9 | * 10 | * - available through `directories` and 11 | * `files` properties respectively. 12 | */ 13 | class Items { 14 | /** 15 | * Creates an instance of `Items`. 16 | */ 17 | constructor() { 18 | /** 19 | * @type {string[]} 20 | * */ 21 | this._directories = []; 22 | 23 | /** 24 | * @type {string[]} 25 | * */ 26 | this._files = []; 27 | } 28 | 29 | /** 30 | * @returns {string[]} 31 | * Directories. 32 | */ 33 | get directories() { 34 | return this._directories; 35 | } 36 | 37 | /** 38 | * Sets directories. 39 | * 40 | * @param {string[]} value 41 | * Value to set. 42 | */ 43 | set directories(value) { 44 | this._directories = value; 45 | } 46 | 47 | /** 48 | * @returns {string[]} 49 | * Files. 50 | */ 51 | get files() { 52 | return this._files; 53 | } 54 | 55 | /** 56 | * Sets files. 57 | * 58 | * @param {string[]} value 59 | * Value to set. 60 | */ 61 | set files(value) { 62 | this._files = value; 63 | } 64 | 65 | /** 66 | * Removes unnecessary folders and files. 67 | * 68 | * - it removes children directories or files 69 | * whose parents will be removed; 70 | * - changes `this.directories` and `this.files`. 71 | * 72 | * @example 73 | * Input: 74 | * directories: [ 75 | * 'D:/dist/styles/css', 76 | * 'D:/dist/js/scripts', 77 | * 'D:/dist/styles', 78 | * 'D:/test', 79 | * 'C:/test/test_1' 80 | * ] 81 | * files: [ 82 | * 'D:/dist/styles/popup.css', 83 | * 'D:/dist/styles/popup.css.map', 84 | * 'D:/dist/manifest.json', 85 | * 'D:/test.txt', 86 | * 'D:/dist/js/scripts/test.js' 87 | * ] 88 | * 89 | * Output: 90 | * // there is no point in 'D:/dist/styles/css' 91 | * // because entire 'D:/dist/styles' will be removed. 92 | * directories: [ 93 | * 'D:/dist/js/scripts', 94 | * 'D:/dist/styles', 95 | * 'D:/test', 96 | * 'C:/test/test_1' 97 | * ] 98 | * // there is no point in 'D:/dist/styles/popup.css' and 99 | * // 'D:/dist/styles/popup.css.map' because entire 'D:/dist/styles' 100 | * // will be removed. 101 | * // there is no point in 'D:/dist/js/scripts/test.js' because entire 102 | * // 'D:/dist/js/scripts' will be removed. 103 | * files: [ 104 | * 'D:/dist/manifest.json', 105 | * 'D:/test.txt' 106 | * ] 107 | */ 108 | removeUnnecessary() { 109 | if (!this.directories.length) { 110 | return; 111 | } 112 | 113 | /** @type {Set} */ 114 | const unnecessaryIndexes = new Set(); 115 | 116 | /** @type {string[]} */ 117 | const rightDirectories = []; 118 | 119 | /** @type {string[]} */ 120 | const rightFiles = []; 121 | 122 | /** 123 | * @param {string[]} firstGroup 124 | * @param {string[]} secondGroup 125 | * @param {Set} indexes 126 | */ 127 | const addToUnnecessaryIndexes = (firstGroup, secondGroup, indexes) => { 128 | const path = new Path(); 129 | 130 | for (const itemFirst of firstGroup) { 131 | for (const indexSecond in secondGroup) { 132 | if (path.isSave(itemFirst, secondGroup[indexSecond])) { 133 | indexes.add(indexSecond); 134 | } 135 | } 136 | } 137 | }; 138 | 139 | /** 140 | * @param {string[]} rightGroup 141 | * @param {string[]} itemsGroup 142 | * @param {Set} indexes 143 | */ 144 | const addToRightGroup = (rightGroup, itemsGroup, indexes) => { 145 | for (const itemIndex in itemsGroup) { 146 | if (!indexes.has(itemIndex)) { 147 | rightGroup.push(itemsGroup[itemIndex]); 148 | } 149 | } 150 | }; 151 | 152 | addToUnnecessaryIndexes( 153 | this.directories, 154 | this.directories, 155 | unnecessaryIndexes 156 | ); 157 | addToRightGroup( 158 | rightDirectories, 159 | this.directories, 160 | unnecessaryIndexes 161 | ); 162 | 163 | unnecessaryIndexes.clear(); 164 | 165 | addToUnnecessaryIndexes( 166 | rightDirectories, 167 | this.files, 168 | unnecessaryIndexes 169 | ); 170 | addToRightGroup( 171 | rightFiles, 172 | this.files, 173 | unnecessaryIndexes 174 | ); 175 | 176 | this.directories = rightDirectories.slice(); 177 | this.files = rightFiles.slice(); 178 | } 179 | 180 | /** 181 | * Trims a root. 182 | * 183 | * - changes `this.directories` and `this.files`. 184 | * 185 | * @param {string} root 186 | * A root value that should be removed. 187 | */ 188 | removeRoot(root) { 189 | const method = (value) => { 190 | if (value.indexOf(root) === 0) { 191 | value = value.replace(root, ''); 192 | } 193 | 194 | return value; 195 | }; 196 | 197 | this.directories = this.directories.map(method); 198 | this.files = this.files.map(method); 199 | } 200 | 201 | /** 202 | * Sorts items in ascending, ASCII character order. 203 | * 204 | * - changes `this.directories` and `this.files`. 205 | */ 206 | sort() { 207 | this.directories = this.directories.sort(); 208 | this.files = this.files.sort(); 209 | } 210 | } 211 | 212 | 213 | module.exports = Items; 214 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.5.0 (December 13, 2021) 2 | 3 | ## Changed 4 | 5 | - Symbolic links (soft links) will be treated as ordinary files and thus will be removed. Earlier behavior was silent ignore. [#40](https://github.com/Amaimersion/remove-files-webpack-plugin/pull/40) 6 | - `TestObject.method` will be called for symbolic links. 7 | - Upgrade `trash` dependency to `7.2.0` version. 8 | 9 | 10 | # 1.4.5 (June 1, 2021) 11 | 12 | ## Changed 13 | 14 | - Upgrade `@types/webpack` dependency to `5.28.0` version. 15 | - Upgrade `trash` dependency to `7.1.1` version. 16 | 17 | 18 | # 1.4.4 (September 3, 2020) 19 | 20 | ## Improved 21 | 22 | - Documentation for `trash` parameter. [#32](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/32) 23 | 24 | ## Changed 25 | 26 | - Upgrade `@types/webpack` dependency to `4.41.22` version. 27 | 28 | 29 | # 1.4.3 (June 7, 2020) 30 | 31 | ## Added 32 | 33 | - Information about version naming in README. 34 | - New feature: `readWebpackConfiguration`. Part of plugin configuration may be controlled with global webpack configuration. [#29](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/29) 35 | 36 | 37 | # 1.4.2 (May 15, 2020) 38 | 39 | ## Improved 40 | 41 | - Little refactoring (related to English language, not information) of documentation and log messages. [#27](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/27) 42 | 43 | 44 | # 1.4.1 (April 25, 2020) 45 | 46 | ## Improved 47 | 48 | - Documentation for `beforeRemove`, `afterRemove` parameters. 49 | 50 | ## Added 51 | 52 | - Namespace documentation for every parameter. 53 | - New feature: `beforeForFirstBuild`. For first build `before` parameters will be applied, for subsequent builds `watch` parameters will be applied. Works only in `watch` key. [#25](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/25) 54 | - New feature: `skipFirstBuild`. First build will be skipped. Works only in `watch` key. 55 | 56 | ## Changed 57 | 58 | - Upgrade `@types/webpack` dependency to `4.41.12` version. 59 | 60 | 61 | # 1.4.0 (February 27, 2020) 62 | 63 | ## Added 64 | 65 | - New key: `watch`. Parameters of that key will be applied in "watch" mode. **Parameters of `before` key will no longer be applied in "watch" mode. So, make sure your current configuration still have expected behavior.** [#22](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/22) 66 | 67 | ## Fixed 68 | 69 | - A bug when old `include` and old `exclude` were mixed with new `include` and `exclude` in "watch" mode. It had no effect, because the plugin checks an items for existence before removing. Just strange messages were presented in warning log. 70 | 71 | 72 | # 1.3.0 (February 24, 2020) 73 | 74 | ## Improved 75 | 76 | - Stability, performance and quality. 77 | - Documentation and description. 78 | - Pretty printing of folders and files that have been removed. 79 | 80 | ## Added 81 | 82 | - New features: `beforeRemove` and `afterRemove` parameters. See [readme](https://github.com/Amaimersion/remove-files-webpack-plugin#parameters) for documentation. 83 | 84 | ## Changed 85 | 86 | - `trash` parameter is `false` by default. 87 | - `TestObject.method` supports testing for folders. **Ensure that behavior of your current tests remain unchanged.** 88 | 89 | ## Fixed 90 | 91 | - A bug when safety of removal could have been incorrectly determined in specific cases. 92 | - A bug when "unnecessary" folders and files could have been incorrectly excluded in specific cases. 93 | - A bug when `exclude` could not work as expected in specific cases. 94 | 95 | 96 | # 1.2.2 (January 19, 2020) 97 | 98 | ## Fixed 99 | 100 | - A bug when root path was escaped incorrectly. [#16](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/16) 101 | 102 | 103 | # 1.2.1 (December 25, 2019) 104 | 105 | ## Added 106 | 107 | - Type definitions. [#13](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/13) 108 | 109 | 110 | # 1.2.0 (December 23, 2019) 111 | 112 | ## Improved 113 | 114 | - Stability and quality. 115 | - Documentation and description. 116 | 117 | ## Added 118 | 119 | - New feature: `trash`. Now you can remove your items in a trash (recycle bin). 120 | - New features: `logWarning`, `logError` and `logDebug`. Now you can control logging of different levels. [#10](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/10) 121 | 122 | ## Changed 123 | 124 | - Text of log messages. 125 | 126 | ## Fixed 127 | 128 | - A bug when safety of removal was incorrectly determined. [#7](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/7) 129 | - A bug when the plugin not worked in `--watch` mode. [#9](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/9) 130 | - A bug when a path could be incorrectly changed for pretty printing. 131 | - A bug when unnecessary items could be cropped incorrectly. 132 | 133 | 134 | # 1.1.3 (May 10, 2019) 135 | 136 | ## Fixed 137 | 138 | - Fix a bug when folder path in warning message was `undefined`. [#5](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/5). 139 | 140 | 141 | # 1.1.2 (April 4, 2019) 142 | 143 | ## Fixed 144 | 145 | - Fix compatibility with webpack 3.x [#3](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/3). 146 | 147 | 148 | # 1.1.1 (March 20, 2019) 149 | 150 | Nothing changed. Had to change the version because of problems with `npm`. See changelog for 1.1.0 version. 151 | 152 | 153 | # 1.1.0 (March 20, 2019) 154 | 155 | ## Fixed 156 | 157 | - No longer necessary set `__dirname` to `root` in order to specify an actual root directory. 158 | - If you not set `include` and `test` parameters, there will be no more message `An items for removing not found.`. 159 | 160 | ## Changed 161 | 162 | - Plugin name that used in terminal now contains a version. 163 | - If `allowRootAndOutside` option is off, then in terminal a paths will be printed without the root. 164 | - Improved cross-platform properties. 165 | - Improved readability of log messages. 166 | 167 | ## Added 168 | 169 | - Add a link to README to error message which appears in case you do not specify neither `before` nor `after` parameters. 170 | - Add a warning message if `allowRootAndOutside` option is on and if there is unsafe removing. 171 | 172 | 173 | # 1.0.0 (June 5, 2018) 174 | 175 | Initial release. 176 | -------------------------------------------------------------------------------- /test/acceptance/large.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const expect = require('chai').expect; 4 | const Webpack = require('../webpack'); 5 | const RemovePlugin = require('../..'); 6 | 7 | 8 | describe('acceptance', function () { 9 | describe('large', function () { 10 | const logs = { 11 | log: false, 12 | logWarning: true, 13 | logError: true, 14 | logDebug: false 15 | }; 16 | const exists = (...paths) => { 17 | return ( 18 | fs.existsSync( 19 | path.resolve( 20 | ...paths 21 | ) 22 | ) 23 | ); 24 | } 25 | 26 | it('should pass tricky test', function () { 27 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 28 | const instance = new RemovePlugin({ 29 | before: { 30 | root: './acceptance_test_remove/large_test', 31 | include: [ 32 | 'Pa.^&th/test/test.txt', 33 | '.\\Pa.^&th/test.txt', 34 | 'test.txt', 35 | './maps/test.txt', 36 | 'styles', 37 | 'styles/test' 38 | ], 39 | test: [ 40 | { 41 | folder: './', 42 | method: (pth) => pth.includes('pa)t-h (t [e]s {t} +'), 43 | recursive: true 44 | }, 45 | { 46 | folder: 'Pa.^&th\\', 47 | method: () => true, 48 | recursive: true 49 | }, 50 | { 51 | folder: './maps', 52 | method: (path) => path.includes('.map'), 53 | recursive: false 54 | } 55 | ], 56 | exclude: [ 57 | '.\\pa)t-h (t [e]s {t} +', 58 | './Pa.^&th\\test\\test.txt', 59 | './maps/test/file.map' 60 | ], 61 | ...logs 62 | } 63 | }); 64 | 65 | instance.apply(webpack); 66 | webpack.runBeforeRun(); 67 | 68 | const beforeIsSuccess = ( 69 | exists( 70 | '.', 71 | 'acceptance_test_remove', 72 | 'large_test', 73 | 'pa)t-h (t [e]s {t} +' 74 | ) && 75 | exists( 76 | '.', 77 | 'acceptance_test_remove', 78 | 'large_test', 79 | 'test' 80 | ) && 81 | exists( 82 | '.', 83 | 'acceptance_test_remove', 84 | 'large_test', 85 | 'Pa.^&th', 86 | 'test', 87 | 'test.txt' 88 | ) && 89 | exists( 90 | '.', 91 | 'acceptance_test_remove', 92 | 'large_test', 93 | 'maps', 94 | 'test', 95 | '1.map' 96 | ) && 97 | exists( 98 | '.', 99 | 'acceptance_test_remove', 100 | 'large_test', 101 | 'maps', 102 | 'test', 103 | 'file.map' 104 | ) && 105 | !exists( 106 | '.', 107 | 'acceptance_test_remove', 108 | 'large_test', 109 | 'styles' 110 | ) && 111 | !exists( 112 | '.', 113 | 'acceptance_test_remove', 114 | 'large_test', 115 | 'pa)t-h (t [e]s {t} + file.name.txt' 116 | ) && 117 | !exists( 118 | '.', 119 | 'acceptance_test_remove', 120 | 'large_test', 121 | 'Pa.^&th', 122 | 'test.txt' 123 | ) && 124 | !exists( 125 | '.', 126 | 'acceptance_test_remove', 127 | 'large_test', 128 | 'Pa.^&th', 129 | 'another.txt' 130 | ) && 131 | !exists( 132 | '.', 133 | 'acceptance_test_remove', 134 | 'large_test', 135 | 'Pa.^&th', 136 | 'test', 137 | 'another.txt' 138 | ) && 139 | !exists( 140 | '.', 141 | 'acceptance_test_remove', 142 | 'large_test', 143 | 'maps', 144 | 'test.txt' 145 | ) && 146 | !exists( 147 | '.', 148 | 'acceptance_test_remove', 149 | 'large_test', 150 | 'maps', 151 | 'test.map' 152 | ) && 153 | !exists( 154 | '.', 155 | 'acceptance_test_remove', 156 | 'large_test', 157 | 'maps', 158 | '1.map' 159 | ) && 160 | !exists( 161 | '.', 162 | 'acceptance_test_remove', 163 | 'large_test', 164 | 'pa)t-h (t [e]s {t} +', 165 | 'test.txt' 166 | ) && 167 | !exists( 168 | '.', 169 | 'acceptance_test_remove', 170 | 'large_test', 171 | 'pa)t-h (t [e]s {t} +', 172 | '1.map' 173 | ) 174 | ); 175 | 176 | expect(beforeIsSuccess).to.equal(true); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /test/unit/fs/unlink-folder-sync.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const expect = require('chai').expect; 3 | const Fs = require('../../../src/fs'); 4 | const Path = require('../../../src/path'); 5 | 6 | 7 | describe('unit', function () { 8 | describe('fs', function () { 9 | describe('.unlinkFolderSync()', function () { 10 | const instance = new Fs(); 11 | const customPath = new Path(); 12 | 13 | it('should throw error in case of permanent removing if folder not exists', function () { 14 | const method = () => instance.unlinkFolderSync({ 15 | pth: 'D:/fs_test_remove/not exists', 16 | toTrash: false 17 | }); 18 | 19 | expect(method).to.throw(); 20 | }); 21 | 22 | it('should permanently remove empty folder', function () { 23 | instance.unlinkFolderSync({ 24 | pth: 'D:/fs_test_folder_remove_1', 25 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 26 | toTrash: false 27 | }); 28 | 29 | const result = fs.existsSync('D:/fs_test_folder_remove_1'); 30 | const correct = false; 31 | 32 | expect(result).to.be.equal(correct); 33 | }); 34 | 35 | it('should permanently remove folder with files', function () { 36 | instance.unlinkFolderSync({ 37 | pth: 'D:/fs_test_folder_remove_2', 38 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 39 | toTrash: false 40 | }); 41 | 42 | const result = fs.existsSync('D:/fs_test_folder_remove_2'); 43 | const correct = false; 44 | 45 | expect(result).to.be.equal(correct); 46 | }); 47 | 48 | it('should permanently remove folder with empty folders', function () { 49 | instance.unlinkFolderSync({ 50 | pth: 'D:/fs_test_folder_remove_3', 51 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 52 | toTrash: false 53 | }); 54 | 55 | const result = fs.existsSync('D:/fs_test_folder_remove_3'); 56 | const correct = false; 57 | 58 | expect(result).to.be.equal(correct); 59 | }); 60 | 61 | it('should permanently remove folder with folders', function () { 62 | instance.unlinkFolderSync({ 63 | pth: 'D:/fs_test_folder_remove_4', 64 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 65 | toTrash: false 66 | }); 67 | 68 | const result = fs.existsSync('D:/fs_test_folder_remove_4'); 69 | const correct = false; 70 | 71 | expect(result).to.be.equal(correct); 72 | }); 73 | 74 | it('should move to trash empty folder', function (done) { 75 | instance.unlinkFolderSync({ 76 | pth: 'D:/fs_test_folder_remove_5', 77 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 78 | toTrash: true, 79 | rightTrashCallbacks: true, 80 | onFolderSuccess: () => { 81 | const result = fs.existsSync('D:/fs_test_folder_remove_5'); 82 | const correct = false; 83 | 84 | expect(result).to.be.equal(correct); 85 | done(); 86 | }, 87 | onFolderError: (error) => { 88 | done(error); 89 | } 90 | }); 91 | }); 92 | 93 | it('should move to trash folder with files', function (done) { 94 | instance.unlinkFolderSync({ 95 | pth: 'D:/fs_test_folder_remove_6', 96 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 97 | toTrash: true, 98 | rightTrashCallbacks: true, 99 | onFolderSuccess: () => { 100 | const result = fs.existsSync('D:/fs_test_folder_remove_6'); 101 | const correct = false; 102 | 103 | expect(result).to.be.equal(correct); 104 | done(); 105 | }, 106 | onFolderError: (error) => { 107 | done(error); 108 | } 109 | }); 110 | }); 111 | 112 | it('should move to trash folder with empty folders', function (done) { 113 | instance.unlinkFolderSync({ 114 | pth: 'D:/fs_test_folder_remove_7', 115 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 116 | toTrash: true, 117 | rightTrashCallbacks: true, 118 | onFolderSuccess: () => { 119 | const result = fs.existsSync('D:/fs_test_folder_remove_7'); 120 | const correct = false; 121 | 122 | expect(result).to.be.equal(correct); 123 | done(); 124 | }, 125 | onFolderError: (error) => { 126 | done(error); 127 | } 128 | }); 129 | }); 130 | 131 | it('should move to trash folder with folders', function (done) { 132 | instance.unlinkFolderSync({ 133 | pth: 'D:/fs_test_folder_remove_8', 134 | toAbsoluteS: (...args) => customPath.toAbsoluteS(...args), 135 | toTrash: true, 136 | rightTrashCallbacks: true, 137 | onFolderSuccess: () => { 138 | const result = fs.existsSync('D:/fs_test_folder_remove_8'); 139 | const correct = false; 140 | 141 | expect(result).to.be.equal(correct); 142 | done(); 143 | }, 144 | onFolderError: (error) => { 145 | done(error); 146 | } 147 | }); 148 | }); 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /src/path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | //#region Types 5 | 6 | /** 7 | * @typedef {'' | 'win32' | 'posix'} PathType 8 | * Type of `path` module. 9 | */ 10 | 11 | //#endregion 12 | 13 | 14 | const path = require('path'); 15 | const Fs = require('./fs'); 16 | const Utils = require('./utils'); 17 | 18 | 19 | /** 20 | * Wrapper for Node `path` module. 21 | */ 22 | class Path { 23 | /** 24 | * Creates an instance of `Path`. 25 | */ 26 | constructor() { 27 | /** 28 | * `fs` module. 29 | */ 30 | this.fs = new Fs(); 31 | 32 | /** 33 | * @type {PathType} 34 | * */ 35 | this._type = ''; 36 | } 37 | 38 | /** 39 | * Returns current `path` type. 40 | * 41 | * @returns {string} 42 | * Current `path` type. 43 | */ 44 | get type() { 45 | return this._type; 46 | } 47 | 48 | /** 49 | * Sets type of `path` module. 50 | * 51 | * @param {PathType} value 52 | * Type of `path` module. 53 | */ 54 | set type(value) { 55 | this._type = value; 56 | } 57 | 58 | /** 59 | * Returns `path` module with 60 | * specified type. 61 | * 62 | * - if type is not specified, 63 | * then it will be auto picked. 64 | * 65 | * @returns {path.PlatformPath} 66 | * `path` module. 67 | */ 68 | get path() { 69 | return ( 70 | this.type ? 71 | path[this.type] : 72 | path 73 | ); 74 | } 75 | 76 | /** 77 | * Converts provided path to absolute path 78 | * (based on provided root). 79 | * 80 | * - **single `\` (Windows) not supported, 81 | * because JS uses it for escaping.** 82 | * - normalizes paths that has been converted. 83 | * 84 | * @param {string} root 85 | * A root that will be appended to non absolute path. 86 | * 87 | * @param {string} pth 88 | * A path for converting. 89 | * 90 | * @param {( 91 | * oldPath: string, 92 | * newPath: string 93 | * ) => any} onChange 94 | * If specified, will be called when 95 | * provided path changes. 96 | * 97 | * @returns {string} 98 | * An absolute `pth`. 99 | */ 100 | toAbsolute(root, pth, onChange = undefined) { 101 | let newPath = pth; 102 | 103 | if (!this.path.isAbsolute(pth)) { 104 | newPath = this.path.join(root, pth); 105 | 106 | if (onChange) { 107 | onChange(pth, newPath); 108 | } 109 | } 110 | 111 | return newPath; 112 | } 113 | 114 | /** 115 | * Converts all paths to absolute paths. 116 | * 117 | * - see `toAbsolute()` documentation. 118 | * 119 | * @param {string} root 120 | * @param {string[]} pth 121 | * @param {( 122 | * oldPath: string, 123 | * newPath: string 124 | * ) => any} onChange 125 | * 126 | * @returns {string[]} 127 | */ 128 | toAbsoluteS(root, paths, onChange = undefined) { 129 | const newPaths = []; 130 | 131 | for (const item of paths) { 132 | newPaths.push( 133 | this.toAbsolute( 134 | root, 135 | item, 136 | onChange 137 | ) 138 | ); 139 | } 140 | 141 | return newPaths; 142 | } 143 | 144 | /** 145 | * Checks a path for safety. 146 | * 147 | * - checks for either exit beyond the root 148 | * or identicality with the root. 149 | * 150 | * @param {string} root 151 | * A root directory. 152 | * 153 | * @param {string} pth 154 | * A path for checking. 155 | * 156 | * @param {( 157 | * comparedRoot: string, 158 | * comparedPth: string 159 | * ) => any} onCompare 160 | * If specified, will be called when 161 | * comparation occurs. 162 | * 163 | * @returns {boolean} 164 | * `true` – save, 165 | * `false` – not save. 166 | * 167 | * @throws 168 | * Throws an error if occurs 169 | * (for example, if file or folder not exists). 170 | * 171 | * @example 172 | * root = 'D:/dist' 173 | * pth = 'D:/dist/chromium' 174 | * Returns – true 175 | * 176 | * root = 'D:/dist' 177 | * pth = 'D:/dist' 178 | * Returns – false 179 | * 180 | * root = 'D:/dist' 181 | * pth = 'D:/' 182 | * Returns – false 183 | * 184 | * root = './dist' 185 | * pth = 'dist/scripts' 186 | * Returns – true 187 | * 188 | * root = '.' 189 | * pth = './dist/scripts' 190 | * Returns - true 191 | * 192 | * root = 'D:\\Desktop\\test' 193 | * pth = 'D:\\Desktop\\test.txt' 194 | * Returns - false 195 | * 196 | * root = 'D:/te)st-tes(t and te[s]t {df} df+g.df^g&t' 197 | * pth = 'D:\\te)st-tes(t and te[s]t {df} df+g.df^g&t/chromium' 198 | * Returns – true 199 | * 200 | * root = 'D:/test/../chromium' 201 | * pth = 'D:\\chromium/file.txt' 202 | * Returns – true 203 | */ 204 | isSave(root, pth, onCompare = undefined) { 205 | root = this.path.resolve(root); 206 | pth = this.path.resolve(pth); 207 | 208 | if (!this.fs.fs.existsSync(root)) { 209 | throw new Error(`Root not exists – ${root}`); 210 | } 211 | 212 | if (!this.fs.fs.existsSync(pth)) { 213 | throw new Error(`Pth not exists – ${pth}`); 214 | } 215 | 216 | const parsedRoot = this.path.parse(root); 217 | const parsedPth = this.path.parse(pth); 218 | 219 | if (onCompare) { 220 | onCompare( 221 | root, 222 | pth 223 | ); 224 | } 225 | 226 | /** 227 | * Cases which unable to handle with RegExp. 228 | */ 229 | if ( 230 | /** 231 | * 'D:/' and 'D:/test' 232 | */ 233 | !parsedRoot.base && 234 | parsedRoot.root === parsedPth.root && 235 | parsedPth.base 236 | ) { 237 | return true; 238 | } 239 | 240 | const pthIsFile = !!parsedPth.ext; 241 | root = Utils.escapeForRegExp(root); 242 | 243 | return new RegExp( 244 | `(^${root})${pthIsFile ? '([/\\\\]|$)' : '([/\\\\]+)'}`, 245 | 'm' 246 | ).test(pthIsFile ? this.path.dirname(pth) : pth); 247 | } 248 | } 249 | 250 | 251 | module.exports = Path; 252 | -------------------------------------------------------------------------------- /test/unit/plugin/handle-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Plugin = require('../../../src/plugin'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('plugin', function () { 7 | describe('.handleTest()', function () { 8 | it('should return empty list if test parameters not specified', function () { 9 | const instance = new Plugin({ 10 | before: {} 11 | }); 12 | const result = instance.handleTest(instance.beforeParams); 13 | 14 | expect(result).to.have.lengthOf(0); 15 | }); 16 | 17 | it('should return empty list if test parameters is empty', function () { 18 | const instance = new Plugin({ 19 | after: { 20 | test: [] 21 | } 22 | }); 23 | const result = instance.handleTest(instance.afterParams); 24 | 25 | expect(result).to.have.lengthOf(0); 26 | }); 27 | 28 | it('should skip not existed folders', function () { 29 | const instance = new Plugin({ 30 | before: { 31 | test: [ 32 | { 33 | folder: './plugin_test/not exists', 34 | method: () => true 35 | } 36 | ] 37 | } 38 | }); 39 | const result = instance.handleTest(instance.beforeParams); 40 | 41 | expect(result).to.have.lengthOf(0); 42 | }); 43 | 44 | it('should skip folders that not actually folders', function () { 45 | const instance = new Plugin({ 46 | before: { 47 | test: [ 48 | { 49 | folder: './plugin_test/test.txt', 50 | method: () => true 51 | } 52 | ] 53 | } 54 | }); 55 | const result = instance.handleTest(instance.beforeParams); 56 | 57 | expect(result).to.have.lengthOf(0); 58 | }); 59 | 60 | it('should not allow unsafe removing', function () { 61 | const instance = new Plugin({ 62 | after: { 63 | root: '.', 64 | allowRootAndOutside: false, 65 | test: [ 66 | { 67 | folder: 'D:/plugin_test', 68 | method: () => true 69 | } 70 | ] 71 | } 72 | }); 73 | const result = instance.handleTest(instance.afterParams); 74 | 75 | expect(result).to.have.lengthOf(0); 76 | }); 77 | 78 | it('should allow unsafe removing', function () { 79 | const instance = new Plugin({ 80 | watch: { 81 | root: '.', 82 | allowRootAndOutside: true, 83 | test: [ 84 | { 85 | folder: 'D:/plugin_test', 86 | method: () => true 87 | } 88 | ] 89 | } 90 | }); 91 | const result = instance.handleTest(instance.watchParams); 92 | 93 | expect(result).not.to.have.lengthOf(0); 94 | }); 95 | 96 | it('should handle unrecursive mode', function () { 97 | const instance = new Plugin({ 98 | before: { 99 | test: [ 100 | { 101 | folder: './plugin_test', 102 | recursive: false, 103 | method: (absoluteItemPath) => absoluteItemPath.includes('.txt') 104 | } 105 | ] 106 | } 107 | }); 108 | const result = instance.handleTest(instance.beforeParams); 109 | 110 | expect(result).to.have.lengthOf(2); 111 | }); 112 | 113 | it('should handle recursive mode', function () { 114 | const instance = new Plugin({ 115 | after: { 116 | test: [ 117 | { 118 | folder: 'plugin_test', 119 | recursive: true, 120 | method: (absoluteItemPath) => ( 121 | absoluteItemPath.includes('.jpg') || 122 | absoluteItemPath.includes('.png') 123 | ) 124 | } 125 | ] 126 | } 127 | }); 128 | const result = instance.handleTest(instance.afterParams); 129 | 130 | expect(result).to.have.lengthOf(2); 131 | }); 132 | 133 | it('should handle several tests', function () { 134 | const instance = new Plugin({ 135 | watch: { 136 | root: '.', 137 | allowRootAndOutside: true, 138 | test: [ 139 | { 140 | folder: 'plugin_test', 141 | recursive: false, 142 | method: (absoluteItemPath) => absoluteItemPath.includes('.txt') 143 | }, 144 | { 145 | folder: 'plugin_test/test', 146 | recursive: false, 147 | method: (absoluteItemPath) => absoluteItemPath.includes('.png') 148 | }, 149 | { 150 | folder: 'D:/plugin_test', 151 | method: () => true 152 | } 153 | ] 154 | } 155 | }); 156 | const result = instance.handleTest(instance.watchParams); 157 | 158 | expect(result).to.have.lengthOf(5); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/unit/fs/get-items-sync.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const expect = require('chai').expect; 4 | const Fs = require('../../../src/fs'); 5 | 6 | 7 | describe('unit', function () { 8 | describe('fs', function () { 9 | describe('.getItemsSync()', function () { 10 | const instance = new Fs(); 11 | 12 | it('should throw an error if folder not exists', function () { 13 | const method = () => instance.getItemsSync({ 14 | pth: 'D:/fs_test/not exists' 15 | }); 16 | 17 | expect(method).to.throw(); 18 | }); 19 | 20 | it('should return all root files', function () { 21 | const result = instance.getItemsSync({ 22 | pth: 'D:/fs_test/', 23 | join: path.join, 24 | recursive: false 25 | }); 26 | const correct = [ 27 | 'D:\\fs_test\\test_1.txt', 28 | 'D:\\fs_test\\test 2.txt', 29 | 'D:\\fs_test\\test-3.bin', 30 | ]; 31 | 32 | expect(result).to.have.members(correct); 33 | }); 34 | 35 | it('should return all files from root and nested folders', function () { 36 | const result = instance.getItemsSync({ 37 | pth: 'D:/fs_test/', 38 | join: path.join, 39 | recursive: true 40 | }); 41 | const correct = [ 42 | 'D:\\fs_test\\test_1.txt', 43 | 'D:\\fs_test\\test 2.txt', 44 | 'D:\\fs_test\\test-3.bin', 45 | 'D:\\fs_test\\fs-test\\test_1.txt', 46 | 'D:\\fs_test\\fs-test\\test-2.bin', 47 | 'D:\\fs_test\\fs test\\test 1.bin', 48 | 'D:\\fs_test\\fs test\\test 2.txt', 49 | 'D:\\fs_test\\fs_test\\fs_test\\test_1.txt', 50 | 'D:\\fs_test\\fs_test\\fs_test\\test_2.test' 51 | ]; 52 | 53 | expect(result).to.have.members(correct); 54 | }); 55 | 56 | it('should return files that passed the test № 1', function () { 57 | const result = instance.getItemsSync({ 58 | pth: 'D:/fs_test/', 59 | join: path.join, 60 | recursive: true, 61 | test: (absolutePath) => { 62 | return ( 63 | absolutePath.includes('.txt') || 64 | absolutePath.includes('.test') 65 | ); 66 | } 67 | }); 68 | const correct = [ 69 | 'D:\\fs_test\\test_1.txt', 70 | 'D:\\fs_test\\test 2.txt', 71 | 'D:\\fs_test\\fs-test\\test_1.txt', 72 | 'D:\\fs_test\\fs test\\test 2.txt', 73 | 'D:\\fs_test\\fs_test\\fs_test\\test_1.txt', 74 | 'D:\\fs_test\\fs_test\\fs_test\\test_2.test' 75 | ]; 76 | 77 | expect(result).to.have.members(correct); 78 | }); 79 | 80 | it('should return files that passed the test № 2', function () { 81 | const result = instance.getItemsSync({ 82 | pth: 'D:/fs_test/', 83 | join: path.join, 84 | recursive: true, 85 | test: (absolutePath) => { 86 | return new RegExp( 87 | /([^:])(\\fs(-|_)test\\)([^\.]*\.(?!test))/, 88 | '' 89 | ).test(absolutePath); 90 | } 91 | }); 92 | const correct = [ 93 | 'D:\\fs_test\\fs-test\\test_1.txt', 94 | 'D:\\fs_test\\fs-test\\test-2.bin', 95 | 'D:\\fs_test\\fs_test\\fs_test\\test_1.txt' 96 | ]; 97 | 98 | expect(result).to.have.members(correct); 99 | }); 100 | 101 | it('should return folders that passed the test № 1', function () { 102 | const result = instance.getItemsSync({ 103 | pth: 'D:/fs_test/', 104 | join: path.join, 105 | recursive: false, 106 | test: (absolutePath) => ( 107 | absolutePath.includes('fs-test') || 108 | absolutePath.includes('fs test') 109 | ) 110 | }); 111 | const correct = [ 112 | 'D:\\fs_test\\fs-test', 113 | 'D:\\fs_test\\fs test' 114 | ]; 115 | 116 | expect(result).to.have.members(correct); 117 | }); 118 | 119 | it('should return folders that passed the test № 2', function () { 120 | const result = instance.getItemsSync({ 121 | pth: 'D:/fs_test/', 122 | join: path.join, 123 | recursive: true, 124 | test: (absolutePath) => fs.statSync(absolutePath).isDirectory() 125 | }); 126 | const correct = [ 127 | 'D:\\fs_test\\fs-test', 128 | 'D:\\fs_test\\fs test', 129 | 'D:\\fs_test\\fs_test', 130 | 'D:\\fs_test\\fs_test\\fs_test' 131 | ]; 132 | 133 | expect(result).to.have.members(correct); 134 | }); 135 | 136 | it('should return folders and files that passed the test', function () { 137 | const result = instance.getItemsSync({ 138 | pth: 'D:/fs_test/', 139 | join: path.join, 140 | recursive: true, 141 | test: (absolutePath) => ( 142 | absolutePath.includes('fs_test\\fs_test') || 143 | absolutePath.includes('fs_test\\fs-test') || 144 | absolutePath.includes('test_1') 145 | ) 146 | }); 147 | const correct = [ 148 | 'D:\\fs_test\\fs-test', 149 | 'D:\\fs_test\\fs_test', 150 | 'D:\\fs_test\\fs_test\\fs_test', 151 | 'D:\\fs_test\\test_1.txt', 152 | 'D:\\fs_test\\fs-test\\test_1.txt', 153 | 'D:\\fs_test\\fs-test\\test-2.bin', 154 | 'D:\\fs_test\\fs_test\\fs_test\\test_1.txt', 155 | 'D:\\fs_test\\fs_test\\fs_test\\test_2.test' 156 | ]; 157 | 158 | expect(result).to.have.members(correct); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for remove-files-webpack-plugin 1.4 2 | // Project: https://github.com/Amaimersion/remove-files-webpack-plugin/blob/master/README.md 3 | // Definitions by: Sergey Kuznetsov 4 | // Definitions: https://github.com/Amaimersion/remove-files-webpack-plugin 5 | // TypeScript Version: 3.7 6 | 7 | import {Compiler} from "webpack"; 8 | 9 | /** 10 | * A plugin for webpack that removes files and 11 | * folders before and after compilation. 12 | */ 13 | declare class RemovePlugin { 14 | constructor(parameters: PluginParameters); 15 | apply(compiler: Compiler): void; 16 | } 17 | 18 | /** 19 | * A parameters of plugin. 20 | */ 21 | interface PluginParameters { 22 | /** 23 | * Executes once before "normal" compilation. 24 | */ 25 | before?: RemoveParameters; 26 | 27 | /** 28 | * Executes every time before "watch" compilation. 29 | */ 30 | watch?: RemoveParameters; 31 | 32 | /** 33 | * Executes once after "normal" compilation and 34 | * every time after "watch" compilation. 35 | */ 36 | after?: RemoveParameters; 37 | } 38 | 39 | /** 40 | * A parameters of removing. 41 | */ 42 | interface RemoveParameters { 43 | /** 44 | * A root directory. 45 | * Not absolute paths will be appended to this. 46 | * 47 | * Defaults to `.` (where "package.json" and 48 | * "node_modules" are located). 49 | * 50 | * Namespace: all. 51 | */ 52 | root?: string; 53 | 54 | /** 55 | * A folders or files for removing. 56 | * 57 | * Defaults to `[]`. 58 | * 59 | * Namespace: all. 60 | */ 61 | include?: ReadonlyArray; 62 | 63 | /** 64 | * A folders or files for excluding. 65 | * 66 | * Defaults to `[]`. 67 | * 68 | * Namespace: all. 69 | */ 70 | exclude?: ReadonlyArray; 71 | 72 | /** 73 | * A folders for testing. 74 | * 75 | * Defaults to `[]`. 76 | * 77 | * Namespace: all. 78 | */ 79 | test?: ReadonlyArray; 80 | 81 | /** 82 | * If specified, will be called before removing. 83 | * Absolute paths of folders and files that will be removed 84 | * will be passed into this function. 85 | * If returned value is `true`, then 86 | * remove process will be canceled. 87 | * Will be not called if items for removing 88 | * not found, `emulate: true` or `skipFirstBuild: true`. 89 | * 90 | * Defaults to `undefined`. 91 | * 92 | * Namespace: all. 93 | */ 94 | beforeRemove?: ( 95 | absoluteFoldersPaths: string[], 96 | absoluteFilesPaths: string[] 97 | ) => boolean; 98 | 99 | /** 100 | * If specified, will be called after removing. 101 | * Absolute paths of folders and files that have been 102 | * removed will be passed into this function. 103 | * Will be not called if `emulate: true` or `skipFirstBuild: true`. 104 | * 105 | * Defaults to `undefined`. 106 | * 107 | * Namespace: all. 108 | */ 109 | afterRemove?: ( 110 | absoluteFoldersPaths: string[], 111 | absoluteFilesPaths: string[] 112 | ) => void; 113 | 114 | /** 115 | * Move folders and files to the trash (recycle bin) 116 | * instead of permanent removing. 117 | * **It is an async operation and you won't be 118 | * able to control an execution chain along with 119 | * other webpack plugins!** 120 | * `afterRemove` callback behavior is undefined 121 | * (it can be executed before, during or after actual execution). 122 | * 123 | * Defaults to `false`. 124 | * 125 | * Namespace: all. 126 | */ 127 | trash?: boolean; 128 | 129 | /** 130 | * Print messages of "info" level 131 | * (example: "Which folders or files have been removed"). 132 | * 133 | * Defaults to `true`. 134 | * 135 | * Namespace: all. 136 | */ 137 | log?: boolean; 138 | 139 | /** 140 | * Print messages of "warning" level 141 | * (example: "An items for removing not found"). 142 | * 143 | * Defaults to `true`. 144 | * 145 | * Namespace: all. 146 | */ 147 | logWarning?: boolean; 148 | 149 | /** 150 | * Print messages of "error" level 151 | * (example: "No such file or directory"). 152 | * 153 | * Defaults to `false`. 154 | * 155 | * Namespace: all. 156 | */ 157 | logError?: boolean; 158 | 159 | /** 160 | * Print messages of "debug" level 161 | * (used for debugging). 162 | * 163 | * Defaults to `false`. 164 | * 165 | * Namespace: all. 166 | */ 167 | logDebug?: boolean; 168 | 169 | /** 170 | * Emulate remove process. 171 | * Print which folders and files will be removed 172 | * without actually removing them. 173 | * Ignores `log` parameter. 174 | * 175 | * Defaults to `false`. 176 | * 177 | * Namespace: all. 178 | */ 179 | emulate?: boolean; 180 | 181 | /** 182 | * Allow removing of `root` directory or outside `root` directory. 183 | * It is kind of safe mode. 184 | * **Don't turn it on if you don't know what you actually do!** 185 | * 186 | * Defaults to `false`. 187 | * 188 | * Namespace: all. 189 | */ 190 | allowRootAndOutside?: boolean; 191 | 192 | /** 193 | * Change parameters based on webpack configuration. 194 | * Following webpack parameters are supported: 195 | * `stats` (controls logging). 196 | * These webpack parameters have priority over 197 | * the plugin parameters. 198 | * See webpack documentation for more - 199 | * https://webpack.js.org/configuration 200 | * 201 | * Defaults to `false`. 202 | * 203 | * Namespace: all. 204 | */ 205 | readWebpackConfiguration?: boolean; 206 | 207 | /** 208 | * First build will be skipped. 209 | * 210 | * Defaults to `false`. 211 | * 212 | * Namespace: `watch`. 213 | */ 214 | skipFirstBuild?: boolean; 215 | 216 | /** 217 | * For first build `before` parameters will be applied, 218 | * for subsequent builds `watch` parameters will be applied. 219 | * 220 | * Defaults to `false`. 221 | * 222 | * Namespace: `watch`. 223 | */ 224 | beforeForFirstBuild?: boolean; 225 | } 226 | 227 | /** 228 | * A folder for testing of items (folders or files) that should be removed. 229 | */ 230 | interface TestObject { 231 | /** 232 | * A path to the folder (relative to `root`). 233 | * 234 | * Namespace: all. 235 | */ 236 | folder: string; 237 | 238 | /** 239 | * A method that accepts an item path (`root` + folderPath + fileName) and 240 | * returns value that indicates should this item be removed or not. 241 | * 242 | * Namespace: all. 243 | */ 244 | method: (absolutePath: string) => boolean; 245 | 246 | /** 247 | * Apply this method to all items in subdirectories. 248 | * 249 | * Defaults to `false`. 250 | * 251 | * Namespace: all. 252 | */ 253 | recursive?: boolean; 254 | } 255 | 256 | export = RemovePlugin; 257 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: "eslint:recommended" 2 | 3 | env: 4 | es6: true 5 | node: true 6 | commonjs: true 7 | 8 | parserOptions: 9 | ecmaVersion: 9 10 | 11 | ignorePatterns: 12 | - "test/" 13 | 14 | rules: 15 | # Possible Errors 16 | no-await-in-loop: "warn" 17 | no-dupe-else-if: "error" 18 | no-import-assign: "error" 19 | no-template-curly-in-string: "warn" 20 | 21 | # Best Practices 22 | array-callback-return: "error" 23 | block-scoped-var: "error" 24 | complexity: 25 | - "error" 26 | - 20 27 | curly: "error" 28 | default-param-last: "error" 29 | dot-notation: "error" 30 | eqeqeq: "error" 31 | grouped-accessor-pairs: "error" 32 | guard-for-in: "error" 33 | no-caller: "error" 34 | no-else-return: "error" 35 | no-empty-function: "error" 36 | no-eq-null: "warn" 37 | no-eval: "error" 38 | no-extend-native: "error" 39 | no-extra-label: "error" 40 | no-implicit-coercion: 41 | - "error" 42 | - "allow": 43 | - "!!" 44 | no-implicit-globals: "off" # disabled because it is node package. 45 | no-implied-eval: "error" 46 | no-invalid-this: "error" 47 | no-iterator: "error" 48 | no-labels: "error" 49 | no-lone-blocks: "error" 50 | no-loop-func: "error" 51 | no-magic-numbers: 52 | - "error" 53 | - "ignore": 54 | - 0 55 | - 1 56 | "ignoreArrayIndexes": true 57 | "enforceConst": true 58 | no-multi-spaces: "error" 59 | no-new: "error" 60 | no-new-func: "error" 61 | no-new-wrappers: "error" 62 | no-octal-escape: "error" 63 | # no-param-reassign: "error" 64 | no-proto: "error" 65 | no-return-assign: "error" 66 | no-return-await: "error" 67 | no-script-url: "error" 68 | no-self-compare: "error" 69 | no-sequences: "error" 70 | no-throw-literal: "error" 71 | no-unmodified-loop-condition: "error" 72 | no-unused-expressions: "error" 73 | no-unused-labels: "error" 74 | no-useless-call: "error" 75 | no-useless-concat: "error" 76 | no-useless-return: "error" 77 | no-void: "error" 78 | no-warning-comments: "warn" 79 | no-with: "error" 80 | prefer-promise-reject-errors: "error" 81 | radix: 82 | - "error" 83 | - "always" 84 | require-await: "error" 85 | wrap-iife: 86 | - "error" 87 | - "outside" 88 | 89 | # Strict 90 | strict: 91 | - "error" 92 | - "safe" 93 | 94 | # Variables 95 | no-label-var: "error" 96 | no-shadow: "error" 97 | no-unused-vars: 98 | - "error" 99 | - "argsIgnorePattern": "^_" 100 | no-use-before-define: "error" 101 | 102 | # Node.js and CommonJS 103 | callback-return: "error" 104 | global-require: "error" 105 | handle-callback-err: "error" 106 | no-buffer-constructor: "error" 107 | no-new-require: "error" 108 | no-path-concat: "error" 109 | no-process-exit: "error" 110 | # no-sync: "warn" # because in webpack plugin sync operations should be used. 111 | 112 | # Stylistic Issues 113 | array-bracket-newline: 114 | - "error" 115 | - "consistent" 116 | array-bracket-spacing: 117 | - "error" 118 | - "never" 119 | array-element-newline: 120 | - "error" 121 | - "consistent" 122 | block-spacing: 123 | - "error" 124 | - "never" 125 | brace-style: 126 | - "error" 127 | - "1tbs" 128 | camelcase: 129 | - "error" 130 | - "properties": "always" 131 | comma-dangle: 132 | - "error" 133 | - "never" 134 | comma-spacing: 135 | - "error" 136 | - "before": false 137 | "after": true 138 | comma-style: 139 | - "error" 140 | - "last" 141 | computed-property-spacing: 142 | - "error" 143 | - "never" 144 | consistent-this: 145 | - "error" 146 | - "self" 147 | eol-last: 148 | - "error" 149 | - "always" 150 | func-call-spacing: 151 | - "error" 152 | - "never" 153 | implicit-arrow-linebreak: 154 | - "error" 155 | - "beside" 156 | indent: 157 | - "error" 158 | - 4 159 | - "SwitchCase": 1 160 | "ignoredNodes": 161 | - "ConditionalExpression" 162 | key-spacing: 163 | - "error" 164 | - "beforeColon": false 165 | "afterColon": true 166 | "mode": "strict" 167 | keyword-spacing: 168 | - "error" 169 | - "before": true 170 | "after": true 171 | lines-around-comment: 172 | - "error" 173 | - "beforeBlockComment": true 174 | "beforeLineComment": true 175 | "allowBlockStart": true 176 | "allowObjectStart": true 177 | "allowArrayStart": true 178 | "allowClassStart": true 179 | lines-between-class-members: 180 | - "error" 181 | - "always" 182 | max-depth: 183 | - "error" 184 | - 4 185 | max-statements-per-line: 186 | - "error" 187 | - "max": 1 188 | new-parens: 189 | - "error" 190 | - "always" 191 | newline-per-chained-call: "error" 192 | no-bitwise: "error" 193 | no-mixed-operators: "error" 194 | no-multi-assign: "error" 195 | no-multiple-empty-lines: 196 | - "error" 197 | - "max": 2 198 | no-negated-condition: "error" 199 | no-nested-ternary: "error" 200 | no-new-object: "error" 201 | no-tabs: "error" 202 | no-trailing-spaces: "error" 203 | no-unneeded-ternary: "error" 204 | no-whitespace-before-property: "error" 205 | nonblock-statement-body-position: 206 | - "error" 207 | - "beside" 208 | object-curly-spacing: 209 | - "error" 210 | - "never" 211 | object-property-newline: "error" 212 | operator-assignment: 213 | - "error" 214 | - "always" 215 | operator-linebreak: 216 | - "error" 217 | - "after" 218 | padded-blocks: 219 | - "error" 220 | - "never" 221 | padding-line-between-statements: 222 | - "error" 223 | - "blankLine": "always" 224 | "prev": "*" 225 | "next": "return" 226 | prefer-object-spread: "error" 227 | quote-props: 228 | - "error" 229 | - "as-needed" 230 | quotes: 231 | - "error" 232 | - "single" 233 | semi: 234 | - "error" 235 | - "always" 236 | semi-spacing: 237 | - "error" 238 | - "before": false 239 | "after": true 240 | semi-style: 241 | - "error" 242 | - "last" 243 | space-before-blocks: 244 | - "error" 245 | - "always" 246 | space-before-function-paren: 247 | - "error" 248 | - "never" 249 | space-in-parens: 250 | - "error" 251 | - "never" 252 | space-infix-ops: "error" 253 | space-unary-ops: 254 | - "error" 255 | - "words": true 256 | "nonwords": false 257 | spaced-comment: 258 | - "error" 259 | - "always" 260 | - "markers": 261 | - "#region" 262 | - "#endregion" 263 | switch-colon-spacing: 264 | - "error" 265 | - "after": true 266 | "before": false 267 | template-tag-spacing: 268 | - "error" 269 | - "always" 270 | 271 | # ECMAScript 6 272 | arrow-body-style: 273 | - "error" 274 | - "as-needed" 275 | arrow-parens: 276 | - "error" 277 | - "always" 278 | arrow-spacing: 279 | - "error" 280 | - "before": true 281 | "after": true 282 | generator-star-spacing: 283 | - "error" 284 | - "before": false 285 | "after": true 286 | no-confusing-arrow: 287 | - "error" 288 | - "allowParens": true 289 | no-duplicate-imports: "error" 290 | no-useless-computed-key: "error" 291 | no-useless-constructor: "error" 292 | no-useless-rename: "error" 293 | no-var: "error" 294 | prefer-arrow-callback: "error" 295 | prefer-const: "error" 296 | prefer-spread: "error" 297 | prefer-template: "off" # disabled because i have to use it in some places. 298 | template-curly-spacing: 299 | - "error" 300 | - "never" 301 | yield-star-spacing: 302 | - "error" 303 | - "after": true 304 | "before": false 305 | -------------------------------------------------------------------------------- /test/unit/path/to-absolute.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Path = require('../../../src/path'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('path', function () { 7 | describe('.toAbsolute()', function () { 8 | describe('win32', function () { 9 | const instance = new Path(); 10 | instance._type = 'win32'; 11 | 12 | it('double backward slash', function () { 13 | const result = instance.toAbsolute( 14 | 'D:\\path_test\\path-test', 15 | 'PaTh TeSt\\big + file.name.txt' 16 | ); 17 | const correct = 'D:\\path_test\\path-test\\PaTh TeSt\\big + file.name.txt'; 18 | 19 | expect(result).to.equal(correct); 20 | }); 21 | 22 | it('single forward slash', function () { 23 | const result = instance.toAbsolute( 24 | 'D:/path_test/path-test', 25 | 'PaTh TeSt/big + file.name.txt' 26 | ); 27 | const correct = 'D:\\path_test\\path-test\\PaTh TeSt\\big + file.name.txt'; 28 | 29 | expect(result).to.equal(correct); 30 | }); 31 | 32 | it('mixed slash', function () { 33 | const result = instance.toAbsolute( 34 | 'D:/path_test\\path-test/', 35 | 'PaTh TeSt\\big + file.name.txt' 36 | ); 37 | const correct = 'D:\\path_test\\path-test\\PaTh TeSt\\big + file.name.txt'; 38 | 39 | expect(result).to.equal(correct); 40 | }); 41 | 42 | it('single relative dot', function () { 43 | const result = instance.toAbsolute( 44 | 'D:/path_test/path-test', 45 | './PaTh TeSt/big + file.name.txt' 46 | ); 47 | const correct = 'D:\\path_test\\path-test\\PaTh TeSt\\big + file.name.txt'; 48 | 49 | expect(result).to.equal(correct); 50 | }); 51 | 52 | it('double relative dot', function () { 53 | const result = instance.toAbsolute( 54 | 'D:/path_test/path-test', 55 | 'PaTh TeSt/../big + file.name.txt' 56 | ); 57 | const correct = 'D:\\path_test\\path-test\\big + file.name.txt'; 58 | 59 | expect(result).to.equal(correct); 60 | }); 61 | 62 | it('relative dots lead abroad the disk drive', function () { 63 | const result = instance.toAbsolute( 64 | 'D:/path_test', 65 | 'path-test/../../..' 66 | ); 67 | const correct = 'D:\\'; 68 | 69 | expect(result).to.equal(correct); 70 | }); 71 | 72 | it('mixed slash and relative dots', function () { 73 | const result = instance.toAbsolute( 74 | 'D:\\path_test/path-test\\', 75 | './PaTh TeSt/..\\big + file.name.txt' 76 | ); 77 | const correct = 'D:\\path_test\\path-test\\big + file.name.txt'; 78 | 79 | expect(result).to.equal(correct); 80 | }); 81 | 82 | it('already absolute with double backward slash', function () { 83 | const result = instance.toAbsolute( 84 | 'D:\\path_test\\path-test', 85 | '\\PaTh TeSt\\big + file.name.txt' 86 | ); 87 | const correct = '\\PaTh TeSt\\big + file.name.txt'; 88 | 89 | expect(result).to.equal(correct); 90 | }); 91 | 92 | it('already absolute with single forward slash', function () { 93 | const result = instance.toAbsolute( 94 | 'D:\\path_test\\path-test', 95 | '/PaTh TeSt/big + file.name.txt' 96 | ); 97 | const correct = '/PaTh TeSt/big + file.name.txt'; 98 | 99 | expect(result).to.equal(correct); 100 | }); 101 | }); 102 | 103 | describe('posix', function () { 104 | const instance = new Path(); 105 | instance._type = 'posix'; 106 | 107 | it('single forward slash', function () { 108 | const result = instance.toAbsolute( 109 | '/path_test/path-test', 110 | 'PaTh TeSt/big + file.name.txt' 111 | ); 112 | const correct = '/path_test/path-test/PaTh TeSt/big + file.name.txt'; 113 | 114 | expect(result).to.equal(correct); 115 | }); 116 | 117 | it('single relative dot', function () { 118 | const result = instance.toAbsolute( 119 | '/path_test/path-test', 120 | './PaTh TeSt/big + file.name.txt' 121 | ); 122 | const correct = '/path_test/path-test/PaTh TeSt/big + file.name.txt'; 123 | 124 | expect(result).to.equal(correct); 125 | }); 126 | 127 | it('double relative dot', function () { 128 | const result = instance.toAbsolute( 129 | '/path_test/path-test', 130 | 'PaTh TeSt/../big + file.name.txt' 131 | ); 132 | const correct = '/path_test/path-test/big + file.name.txt'; 133 | 134 | expect(result).to.equal(correct); 135 | }); 136 | 137 | it('relative dots lead abroad the root', function () { 138 | const result = instance.toAbsolute( 139 | '/path_test', 140 | 'path-test/../../..' 141 | ); 142 | const correct = '/'; 143 | 144 | expect(result).to.equal(correct); 145 | }); 146 | 147 | it('mixed slash and relative dots', function () { 148 | const result = instance.toAbsolute( 149 | '/path_test/path-test/', 150 | './PaTh TeSt/../big + file.name.txt' 151 | ); 152 | const correct = '/path_test/path-test/big + file.name.txt'; 153 | 154 | expect(result).to.equal(correct); 155 | }); 156 | 157 | it('already absolute', function () { 158 | const result = instance.toAbsolute( 159 | '/path_test/path-test', 160 | '/PaTh TeSt/big + file.name.txt' 161 | ); 162 | const correct = '/PaTh TeSt/big + file.name.txt'; 163 | 164 | expect(result).to.equal(correct); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('.toAbsoluteS()', function () { 170 | const instance = new Path(); 171 | 172 | it('returns array with correct length', function () { 173 | const result = instance.toAbsoluteS( 174 | 'D:/path_test', 175 | [ 176 | 'test_1', 177 | '/test_2' 178 | ] 179 | ); 180 | 181 | expect(result).to.have.lengthOf(2); 182 | }); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /src/terminal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | //#region Types 5 | 6 | /** 7 | * @typedef {( 8 | * 'red' | 9 | * 'green' | 10 | * 'yellow' | 11 | * 'blue' | 12 | * 'magenta' | 13 | * 'cyan' | 14 | * 'white' 15 | * )} TerminalColor 16 | * Available color that can be used in terminal. 17 | */ 18 | 19 | /** 20 | * @typedef {Object} GenerateProperties 21 | * A properties for formatting of message that will be generated. 22 | * 23 | * @property {boolean} endDot 24 | * Dot character will be appended to the end of message, if not already presented. 25 | * Defaults to `false`. 26 | * 27 | * @property {TerminalColor} color 28 | * A desired color of message. 29 | * Defaults to `white`. 30 | */ 31 | 32 | /** 33 | * @typedef {Object} PrintProperties 34 | * A properties for formatting of print process. 35 | * 36 | * @property {boolean} pluginName 37 | * Plugin name will be appended to the start of message. 38 | * It will be in `cyan` color. 39 | * Defaults to `true`. 40 | 41 | * @property {boolean} newLineStart 42 | * New line character will be appended to the start of message. 43 | * Defaults to `true`. 44 | * 45 | * @property {boolean} newLineEnd 46 | * New line character will be appended to the end of message. 47 | * Defaults to `true`. 48 | */ 49 | 50 | /** 51 | * @typedef {Object} Items 52 | * Contains both files and folders paths. 53 | * 54 | * @property {string[]} files 55 | * Files paths. 56 | * 57 | * @property {string[]} directories 58 | * Folders paths. 59 | */ 60 | 61 | //#endregion 62 | 63 | 64 | const Info = require('./info'); 65 | 66 | 67 | /** 68 | * Operations with terminal. 69 | */ 70 | class Terminal { 71 | /** 72 | * Tab symbol. 73 | */ 74 | static get tab() { 75 | return ' '; 76 | } 77 | 78 | /** 79 | * Colorizes a message. 80 | * 81 | * - ANSI escape sequences is used for colors. 82 | * 83 | * @param {string} message 84 | * A message for colorizing. 85 | * 86 | * @param {TerminalColor} color 87 | * A desired color. 88 | * 89 | * @returns {string} 90 | * A colorized string. 91 | * 92 | * @see https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color#answer-41407246 93 | * @see http://bluesock.org/~willkg/dev/ansi.html 94 | */ 95 | static colorize(message, color) { 96 | message = `m${message}`; 97 | 98 | switch (color) { 99 | case 'red': 100 | message = `31${message}`; 101 | break; 102 | 103 | case 'green': 104 | message = `32${message}`; 105 | break; 106 | 107 | case 'yellow': 108 | message = `33${message}`; 109 | break; 110 | 111 | case 'blue': 112 | message = `34${message}`; 113 | break; 114 | 115 | case 'magenta': 116 | message = `35${message}`; 117 | break; 118 | 119 | case 'cyan': 120 | message = `36${message}`; 121 | break; 122 | 123 | default: 124 | case 'white': 125 | message = `37${message}`; 126 | break; 127 | } 128 | 129 | message = `\x1b[${message}\x1b[0m`; 130 | 131 | return message; 132 | } 133 | 134 | /** 135 | * Generates a message for terminal. 136 | * 137 | * @param {string} message 138 | * A raw message. 139 | * 140 | * @param {GenerateProperties} [params] 141 | * A properties of the message. 142 | * 143 | * @returns {string} 144 | * A modified message. 145 | */ 146 | static generateMessage(message, params = {}) { 147 | params = { 148 | ...{ 149 | endDot: false, 150 | color: 'white' 151 | }, 152 | ...params 153 | }; 154 | 155 | if ( 156 | params.endDot && 157 | message.charAt(message.length - 1) !== '.' 158 | ) { 159 | message += '.'; 160 | } 161 | 162 | if (params.color) { 163 | message = this.colorize(message, params.color); 164 | } 165 | 166 | return message; 167 | } 168 | 169 | /** 170 | * Generates an items message for 171 | * printing in terminal. 172 | * 173 | * @param {Items} items 174 | * An items for printing. 175 | * 176 | * @returns {string[]} 177 | * Generated messages. 178 | */ 179 | static generateItemsMessage(items) { 180 | const tabCount = 2; 181 | let tab = ''; 182 | 183 | for (let i = 0; i !== tabCount; i++) { 184 | tab += this.tab; 185 | } 186 | 187 | const messages = []; 188 | 189 | /** 190 | * @param {string[]} itms 191 | * @param {string} name 192 | */ 193 | const generate = (itms, name) => { 194 | if (!itms.length) { 195 | return; 196 | } 197 | 198 | /** @type {GenerateProperties} */ 199 | const commonParams = { 200 | endDot: false 201 | }; 202 | 203 | messages.push( 204 | this.generateMessage( 205 | `${tab}${name}:`, 206 | { 207 | ...commonParams, 208 | color: 'yellow' 209 | } 210 | ) 211 | ); 212 | 213 | for (const item of itms) { 214 | messages.push( 215 | this.generateMessage( 216 | `${tab}${tab}${item}`, 217 | { 218 | ...commonParams, 219 | color: 'green' 220 | } 221 | ) 222 | ); 223 | } 224 | }; 225 | 226 | generate(items.directories, 'folders'); 227 | generate(items.files, 'files'); 228 | 229 | return messages; 230 | } 231 | 232 | /** 233 | * Prints a messages in terminal. 234 | * 235 | * @param {Object} main 236 | * Can be `compiler` or `compilation` object. 237 | * 238 | * @param {string[]} messages 239 | * A messages for printing. 240 | * 241 | * @param {Logger.MessageGroup} group 242 | * A group of messages. 243 | * 244 | * @param {PrintProperties} [params] 245 | * A properties of the print process. 246 | */ 247 | static printMessages(_main, messages, group, params = {}) { 248 | if (!messages.length) { 249 | return; 250 | } 251 | 252 | /** @type {PrintProperties} */ 253 | params = { 254 | ...{ 255 | pluginName: true, 256 | newLineStart: true, 257 | newLineEnd: true 258 | }, 259 | ...params 260 | }; 261 | 262 | /** @type {Console} */ 263 | let logger = {}; 264 | 265 | /* https://github.com/Amaimersion/remove-files-webpack-plugin/issues/12 266 | if (main.getLogger) { 267 | logger = main.getLogger(Info.fullName); 268 | } else if (main.getInfrastructureLogger) { 269 | logger = main.getInfrastructureLogger(Info.fullName); 270 | } else { 271 | logger = console; 272 | } 273 | */ 274 | 275 | logger = console; 276 | let loggerMethod = 'log'; 277 | 278 | switch (group) { 279 | case 'info': 280 | loggerMethod = 'info'; 281 | break; 282 | 283 | case 'debug': 284 | loggerMethod = 'log'; 285 | break; 286 | 287 | case 'warning': 288 | loggerMethod = 'warn'; 289 | break; 290 | 291 | case 'error': 292 | loggerMethod = 'error'; 293 | break; 294 | } 295 | 296 | if (params.newLineStart) { 297 | logger[loggerMethod](''); 298 | } 299 | 300 | if (params.pluginName) { 301 | logger[loggerMethod]( 302 | `${this.colorize(`${Info.fullName}:`, 'cyan')}` 303 | ); 304 | } 305 | 306 | for (const message of messages) { 307 | logger[loggerMethod](message); 308 | } 309 | 310 | if (params.newLineEnd) { 311 | logger[loggerMethod](''); 312 | } 313 | } 314 | } 315 | 316 | 317 | module.exports = Terminal; 318 | -------------------------------------------------------------------------------- /test/unit/plugin/handle-remove.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const expect = require('chai').expect; 3 | const Plugin = require('../../../src/plugin'); 4 | 5 | 6 | describe('unit', function () { 7 | describe('plugin', function () { 8 | describe('.handleRemove()', function () { 9 | it('should not remove when emulate parameter is on', function () { 10 | const instance = new Plugin({ 11 | before: { 12 | root: './plugin_test_remove', 13 | allowRootAndOutside: false, 14 | emulate: true, 15 | trash: false, 16 | include: [ 17 | 'test1.txt', 18 | 'test1' 19 | ] 20 | } 21 | }); 22 | 23 | instance.handleRemove(instance.beforeParams); 24 | 25 | const folderExists = fs.existsSync('./plugin_test_remove/test1'); 26 | const fileExists = fs.existsSync('./plugin_test_remove/test1.txt'); 27 | 28 | expect(folderExists).to.equal(true); 29 | expect(fileExists).to.equal(true); 30 | }); 31 | 32 | it('should not remove when both emulate and trash parameters is on', function (done) { 33 | // trash removing is async, not sync. 34 | this.timeout(1000); 35 | 36 | const instance = new Plugin({ 37 | after: { 38 | root: './plugin_test_remove', 39 | allowRootAndOutside: false, 40 | emulate: true, 41 | trash: true, 42 | include: [ 43 | 'test1.txt', 44 | 'test1' 45 | ] 46 | } 47 | }); 48 | 49 | instance.handleRemove(instance.afterParams); 50 | 51 | setTimeout(() => { 52 | const folderExists = fs.existsSync('./plugin_test_remove/test1'); 53 | const fileExists = fs.existsSync('./plugin_test_remove/test1.txt'); 54 | 55 | if (folderExists && fileExists) { 56 | done(); 57 | } else { 58 | if (!folderExists) { 59 | done(new Error('Folder not exists')); 60 | } else { 61 | done(new Error('File not exists')); 62 | } 63 | } 64 | }, 800); 65 | }); 66 | 67 | it('should remove folders and files', function () { 68 | const instance = new Plugin({ 69 | watch: { 70 | root: './plugin_test_remove', 71 | allowRootAndOutside: false, 72 | emulate: false, 73 | trash: false, 74 | test: [ 75 | { 76 | folder: '.', 77 | method: (pth) => pth.includes('test2.txt') 78 | }, 79 | { 80 | folder: '.', 81 | method: (pth) => pth.includes('test2') 82 | } 83 | ] 84 | } 85 | }); 86 | 87 | instance.handleRemove(instance.watchParams); 88 | 89 | const folderExists = fs.existsSync('./plugin_test_remove/test2'); 90 | const fileExists = fs.existsSync('./plugin_test_remove/test2.txt'); 91 | 92 | expect(folderExists).to.equal(false); 93 | expect(fileExists).to.equal(false); 94 | }); 95 | 96 | it('should move folders and files to trash', function (done) { 97 | // trash removing is async, not sync. 98 | this.timeout(1000); 99 | 100 | const instance = new Plugin({ 101 | after: { 102 | root: './plugin_test_remove', 103 | allowRootAndOutside: false, 104 | emulate: false, 105 | trash: true, 106 | test: [ 107 | { 108 | folder: '.', 109 | method: (pth) => pth.includes('test3.txt') 110 | }, 111 | { 112 | folder: '.', 113 | method: (pth) => pth.includes('test3') 114 | } 115 | ] 116 | } 117 | }); 118 | 119 | instance.handleRemove(instance.afterParams); 120 | 121 | setTimeout(() => { 122 | const folderExists = fs.existsSync('./plugin_test_remove/test3'); 123 | const fileExists = fs.existsSync('./plugin_test_remove/test3.txt'); 124 | 125 | if (!folderExists && !fileExists) { 126 | done(); 127 | } else { 128 | if (folderExists) { 129 | done(new Error('Folder exists')); 130 | } else { 131 | done(new Error('File exists')); 132 | } 133 | } 134 | }, 800); 135 | }); 136 | 137 | it('should call beforeRemove and afterRemove', function () { 138 | let beforeRemoveIsCalled = false; 139 | let afterRemoveIsCalled = false; 140 | const instance = new Plugin({ 141 | before: { 142 | root: './plugin_test_remove', 143 | trash: false, 144 | include: [ 145 | 'test4.txt', 146 | 'test5.txt', 147 | 'test4' 148 | ], 149 | beforeRemove: (folders, files) => { 150 | expect(folders).to.have.lengthOf(1); 151 | expect(files).to.have.lengthOf(2); 152 | 153 | beforeRemoveIsCalled = true; 154 | }, 155 | afterRemove: (folders, files) => { 156 | expect(folders).to.have.lengthOf(1); 157 | expect(files).to.have.lengthOf(2); 158 | 159 | afterRemoveIsCalled = true; 160 | } 161 | } 162 | }); 163 | 164 | instance.handleRemove(instance.beforeParams); 165 | 166 | const folderExists = fs.existsSync('./plugin_test_remove/test4'); 167 | const filesExists = ( 168 | fs.existsSync('./plugin_test_remove/test4.txt') && 169 | fs.existsSync('./plugin_test_remove/test5.txt') 170 | ); 171 | 172 | expect(beforeRemoveIsCalled).to.equal(true); 173 | expect(afterRemoveIsCalled).to.equal(true); 174 | expect(folderExists).to.equal(false); 175 | expect(filesExists).to.equal(false); 176 | }); 177 | 178 | it('should call beforeRemove and cancel removing', function () { 179 | let beforeRemoveIsCalled = false; 180 | const instance = new Plugin({ 181 | watch: { 182 | root: './plugin_test_remove', 183 | trash: false, 184 | include: [ 185 | 'test6.txt' 186 | ], 187 | beforeRemove: (folders, files) => { 188 | expect(folders).to.have.lengthOf(0); 189 | expect(files).to.have.lengthOf(1); 190 | 191 | beforeRemoveIsCalled = true; 192 | 193 | return true; 194 | } 195 | } 196 | }); 197 | 198 | instance.handleRemove(instance.watchParams); 199 | 200 | const fileExists = fs.existsSync('./plugin_test_remove/test6.txt'); 201 | 202 | expect(beforeRemoveIsCalled).to.equal(true); 203 | expect(fileExists).to.equal(true); 204 | }); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /test/acceptance/small.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const expect = require('chai').expect; 4 | const Webpack = require('../webpack'); 5 | const RemovePlugin = require('../..'); 6 | 7 | 8 | describe('acceptance', function () { 9 | describe('small', function () { 10 | const logs = { 11 | log: false, 12 | logWarning: true, 13 | logError: true, 14 | logDebug: false 15 | }; 16 | 17 | it('should run beforeRun hook without trash', function () { 18 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 19 | const instance = new RemovePlugin({ 20 | before: { 21 | include: [ 22 | './acceptance_test_remove/test1.txt', 23 | './acceptance_test_remove/test1' 24 | ], 25 | trash: false, 26 | ...logs 27 | } 28 | }); 29 | 30 | instance.apply(webpack); 31 | webpack.runBeforeRun(); 32 | 33 | const fileExists = fs.existsSync( 34 | path.resolve( 35 | '.', 36 | 'acceptance_test_remove', 37 | 'test1.txt' 38 | ) 39 | ); 40 | const folderExists = fs.existsSync( 41 | path.resolve( 42 | '.', 43 | 'acceptance_test_remove', 44 | 'test1' 45 | ) 46 | ); 47 | 48 | expect(fileExists).to.equal(false); 49 | expect(folderExists).to.equal(false); 50 | }); 51 | 52 | it('should run watchRun hook with trash', function (done) { 53 | this.timeout(1000); 54 | 55 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 56 | const instance = new RemovePlugin({ 57 | watch: { 58 | include: [ 59 | './acceptance_test_remove/test2.txt', 60 | './acceptance_test_remove/test2' 61 | ], 62 | trash: true, 63 | ...logs 64 | } 65 | }); 66 | 67 | instance.apply(webpack); 68 | webpack.runWatchRun(); 69 | 70 | setTimeout(() => { 71 | const fileExists = fs.existsSync( 72 | path.resolve( 73 | '.', 74 | 'acceptance_test_remove', 75 | 'test2.txt' 76 | ) 77 | ); 78 | const folderExists = fs.existsSync( 79 | path.resolve( 80 | '.', 81 | 'acceptance_test_remove', 82 | 'test2' 83 | ) 84 | ); 85 | 86 | if (fileExists) { 87 | done(new Error('File exists')); 88 | } else if (folderExists) { 89 | done(new Error('Folder exists')); 90 | } else { 91 | done(); 92 | } 93 | }, 800); 94 | }); 95 | 96 | it('should run afterEmit hook without trash', function () { 97 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 98 | const instance = new RemovePlugin({ 99 | after: { 100 | include: [ 101 | './acceptance_test_remove/test3.txt', 102 | './acceptance_test_remove/test3' 103 | ], 104 | trash: false, 105 | ...logs 106 | } 107 | }); 108 | 109 | instance.apply(webpack); 110 | webpack.runAfterEmit(); 111 | 112 | const fileExists = fs.existsSync( 113 | path.resolve( 114 | '.', 115 | 'acceptance_test_remove', 116 | 'test3.txt' 117 | ) 118 | ); 119 | const folderExists = fs.existsSync( 120 | path.resolve( 121 | '.', 122 | 'acceptance_test_remove', 123 | 'test3' 124 | ) 125 | ); 126 | 127 | expect(fileExists).to.equal(false); 128 | expect(folderExists).to.equal(false); 129 | }); 130 | 131 | it('should run afterEmit hook with trash', function (done) { 132 | this.timeout(1000); 133 | 134 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 135 | const instance = new RemovePlugin({ 136 | after: { 137 | include: [ 138 | './acceptance_test_remove/test4.txt', 139 | './acceptance_test_remove/test4' 140 | ], 141 | trash: true, 142 | ...logs 143 | } 144 | }); 145 | 146 | instance.apply(webpack); 147 | webpack.runAfterEmit(); 148 | 149 | setTimeout(() => { 150 | const fileExists = fs.existsSync( 151 | path.resolve( 152 | '.', 153 | 'acceptance_test_remove', 154 | 'test4.txt' 155 | ) 156 | ); 157 | const folderExists = fs.existsSync( 158 | path.resolve( 159 | '.', 160 | 'acceptance_test_remove', 161 | 'test4' 162 | ) 163 | ); 164 | 165 | if (fileExists) { 166 | done(new Error('File exists')); 167 | } else if (folderExists) { 168 | done(new Error('Folder exists')); 169 | } else { 170 | done(); 171 | } 172 | }, 800); 173 | }); 174 | 175 | it('should run both beforeRun and afterEmit hooks', function () { 176 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 177 | const instance = new RemovePlugin({ 178 | before: { 179 | include: [ 180 | './acceptance_test_remove/test5.txt', 181 | './acceptance_test_remove/test5' 182 | ], 183 | trash: false, 184 | ...logs 185 | }, 186 | after: { 187 | include: [ 188 | './acceptance_test_remove/test6.txt', 189 | './acceptance_test_remove/test6' 190 | ], 191 | trash: false, 192 | ...logs 193 | } 194 | }); 195 | 196 | instance.apply(webpack); 197 | webpack.runBeforeRun(); 198 | webpack.runAfterEmit(); 199 | 200 | const filesExists = ( 201 | fs.existsSync( 202 | path.resolve( 203 | '.', 204 | 'acceptance_test_remove', 205 | 'test5.txt' 206 | ) 207 | ) || 208 | fs.existsSync( 209 | path.resolve( 210 | '.', 211 | 'acceptance_test_remove', 212 | 'test6.txt' 213 | ) 214 | ) 215 | ); 216 | const foldersExists = fs.existsSync( 217 | fs.existsSync( 218 | path.resolve( 219 | '.', 220 | 'acceptance_test_remove', 221 | 'test5' 222 | ) 223 | ) || 224 | fs.existsSync( 225 | path.resolve( 226 | '.', 227 | 'acceptance_test_remove', 228 | 'test6' 229 | ) 230 | ) 231 | ); 232 | 233 | expect(filesExists).to.equal(false); 234 | expect(foldersExists).to.equal(false); 235 | }); 236 | 237 | it('should remove file that is outside the root', function () { 238 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 239 | const instance = new RemovePlugin({ 240 | before: { 241 | root: '.', 242 | include: [ 243 | 'D:/acceptance_test_remove/test1.txt' 244 | ], 245 | allowRootAndOutside: true, 246 | ...logs 247 | } 248 | }); 249 | 250 | instance.apply(webpack); 251 | webpack.runBeforeRun(); 252 | 253 | const fileExists = fs.existsSync( 254 | 'D:/acceptance_test_remove/test1.txt' 255 | ); 256 | 257 | expect(fileExists).to.equal(false); 258 | }); 259 | 260 | it('should not remove file that is outside the root', function () { 261 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 262 | const instance = new RemovePlugin({ 263 | before: { 264 | root: '.', 265 | include: [ 266 | 'D:/acceptance_test_remove/test2.txt' 267 | ], 268 | allowRootAndOutside: false, 269 | ...logs, 270 | logWarning: false 271 | } 272 | }); 273 | 274 | instance.apply(webpack); 275 | webpack.runBeforeRun(); 276 | 277 | const fileExists = fs.existsSync( 278 | 'D:/acceptance_test_remove/test2.txt' 279 | ); 280 | 281 | expect(fileExists).to.equal(true); 282 | }); 283 | 284 | it('should not remove root', function () { 285 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 286 | const instance = new RemovePlugin({ 287 | before: { 288 | root: '.', 289 | include: [ 290 | '.' 291 | ], 292 | allowRootAndOutside: false, 293 | beforeRemove: (folders) => { 294 | if (folders.length || files.length) { 295 | throw new Error("Dangerous!"); 296 | } 297 | }, 298 | ...logs, 299 | logWarning: false 300 | } 301 | }); 302 | 303 | instance.apply(webpack); 304 | 305 | const run = () => { 306 | webpack.runBeforeRun(); 307 | }; 308 | 309 | expect(run).not.to.throw(); 310 | }); 311 | 312 | it('should not conflict when both include and test are provided', function () { 313 | const webpack = new Webpack.EmulatedWebpackCompiler.v4(); 314 | const instance = new RemovePlugin({ 315 | before: { 316 | root: './acceptance_test_remove/test7', 317 | include: [ 318 | 'test', 319 | 'test.txt' 320 | ], 321 | test: [ 322 | { 323 | folder: '.', 324 | method: () => true, 325 | recursive: true 326 | } 327 | ], 328 | ...logs 329 | } 330 | }); 331 | 332 | instance.apply(webpack); 333 | webpack.runBeforeRun(); 334 | 335 | const fileExists = fs.existsSync( 336 | path.resolve( 337 | '.', 338 | 'acceptance_test_remove', 339 | 'test7', 340 | 'test.txt' 341 | ) 342 | ); 343 | const folderExists = fs.existsSync( 344 | path.resolve( 345 | '.', 346 | 'acceptance_test_remove', 347 | 'test7', 348 | 'test' 349 | ) 350 | ); 351 | 352 | expect(fileExists).to.equal(false); 353 | expect(folderExists).to.equal(false); 354 | }); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /src/fs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | //#region Types 5 | 6 | /** 7 | * @typedef {Object} UnlinkFileParams 8 | * 9 | * @property {string} pth 10 | * An absolute path to the file. 11 | * 12 | * @property {boolean} [toTrash] 13 | * Move item to trash. 14 | * Defaults to `false`. 15 | * 16 | * @property {boolean} [rightTrashCallbacks] 17 | * Callbacks not works as expected, because trash is async. 18 | * We have to assume that trash working without errors, 19 | * because there is no more way to call success callback. 20 | * If `true`, then callbacks will be added in promise, 21 | * otherwise will be called (only success callbacks) before finising. 22 | * Defaults to `false`. 23 | * 24 | * @property {(pth: string) => any} [onSuccess] 25 | * Will be called if file successfully removed. 26 | * Defaults to `undefined`. 27 | * 28 | * @property {(error: string) => any} [onError] 29 | * Will be called if error occurs. 30 | * Defaults to `undefined`. 31 | */ 32 | 33 | /** 34 | * @typedef {Object} UnlinkFolderParams 35 | * 36 | * @property {string} pth 37 | * An absolute path to the folder. 38 | * 39 | * @property {(root: string, files: string[]) => string[]} toAbsoluteS 40 | * Converts paths to absolute paths. 41 | * 42 | * @property {boolean} [toTrash] 43 | * Move item to trash. 44 | * Defaults to `false`. 45 | * 46 | * @property {boolean} [rightTrashCallbacks] 47 | * Callbacks not works as expected, because trash is async. 48 | * We have to assume that trash working without errors, 49 | * because there is no more way to call success callback. 50 | * If `true`, then callbacks will be added in promise, 51 | * otherwise will be called (only success callbacks) before finising. 52 | * Defaults to `false`. 53 | * 54 | * @property {(filePath: string) => any} [onFileSuccess] 55 | * Will be called if file successfully removed. 56 | * Defaults to `undefined`. 57 | * 58 | * @property {(error: string) => any} [onFileError] 59 | * Will be called if error occurred during file removing. 60 | * Defaults to `undefined`. 61 | * 62 | * @property {(folderPath: string) => any} [onFolderSuccess] 63 | * Will be called if folder successfully removed. 64 | * Defaults to `undefined`. 65 | * 66 | * @property {(error: string) => any} [onFolderError] 67 | * Will be called if error occurred during folder removing. 68 | * Defaults to `undefined`. 69 | */ 70 | 71 | /** 72 | * @typedef {Object} GetItemsParams 73 | * 74 | * @property {string} pth 75 | * An absolute path to the folder. 76 | * 77 | * @property {(root: string, pth: string) => string[]} join 78 | * Joins provided root and path together. 79 | * 80 | * @property {boolean} [recursive] 81 | * Looks through all nested folders. 82 | * Defaults to `false`. 83 | * 84 | * @property {(absolutePath: string) => boolean} [test] 85 | * If provided, will be called, and item path will be added to 86 | * result in case of `true`, otherwise item path will be skipped. 87 | * Defaults to `undefined`. 88 | * 89 | * @property {(error: string) => any} [onItemError] 90 | * Will be called if item error occurs. 91 | * Defaults to `undefined`. 92 | * 93 | * @property {(absolutePath: string) => any} [onTestSuccess] 94 | * Will be called if test passed. 95 | * Defaults to `undefined`. 96 | * 97 | * @property {(absolutePath: string) => any} [onTestFail] 98 | * Will be called if test failed. 99 | * Defaults to `undefined`. 100 | */ 101 | 102 | //#endregion 103 | 104 | 105 | const fs = require('fs'); 106 | const trash = require('trash'); 107 | 108 | 109 | /** 110 | * Wrapper for Node `fs` module. 111 | */ 112 | class Fs { 113 | /** 114 | * Returns `fs` module. 115 | * 116 | * @returns {fs} 117 | * `fs` module. 118 | */ 119 | get fs() { 120 | return fs; 121 | } 122 | 123 | /** 124 | * Gets a stat for an item. 125 | * 126 | * @param {string} pth 127 | * An absolute path to the folder or file. 128 | * 129 | * @param {(error: string) => any} onError 130 | * Will be called if error occurs. 131 | * Defaults to `undefined`. 132 | * 133 | * @returns {fs.Stats} 134 | * An item stat or `undefined` if cannot get. 135 | */ 136 | getStatSync(pth, onError = undefined) { 137 | let stat = undefined; 138 | 139 | try { 140 | stat = this.fs.lstatSync(pth); 141 | } catch (error) { 142 | if (onError) { 143 | onError(error.message || error); 144 | } 145 | } 146 | 147 | return stat; 148 | } 149 | 150 | /** 151 | * Removes a file. 152 | * 153 | * - don't throws an error if file not exists. 154 | * - in case of moving to the trash this becomes an 155 | * async function and you should use callbacks 156 | * (consider `rightTrashCallbacks` parameter). 157 | * 158 | * @param {UnlinkFileParams} params 159 | * See `UnlinkFileParams` documentation. 160 | */ 161 | unlinkFileSync(params) { 162 | /** @type {UnlinkFileParams} */ 163 | params = { 164 | pth: '', 165 | toTrash: false, 166 | rightTrashCallbacks: false, 167 | onSuccess: undefined, 168 | onError: undefined, 169 | ...params 170 | }; 171 | const onSuccess = () => { 172 | if (params.onSuccess) { 173 | params.onSuccess(params.pth); 174 | } 175 | }; 176 | const onError = (message) => { 177 | if (params.onError) { 178 | params.onError(message); 179 | } 180 | }; 181 | 182 | if (params.toTrash) { 183 | if (params.rightTrashCallbacks) { 184 | trash(params.pth) 185 | .then(() => onSuccess()) 186 | .catch((error) => onError(error.message || error)); 187 | } else { 188 | onSuccess(); 189 | trash(params.pth) 190 | .catch((error) => { 191 | throw error; 192 | }); 193 | } 194 | 195 | return; 196 | } 197 | 198 | try { 199 | this.fs.unlinkSync(params.pth); 200 | onSuccess(); 201 | } catch (error) { 202 | onError(error.message || error); 203 | } 204 | } 205 | 206 | /** 207 | * Removes a folder. 208 | * 209 | * - in case of moving to the trash this becomes an 210 | * async function and you should use callbacks 211 | * (consider `rightTrashCallbacks` parameter). 212 | * - in case of moving to the trash 213 | * don't throws an error if folder not exists. 214 | * 215 | * @param {UnlinkFolderParams} params 216 | * See `UnlinkFolderParams` documentation. 217 | */ 218 | unlinkFolderSync(params) { 219 | /** @type {UnlinkFolderParams} */ 220 | params = { 221 | pth: '', 222 | toAbsoluteS: undefined, 223 | toTrash: false, 224 | rightTrashCallbacks: false, 225 | onFileSuccess: undefined, 226 | onFileError: undefined, 227 | onFolderSuccess: undefined, 228 | onFolderError: undefined, 229 | ...params 230 | }; 231 | const onFileError = (message) => { 232 | if (params.onFileError) { 233 | params.onFileError(message); 234 | } 235 | }; 236 | const onFolderSuccess = () => { 237 | if (params.onFolderSuccess) { 238 | params.onFolderSuccess(params.pth); 239 | } 240 | }; 241 | const onFolderError = (message) => { 242 | if (params.onFolderError) { 243 | params.onFolderError(message); 244 | } 245 | }; 246 | 247 | if (params.toTrash) { 248 | if (params.rightTrashCallbacks) { 249 | trash(params.pth) 250 | .then(() => onFolderSuccess()) 251 | .catch((error) => onFolderError(error.message || error)); 252 | } else { 253 | onFolderSuccess(); 254 | 255 | trash(params.pth).catch((error) => { 256 | throw error; 257 | }); 258 | } 259 | 260 | return; 261 | } 262 | 263 | let content = this.fs.readdirSync(params.pth); 264 | content = params.toAbsoluteS(params.pth, content); 265 | 266 | for (const item of content) { 267 | const stat = this.getStatSync(item); 268 | 269 | if (!stat) { 270 | onFileError(`Skipped, because unable to get stat - "${item}"`); 271 | 272 | continue; 273 | } else if (stat.isFile() || stat.isSymbolicLink()) { 274 | this.unlinkFileSync({ 275 | pth: item, 276 | toTrash: params.toTrash, 277 | rightTrashCallbacks: params.rightTrashCallbacks, 278 | onSuccess: params.onFileSuccess, 279 | onError: params.onFileError 280 | }); 281 | } else if (stat.isDirectory()) { 282 | this.unlinkFolderSync({ 283 | ...params, 284 | ...{ 285 | pth: item 286 | } 287 | }); 288 | } else { 289 | onFileError(`Skipped, because of invalid stat – "${item}"`); 290 | 291 | continue; 292 | } 293 | } 294 | 295 | try { 296 | fs.rmdirSync(params.pth); 297 | onFolderSuccess(); 298 | } catch (error) { 299 | onFolderError(error.message || error); 300 | } 301 | } 302 | 303 | /** 304 | * Gets list of absolute items paths. 305 | * 306 | * - if item is unnecessary, 307 | * it still will be returned. 308 | * 309 | * @param {GetItemsParams} params 310 | * See `GetItemsParams` documentation. 311 | * 312 | * @returns {string[]} 313 | * Absolute folders and files paths. 314 | */ 315 | getItemsSync(params) { 316 | /** @type {GetItemsParams} */ 317 | params = { 318 | pth: '', 319 | join: undefined, 320 | recursive: false, 321 | test: undefined, 322 | onItemError: undefined, 323 | onTestSuccess: undefined, 324 | onTestFail: undefined, 325 | ...params 326 | }; 327 | const onItemError = (message) => { 328 | if (params.onItemError) { 329 | params.onItemError(message); 330 | } 331 | }; 332 | const onTestSuccess = (message) => { 333 | if (params.onTestSuccess) { 334 | params.onTestSuccess(message); 335 | } 336 | }; 337 | const onTestFail = (message) => { 338 | if (params.onTestFail) { 339 | params.onTestFail(message); 340 | } 341 | }; 342 | 343 | /** @type {string[]} */ 344 | const items = this.fs.readdirSync(params.pth); 345 | 346 | /** @type {string[]} */ 347 | let result = []; 348 | 349 | for (let item of items) { 350 | item = params.join(params.pth, item); 351 | const stat = this.getStatSync(item); 352 | 353 | if (!stat) { 354 | onItemError(`Skipped, because unable to get stat - "${item}"`); 355 | 356 | continue; 357 | } else if (stat.isFile() || stat.isSymbolicLink()) { 358 | let passed = true; 359 | 360 | if (params.test) { 361 | passed = params.test(item); 362 | 363 | if (passed) { 364 | onTestSuccess(item); 365 | } else { 366 | onTestFail(item); 367 | } 368 | } 369 | 370 | if (passed) { 371 | result.push(item); 372 | } 373 | } else if (stat.isDirectory()) { 374 | let passed = false; 375 | 376 | if (params.test) { 377 | passed = params.test(item); 378 | 379 | if (passed) { 380 | onTestSuccess(item); 381 | } else { 382 | onTestFail(item); 383 | } 384 | } 385 | 386 | if (passed) { 387 | result.push(item); 388 | } 389 | 390 | if (params.recursive) { 391 | result = result.concat( 392 | this.getItemsSync({ 393 | ...params, 394 | ...{ 395 | pth: item 396 | } 397 | }) 398 | ); 399 | } 400 | } else { 401 | onItemError(`Skipped, because of invalid stat – "${item}"`); 402 | 403 | continue; 404 | } 405 | } 406 | 407 | return result; 408 | } 409 | } 410 | 411 | 412 | module.exports = Fs; 413 | -------------------------------------------------------------------------------- /test/unit/plugin/get-items-for-removing.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const expect = require('chai').expect; 4 | const Plugin = require('../../../src/plugin'); 5 | 6 | 7 | describe('unit', function () { 8 | describe('plugin', function () { 9 | describe('.getItemsForRemoving()', function () { 10 | const toAbsolute = (items) => path.resolve('.', ...items); 11 | 12 | it('should correctly handle include', function () { 13 | const instance = new Plugin({ 14 | before: { 15 | include: [ 16 | './plugin_test/test', 17 | './plugin_test\\test.txt', 18 | 'plugin_test\\test.jpg' 19 | ] 20 | } 21 | }); 22 | const result = instance.getItemsForRemoving(instance.beforeParams); 23 | 24 | expect(result.directories).to.have.members([ 25 | toAbsolute(['plugin_test', 'test']) 26 | ]); 27 | expect(result.files).to.have.members([ 28 | toAbsolute(['plugin_test', 'test.txt']), 29 | toAbsolute(['plugin_test', 'test.jpg']) 30 | ]); 31 | }); 32 | 33 | it('should correctly handle test', function () { 34 | const instance = new Plugin({ 35 | after: { 36 | test: [ 37 | { 38 | folder: './plugin_test', 39 | recursive: false, 40 | method: (absoluteItemPath) => { 41 | return ( 42 | absoluteItemPath === toAbsolute(['plugin_test', 'test.txt']) || 43 | absoluteItemPath === toAbsolute(['plugin_test', 'test.jpg']) 44 | ); 45 | } 46 | } 47 | ] 48 | } 49 | }); 50 | const result = instance.getItemsForRemoving(instance.afterParams); 51 | 52 | expect(result.directories).to.have.members([]); 53 | expect(result.files).to.have.members([ 54 | toAbsolute(['plugin_test', 'test.txt']), 55 | toAbsolute(['plugin_test', 'test.jpg']) 56 | ]); 57 | }); 58 | 59 | it('should correctly handle exclude', function () { 60 | const instance = new Plugin({ 61 | watch: { 62 | root: 'plugin_test', 63 | include: [ 64 | 'test.txt', 65 | 'test test.txt' 66 | ], 67 | test: [ 68 | { 69 | folder: '.', 70 | recursive: true, 71 | method: (absoluteItemPath) => { 72 | return ( 73 | absoluteItemPath.includes('.png') || 74 | absoluteItemPath.includes('.jpg') 75 | ); 76 | } 77 | } 78 | ], 79 | exclude: [ 80 | 'test', 81 | 'test test.txt' 82 | ] 83 | } 84 | }); 85 | const result = instance.getItemsForRemoving(instance.watchParams); 86 | 87 | expect(result.directories).to.have.members([]); 88 | expect(result.files).to.have.members([ 89 | toAbsolute(['plugin_test', 'test.txt']), 90 | toAbsolute(['plugin_test', 'test.jpg']), 91 | toAbsolute(['plugin_test', 'test', 'test.png']) 92 | ]); 93 | }); 94 | 95 | it('should skip not existed folders or files', function () { 96 | const instance = new Plugin({ 97 | before: { 98 | include: [ 99 | './plugin_test/test', 100 | './plugin_test/not exists', 101 | './plugin_test/not exists.txt' 102 | ] 103 | } 104 | }); 105 | const result = instance.getItemsForRemoving(instance.beforeParams); 106 | 107 | expect(result.directories).to.have.members([ 108 | toAbsolute(['plugin_test', 'test']) 109 | ]); 110 | }); 111 | 112 | it('should skip unsafe folders or files', function () { 113 | const instance = new Plugin({ 114 | before: { 115 | allowRootAndOutside: false, 116 | include: [ 117 | path.resolve('.'), 118 | 'D:\\plugin_test/test.txt', 119 | 'D:/plugin_test\\test' 120 | ] 121 | } 122 | }); 123 | const result = instance.getItemsForRemoving(instance.beforeParams); 124 | 125 | expect(result.directories).to.have.members([]); 126 | expect(result.files).to.have.members([]); 127 | }); 128 | 129 | it('should allow unsafe folders or files', function () { 130 | const instance = new Plugin({ 131 | before: { 132 | allowRootAndOutside: true, 133 | include: [ 134 | '.', 135 | 'D:\\plugin_test/test.txt', 136 | 'D:/plugin_test\\test' 137 | ] 138 | } 139 | }); 140 | const result = instance.getItemsForRemoving(instance.beforeParams); 141 | 142 | expect(result.directories).to.have.members([ 143 | path.resolve('.'), 144 | 'D:\\plugin_test\\test' 145 | ]); 146 | expect(result.files).to.have.members([ 147 | 'D:\\plugin_test\\test.txt' 148 | ]); 149 | }); 150 | 151 | it('should skip unnecessary folders or files', function () { 152 | const instance = new Plugin({ 153 | after: { 154 | root: 'plugin_test', 155 | include: [ 156 | 'test.txt', 157 | './test', 158 | '.\\test\\test', 159 | './test/test.txt', 160 | '.\\test/test.png' 161 | ] 162 | } 163 | }); 164 | const result = instance.getItemsForRemoving(instance.afterParams); 165 | 166 | expect(result.directories).to.have.members([ 167 | toAbsolute(['plugin_test', 'test']) 168 | ]); 169 | expect(result.files).to.have.members([ 170 | toAbsolute(['plugin_test', 'test.txt']) 171 | ]); 172 | }); 173 | 174 | it('should handle tricky paths', function () { 175 | const instance = new Plugin({ 176 | watch: { 177 | root: './plugin_test\\..', 178 | allowRootAndOutside: false, 179 | include: [ 180 | '.', 181 | 'plugin_test\\test\\../test.txt', 182 | '.\\plugin_test/test test.txt' 183 | ], 184 | test: [ 185 | { 186 | folder: 'plugin_test', 187 | recursive: false, 188 | method: (absoluteItemPath) => absoluteItemPath.includes('.txt') 189 | }, 190 | { 191 | folder: './plugin_test\\../plugin_test\\test\\', 192 | recursive: false, 193 | method: () => true 194 | } 195 | ], 196 | exclude: [ 197 | './plugin_test/../plugin_test/test test.txt', 198 | 'plugin_test\\test/test.png' 199 | ] 200 | } 201 | }); 202 | const result = instance.getItemsForRemoving(instance.watchParams); 203 | 204 | expect(result.directories).to.have.members([ 205 | toAbsolute(['plugin_test', 'test', 'test']) 206 | ]); 207 | expect(result.files).to.have.members([ 208 | toAbsolute(['plugin_test', 'test.txt']), 209 | toAbsolute(['plugin_test', 'test', 'test.txt']) 210 | ]); 211 | }); 212 | 213 | it('should handle test with folders and files', function () { 214 | const instance = new Plugin({ 215 | after: { 216 | root: 'plugin_test', 217 | allowRootAndOutside: false, 218 | include: [], 219 | test: [ 220 | { 221 | folder: '.', 222 | recursive: true, 223 | method: (absoluteItemPath) => fs.statSync(absoluteItemPath).isDirectory() 224 | }, 225 | { 226 | folder: '.', 227 | recursive: true, 228 | method: (absoluteItemPath) => fs.statSync(absoluteItemPath).isFile() 229 | } 230 | ] 231 | } 232 | }); 233 | const result = instance.getItemsForRemoving(instance.afterParams); 234 | 235 | expect(result.directories).to.have.members([ 236 | toAbsolute(['plugin_test', 'test']) 237 | ]); 238 | expect(result.files).to.have.members([ 239 | toAbsolute(['plugin_test', 'test.txt']), 240 | toAbsolute(['plugin_test', 'test test.txt']), 241 | toAbsolute(['plugin_test', 'test.jpg']) 242 | ]); 243 | }); 244 | 245 | it('should handle test № 1 with folders, files and exclude', function () { 246 | const instance = new Plugin({ 247 | before: { 248 | root: 'plugin_test', 249 | allowRootAndOutside: false, 250 | include: [], 251 | test: [ 252 | { 253 | folder: '.', 254 | recursive: true, 255 | method: (absoluteItemPath) => fs.statSync(absoluteItemPath).isDirectory() 256 | }, 257 | { 258 | folder: '.', 259 | recursive: true, 260 | method: (absoluteItemPath) => fs.statSync(absoluteItemPath).isFile() 261 | } 262 | ], 263 | exclude: [ 264 | './test test.txt', 265 | './test/test' 266 | ] 267 | } 268 | }); 269 | const result = instance.getItemsForRemoving(instance.beforeParams); 270 | 271 | expect(result.directories).to.have.members([]); 272 | expect(result.files).to.have.members([ 273 | toAbsolute(['plugin_test', 'test.txt']), 274 | toAbsolute(['plugin_test', 'test.jpg']), 275 | toAbsolute(['plugin_test', 'test', 'test.txt']), 276 | toAbsolute(['plugin_test', 'test', 'test.png']), 277 | toAbsolute(['plugin_test', 'test', 'test', 'test.bin']) 278 | ]); 279 | }); 280 | 281 | it('should handle test № 2 with folders, files and exclude', function () { 282 | const instance = new Plugin({ 283 | before: { 284 | root: 'plugin_test', 285 | allowRootAndOutside: false, 286 | include: [], 287 | test: [ 288 | { 289 | folder: '.', 290 | recursive: true, 291 | method: (absoluteItemPath) => fs.statSync(absoluteItemPath).isDirectory() 292 | }, 293 | { 294 | folder: '.', 295 | recursive: true, 296 | method: (absoluteItemPath) => fs.statSync(absoluteItemPath).isFile() 297 | } 298 | ], 299 | exclude: [ 300 | './test test.txt', 301 | './test' 302 | ] 303 | } 304 | }); 305 | const result = instance.getItemsForRemoving(instance.beforeParams); 306 | 307 | expect(result.directories).to.have.members([ 308 | toAbsolute(['plugin_test', 'test', 'test']) 309 | ]); 310 | expect(result.files).to.have.members([ 311 | toAbsolute(['plugin_test', 'test.txt']), 312 | toAbsolute(['plugin_test', 'test.jpg']), 313 | toAbsolute(['plugin_test', 'test', 'test.txt']), 314 | toAbsolute(['plugin_test', 'test', 'test.png']) 315 | ]); 316 | }); 317 | }); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script configures things so test scripts can work properly. 3 | * 4 | * At the moment this script will work only on Windows with both C and D local disks. 5 | * You should change root directory to D:/remove-files-webpack-plugin in terminal. 6 | */ 7 | 8 | 9 | const path = require('path'); 10 | const fs = require('fs'); 11 | 12 | 13 | /** 14 | * Folders for creating and deleting. 15 | * 16 | * - add a common root (like `D:/path_test`) 17 | * before children paths if you want to delete 18 | * all nested items with one call. 19 | */ 20 | const FOLDERS = [ 21 | 'D:/path_test', 22 | 'D:/path_test/test', 23 | 'D:/path_test/test%', 24 | 'D:/path_test/test1', 25 | 'D:/path_test/test test', 26 | 'D:/path_test/path-test/PaTh TeSt', 27 | 'D:/path_test/pa)t-h (t [e]s {t} +/Pa.^&th', 28 | 'D:/path_test/pa)t-h (t [e]s {t} +/Pa.^&th/test', 29 | 'D:/path-test', 30 | 'D:/path-test/PaTh TeSt', 31 | 'D:/path test', 32 | path.resolve('.', 'path_test'), 33 | path.resolve('.', 'path_test/path-test'), 34 | path.resolve('.', 'path-test'), 35 | 'D:/fs_test', 36 | 'D:/fs_test/fs-test', 37 | 'D:/fs_test/fs test', 38 | 'D:/fs_test/fs_test/fs_test', 39 | 'D:/fs_test_file_remove', 40 | 'D:/fs_test_folder_remove_1', 41 | 'D:/fs_test_folder_remove_2', 42 | 'D:/fs_test_folder_remove_3', 43 | 'D:/fs_test_folder_remove_3/test_1', 44 | 'D:/fs_test_folder_remove_3/test 2', 45 | 'D:/fs_test_folder_remove_4', 46 | 'D:/fs_test_folder_remove_4/test_1', 47 | 'D:/fs_test_folder_remove_4/test 2', 48 | 'D:/fs_test_folder_remove_4/test 2/test', 49 | 'D:/fs_test_folder_remove_5', 50 | 'D:/fs_test_folder_remove_6', 51 | 'D:/fs_test_folder_remove_7', 52 | 'D:/fs_test_folder_remove_7/test_1', 53 | 'D:/fs_test_folder_remove_7/test 2', 54 | 'D:/fs_test_folder_remove_8', 55 | 'D:/fs_test_folder_remove_8/test_1', 56 | 'D:/fs_test_folder_remove_8/test 2', 57 | 'D:/fs_test_folder_remove_8/test 2/test', 58 | 'D:/items_test', 59 | 'D:/items_test/dist/styles/css', 60 | 'D:/items_test/dist/styles/css_1/css', 61 | 'D:/items_test/dist/js/scripts', 62 | 'D:/items_test/dist/js/files/check', 63 | 'D:/items_test/dist/text', 64 | 'D:/items_test/test/test/test1', 65 | 'D:/items_test/test/styles/map', 66 | 'D:/items_test/js/scripts/files', 67 | 'D:/items_test/js/scripts/js', 68 | 'D:/items_test/js/scripts/maps', 69 | 'D:/items_test/js/maps/test_1', 70 | 'D:/items_test/js/maps/test_2', 71 | 'D:/items_test/test test/test', 72 | 'D:/items_test/css/styles', 73 | 'D:/items_test/css/maps', 74 | 'D:/items_test/css/files', 75 | 'D:/items_test/test/test_1', 76 | 'D:/items_test/test_1/test', 77 | 'D:/items_test/pa)t-h (t [e]s {t}/css', 78 | 'D:/items_test/pa)t-h (t [e]s {t}/folder/test', 79 | path.resolve('.', 'items_test/'), 80 | path.resolve('.', 'items_test/dist/js/scripts/test'), 81 | path.resolve('.', 'items_test/dist/js/scripts/files'), 82 | path.resolve('.', 'items_test/dist/css/maps'), 83 | path.resolve('.', 'items_test/dist/css/styles'), 84 | path.resolve('.', 'items_test/test/styles/styles'), 85 | path.resolve('.', 'items_test/test/test'), 86 | path.resolve('.', 'items_test/test/test_1'), 87 | path.resolve('.', 'items_test/js/maps'), 88 | path.resolve('.', 'items_test/js/scripts'), 89 | path.resolve('.', 'items_test/test_1/test'), 90 | 'D:/plugin_test', 91 | 'D:/plugin_test/test', 92 | path.resolve('.', 'plugin_test'), 93 | path.resolve('.', 'plugin_test/test'), 94 | path.resolve('.', 'plugin_test/test/test'), 95 | path.resolve('.', 'plugin_test_remove'), 96 | path.resolve('.', 'plugin_test_remove/test1'), 97 | path.resolve('.', 'plugin_test_remove/test2'), 98 | path.resolve('.', 'plugin_test_remove/test3'), 99 | path.resolve('.', 'plugin_test_remove/test4'), 100 | path.resolve('.', 'plugin_test_remove/test5'), 101 | 'D:/acceptance_test_remove', 102 | path.resolve('.', 'acceptance_test_remove'), 103 | path.resolve('.', 'acceptance_test_remove/test1'), 104 | path.resolve('.', 'acceptance_test_remove/test2'), 105 | path.resolve('.', 'acceptance_test_remove/test3'), 106 | path.resolve('.', 'acceptance_test_remove/test4'), 107 | path.resolve('.', 'acceptance_test_remove/test5'), 108 | path.resolve('.', 'acceptance_test_remove/test6'), 109 | path.resolve('.', 'acceptance_test_remove/test7/test'), 110 | path.resolve('.', 'acceptance_test_remove/dist1'), 111 | path.resolve('.', 'acceptance_test_remove/dist2/maps'), 112 | path.resolve('.', 'acceptance_test_remove/dist3/maps'), 113 | path.resolve('.', 'acceptance_test_remove/dist4/styles/test1/test2'), 114 | path.resolve('.', 'acceptance_test_remove/dist5/styles/test 1'), 115 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/test 1/test 2'), 116 | path.resolve('.', 'acceptance_test_remove/dist6/maps/test'), 117 | path.resolve('.', 'acceptance_test_remove/dist6/styles/test'), 118 | path.resolve('.', 'acceptance_test_remove/dist8/test/test'), 119 | path.resolve('.', 'acceptance_test_remove/dist8/test test'), 120 | path.resolve('.', 'acceptance_test_remove/dist9/js'), 121 | path.resolve('.', 'acceptance_test_remove/dist10/dist1'), 122 | path.resolve('.', 'acceptance_test_remove/dist10/dist2/js'), 123 | path.resolve('.', 'acceptance_test_remove/dist10/dist3/test'), 124 | path.resolve('.', 'acceptance_test_remove/large_test/Pa.^&th/test'), 125 | path.resolve('.', 'acceptance_test_remove/large_test/pa)t-h (t [e]s {t} +'), 126 | path.resolve('.', 'acceptance_test_remove/large_test/maps/test'), 127 | path.resolve('.', 'acceptance_test_remove/large_test/test'), 128 | path.resolve('.', 'acceptance_test_remove/large_test/styles/test'), 129 | path.resolve('.', 'plugin_test_remove'), 130 | path.resolve('.', 'plugin_test_remove/apply-hook') 131 | ]; 132 | 133 | /** 134 | * Files for creating and deleteing. 135 | */ 136 | const FILES = [ 137 | 'D:/path_test/path-test/PaTh TeSt/big + file.name.txt', 138 | 'D:/path-test/PaTh TeSt/big + file.name.txt', 139 | 'D:/path-test/big + file.name.txt', 140 | 'D:/path test.txt', 141 | 'D:/path_test/pa)t-h (t [e]s {t} +/Pa.^&th/test.txt', 142 | 'D:/fs_test/test_1.txt', 143 | 'D:/fs_test/test 2.txt', 144 | 'D:/fs_test/test-3.bin', 145 | 'D:/fs_test/fs-test/test_1.txt', 146 | 'D:/fs_test/fs-test/test-2.bin', 147 | 'D:/fs_test/fs test/test 1.bin', 148 | 'D:/fs_test/fs test/test 2.txt', 149 | 'D:/fs_test/fs_test/fs_test/test_1.txt', 150 | 'D:/fs_test/fs_test/fs_test/test_2.test', 151 | 'D:/fs_test_file_remove/1 pa)t-h (t [e]s {t} + file.name.txt', 152 | 'D:/fs_test_file_remove/2 pa)t-h (t [e]s {t} + file.name.txt', 153 | 'D:/fs_test_folder_remove_2/1 pa)t-h (t [e]s {t} + file.name.txt', 154 | 'D:/fs_test_folder_remove_2/2 pa)t-h (t [e]s {t} + file.name.txt', 155 | 'D:/fs_test_folder_remove_4/test.txt', 156 | 'D:/fs_test_folder_remove_4/test_1/test 1.txt', 157 | 'D:/fs_test_folder_remove_4/test_1/test 2.txt', 158 | 'D:/fs_test_folder_remove_4/test 2/pa)t-h (t [e]s {t} + file.name.txt', 159 | 'D:/fs_test_folder_remove_4/test 2/test/pa)t-h (t [e]s {t} + file.name.txt', 160 | 'D:/fs_test_folder_remove_6/1 pa)t-h (t [e]s {t} + file.name.txt', 161 | 'D:/fs_test_folder_remove_6/2 pa)t-h (t [e]s {t} + file.name.txt', 162 | 'D:/fs_test_folder_remove_8/test.txt', 163 | 'D:/fs_test_folder_remove_8/test_1/test 1.txt', 164 | 'D:/fs_test_folder_remove_8/test_1/test 2.txt', 165 | 'D:/fs_test_folder_remove_8/test 2/pa)t-h (t [e]s {t} + file.name.txt', 166 | 'D:/fs_test_folder_remove_8/test 2/test/pa)t-h (t [e]s {t} + file.name.txt', 167 | 'D:/items_test/text.txt', 168 | 'D:/items_test/dist/js/test.txt', 169 | 'D:/items_test/dist/manifest.json', 170 | 'D:/items_test/dist/js/scripts/test.js', 171 | 'D:/items_test/dist/text/test.txt', 172 | 'D:/items_test/dist/styles/popup.css', 173 | 'D:/items_test/dist/styles/popup.css.map', 174 | 'D:/items_test/js/scripts/js/test.js', 175 | 'D:/items_test/js/scripts/js/test_2.js', 176 | 'D:/items_test/js/scripts/maps/test.js', 177 | 'D:/items_test/css/styles/1.css', 178 | 'D:/items_test/css/maps/1.css', 179 | 'D:/items_test/js/scripts/test.js', 180 | 'D:/items_test/js/scripts/test.map', 181 | 'D:/items_test/js/test.js', 182 | 'D:/items_test/js/scripts/map.js', 183 | 'D:/items_test/js/scripts/style.js', 184 | 'D:/items_test/test.txt', 185 | 'D:/items_test/js/maps/test.js', 186 | 'D:/items_test/css/style.css', 187 | 'D:/items_test/css/maps/folder.css', 188 | 'D:/items_test/test/test.txt', 189 | 'D:/items_test/test/test_1/test.txt', 190 | 'D:/items_test/test_1/test.txt', 191 | 'D:/items_test/pa)t-h (t [e]s {t}/test.txt', 192 | 'D:/items_test/pa)t-h (t [e]s {t}/css/test.css', 193 | 'D:/items_test/pa)t-h (t [e]s {t}/folder/test.txt', 194 | 'D:/items_test/pa)t-h (t [e]s {t}/folder/test/test.bin', 195 | path.resolve('.', 'items_test/js/1.js'), 196 | path.resolve('.', 'items_test/js/maps/file.js'), 197 | path.resolve('.', 'items_test/js/maps/file2.js'), 198 | path.resolve('.', 'items_test/js/scripts/file.js'), 199 | path.resolve('.', 'items_test/js/scripts/js.js'), 200 | path.resolve('.', 'items_test/test.js'), 201 | path.resolve('.', 'items_test/test_1/test.txt'), 202 | path.resolve('.', 'items_test/test/test.txt'), 203 | path.resolve('.', 'items_test/test/test_1/test.txt'), 204 | path.resolve('.', 'items_test/test_1/test/test.txt'), 205 | path.resolve('.', 'items_test/test/styles/style.css'), 206 | path.resolve('.', 'items_test/test/styles/test.txt'), 207 | 'D:/plugin_test/test.txt', 208 | path.resolve('.', 'plugin_test/test.txt'), 209 | path.resolve('.', 'plugin_test/test test.txt'), 210 | path.resolve('.', 'plugin_test/test.jpg'), 211 | path.resolve('.', 'plugin_test/test/test.txt'), 212 | path.resolve('.', 'plugin_test/test/test.png'), 213 | path.resolve('.', 'plugin_test/test/test/test.bin'), 214 | path.resolve('.', 'plugin_test_remove/test1.txt'), 215 | path.resolve('.', 'plugin_test_remove/test2.txt'), 216 | path.resolve('.', 'plugin_test_remove/test3.txt'), 217 | path.resolve('.', 'plugin_test_remove/test4.txt'), 218 | path.resolve('.', 'plugin_test_remove/test5.txt'), 219 | path.resolve('.', 'plugin_test_remove/test6.txt'), 220 | 'D:/acceptance_test_remove/test1.txt', 221 | 'D:/acceptance_test_remove/test2.txt', 222 | 'D:/acceptance_test_remove/test3.txt', 223 | path.resolve('.', 'acceptance_test_remove/test1.txt'), 224 | path.resolve('.', 'acceptance_test_remove/test2.txt'), 225 | path.resolve('.', 'acceptance_test_remove/test3.txt'), 226 | path.resolve('.', 'acceptance_test_remove/test4.txt'), 227 | path.resolve('.', 'acceptance_test_remove/test5.txt'), 228 | path.resolve('.', 'acceptance_test_remove/test6.txt'), 229 | path.resolve('.', 'acceptance_test_remove/test7/test.txt'), 230 | path.resolve('.', 'acceptance_test_remove/dist2/manifest.json'), 231 | path.resolve('.', 'acceptance_test_remove/dist2/test.txt'), 232 | path.resolve('.', 'acceptance_test_remove/dist2/maps/test.txt'), 233 | path.resolve('.', 'acceptance_test_remove/dist3/manifest.json'), 234 | path.resolve('.', 'acceptance_test_remove/dist3/test.txt'), 235 | path.resolve('.', 'acceptance_test_remove/dist3/maps/test.txt'), 236 | path.resolve('.', 'acceptance_test_remove/dist4/test.txt'), 237 | path.resolve('.', 'acceptance_test_remove/dist4/styles/1.map'), 238 | path.resolve('.', 'acceptance_test_remove/dist4/styles/1.map.css'), 239 | path.resolve('.', 'acceptance_test_remove/dist4/styles/test1/1.map'), 240 | path.resolve('.', 'acceptance_test_remove/dist4/styles/test1/test.txt'), 241 | path.resolve('.', 'acceptance_test_remove/dist4/styles/test1/test2/test file.map'), 242 | path.resolve('.', 'acceptance_test_remove/dist4/styles/test1/test2/test.txt'), 243 | path.resolve('.', 'acceptance_test_remove/dist5/test.txt'), 244 | path.resolve('.', 'acceptance_test_remove/dist5/styles/1.css.map'), 245 | path.resolve('.', 'acceptance_test_remove/dist5/styles/1.css'), 246 | path.resolve('.', 'acceptance_test_remove/dist5/styles/test 1/1.css.map'), 247 | path.resolve('.', 'acceptance_test_remove/dist5/styles/test 1/1.css'), 248 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/1.js.map'), 249 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/1.js'), 250 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/test 1/1.js.map'), 251 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/test 1/1.js'), 252 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/test 1/test 2/1.js.map'), 253 | path.resolve('.', 'acceptance_test_remove/dist5/scripts/test 1/test 2/1.js'), 254 | path.resolve('.', 'acceptance_test_remove/dist6/test.txt'), 255 | path.resolve('.', 'acceptance_test_remove/dist6/maps/1.map.js'), 256 | path.resolve('.', 'acceptance_test_remove/dist6/maps/2.map.js'), 257 | path.resolve('.', 'acceptance_test_remove/dist6/maps/main.map.js'), 258 | path.resolve('.', 'acceptance_test_remove/dist6/maps/test/1.map.js'), 259 | path.resolve('.', 'acceptance_test_remove/dist6/styles/1.css'), 260 | path.resolve('.', 'acceptance_test_remove/dist6/styles/1.css.map'), 261 | path.resolve('.', 'acceptance_test_remove/dist6/styles/popup.css'), 262 | path.resolve('.', 'acceptance_test_remove/dist6/styles/popup.css.map'), 263 | path.resolve('.', 'acceptance_test_remove/dist6/styles/test/1.css'), 264 | path.resolve('.', 'acceptance_test_remove/dist6/styles/test/1.css.map'), 265 | path.resolve('.', 'acceptance_test_remove/dist8/test.txt'), 266 | path.resolve('.', 'acceptance_test_remove/dist8/test/test1.txt'), 267 | path.resolve('.', 'acceptance_test_remove/dist8/test/test2.txt'), 268 | path.resolve('.', 'acceptance_test_remove/dist8/test/test/test3.txt'), 269 | path.resolve('.', 'acceptance_test_remove/dist9/js/entry.js'), 270 | path.resolve('.', 'acceptance_test_remove/dist10/dist2/js/test.js'), 271 | path.resolve('.', 'acceptance_test_remove/dist10/dist2/js/test-nvsdl1234.js'), 272 | path.resolve('.', 'acceptance_test_remove/dist10/dist3/log.txt'), 273 | path.resolve('.', 'acceptance_test_remove/dist10/dist3/test.txt'), 274 | path.resolve('.', 'acceptance_test_remove/large_test/pa)t-h (t [e]s {t} + file.name.txt'), 275 | path.resolve('.', 'acceptance_test_remove/large_test/test.txt'), 276 | path.resolve('.', 'acceptance_test_remove/large_test/Pa.^&th/test.txt'), 277 | path.resolve('.', 'acceptance_test_remove/large_test/Pa.^&th/another.txt'), 278 | path.resolve('.', 'acceptance_test_remove/large_test/Pa.^&th/test/test.txt'), 279 | path.resolve('.', 'acceptance_test_remove/large_test/Pa.^&th/test/another.txt'), 280 | path.resolve('.', 'acceptance_test_remove/large_test/maps/test.txt'), 281 | path.resolve('.', 'acceptance_test_remove/large_test/maps/test.map'), 282 | path.resolve('.', 'acceptance_test_remove/large_test/maps/1.map'), 283 | path.resolve('.', 'acceptance_test_remove/large_test/maps/test/1.map'), 284 | path.resolve('.', 'acceptance_test_remove/large_test/maps/test/file.map'), 285 | path.resolve('.', 'acceptance_test_remove/large_test/pa)t-h (t [e]s {t} +/test.txt'), 286 | path.resolve('.', 'acceptance_test_remove/large_test/pa)t-h (t [e]s {t} +/1.map'), 287 | path.resolve('.', 'plugin_test_remove/apply-hook/test1.txt'), 288 | path.resolve('.', 'plugin_test_remove/apply-hook/test2.txt'), 289 | path.resolve('.', 'plugin_test_remove/apply-hook/test3.txt'), 290 | path.resolve('.', 'plugin_test_remove/apply-hook/test4.txt'), 291 | path.resolve('.', 'plugin_test_remove/apply-hook/test5.txt'), 292 | path.resolve('.', 'plugin_test_remove/apply-hook/test5.txt'), 293 | path.resolve('.', 'plugin_test_remove/apply-hook/test6.txt'), 294 | ]; 295 | 296 | 297 | before(function () { 298 | const createFolder = (pth) => { 299 | fs.mkdirSync(pth, { 300 | recursive: true 301 | }); 302 | }; 303 | const createFile = (pth) => { 304 | fs.writeFileSync(pth); 305 | }; 306 | 307 | for (const folder of FOLDERS) { 308 | createFolder(folder); 309 | } 310 | 311 | for (const file of FILES) { 312 | createFile(file); 313 | } 314 | }); 315 | 316 | 317 | after(function () { 318 | const deleteFolder = (pth) => { 319 | if (fs.existsSync(pth)) { 320 | fs.rmdirSync(pth, { 321 | recursive: true 322 | }); 323 | } 324 | }; 325 | const deleteFile = (pth) => { 326 | if (fs.existsSync(pth)) { 327 | fs.unlinkSync(pth); 328 | } 329 | }; 330 | 331 | for (const folder of FOLDERS) { 332 | deleteFolder(folder); 333 | } 334 | 335 | for (const file of FILES) { 336 | deleteFile(file); 337 | } 338 | }); 339 | -------------------------------------------------------------------------------- /test/unit/items/remove-unnecessary.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Items = require('../../../src/items'); 3 | 4 | 5 | describe('unit', function () { 6 | describe('items', function () { 7 | describe('.removeUnnecessary()', function () { 8 | const instance = new Items(); 9 | 10 | it('should remove nested folders with forward slash', function () { 11 | instance.directories = [ 12 | '/items_test/dist/styles/css', 13 | '/items_test/dist/styles/css_1', 14 | '/items_test/dist/styles/css_1/css', 15 | '/items_test/dist/js/scripts', 16 | '/items_test/dist/js/files', 17 | '/items_test/dist/js/files/check', 18 | '/items_test/dist/styles', 19 | '/items_test/test' 20 | ]; 21 | instance.files = []; 22 | 23 | instance.removeUnnecessary(); 24 | 25 | expect(instance.directories).to.have.members([ 26 | '/items_test/dist/js/scripts', 27 | '/items_test/dist/js/files', 28 | '/items_test/dist/styles', 29 | '/items_test/test' 30 | ]); 31 | expect(instance.files).to.have.members([]); 32 | }); 33 | 34 | it('should remove nested folders with backward slash', function () { 35 | instance.directories = [ 36 | 'D:\\items_test\\js\\scripts\\files', 37 | 'D:\\items_test\\js\\scripts', 38 | 'D:\\items_test\\js\\maps', 39 | 'D:\\items_test\\js\\maps\\test_1', 40 | 'D:\\items_test\\js\\maps\\test_2', 41 | 'D:\\items_test\\test', 42 | 'D:\\items_test\\test test' 43 | ]; 44 | instance.files = []; 45 | 46 | instance.removeUnnecessary(); 47 | 48 | expect(instance.directories).to.have.members([ 49 | 'D:\\items_test\\js\\scripts', 50 | 'D:\\items_test\\js\\maps', 51 | 'D:\\items_test\\test', 52 | 'D:\\items_test\\test test' 53 | ]); 54 | expect(instance.files).to.have.members([]); 55 | }); 56 | 57 | it('should remove nested folders with mixed slash', function () { 58 | instance.directories = [ 59 | 'D:\\items_test/test test', 60 | 'D:\\items_test\\js/maps', 61 | 'D:/items_test/js\\scripts', 62 | 'D:/items_test\\js', 63 | 'D:\\items_test/test/test', 64 | 'D:/items_test/test\\test\\test1', 65 | 'D:/items_test/test test\\test' 66 | ]; 67 | instance.files = []; 68 | 69 | instance.removeUnnecessary(); 70 | 71 | expect(instance.directories).to.have.members([ 72 | 'D:\\items_test/test test', 73 | 'D:/items_test\\js', 74 | 'D:\\items_test/test/test' 75 | ]); 76 | expect(instance.files).to.have.members([]); 77 | }); 78 | 79 | it('should remove relative children folders', function () { 80 | instance.directories = [ 81 | 'items_test/dist/css', 82 | 'items_test/dist/js/scripts/test', 83 | 'items_test/dist/js/scripts/files', 84 | 'items_test/dist/css/maps', 85 | 'items_test/dist/css/styles', 86 | 'items_test/dist/js' 87 | ]; 88 | instance.files = []; 89 | 90 | instance.removeUnnecessary(); 91 | 92 | expect(instance.directories).to.have.members([ 93 | 'items_test/dist/css', 94 | 'items_test/dist/js' 95 | ]); 96 | expect(instance.files).to.have.members([]); 97 | }); 98 | 99 | it('should remove all folders except Windows root', function () { 100 | instance.directories = [ 101 | 'D:\\items_test/test test', 102 | 'D:\\items_test/js/maps', 103 | 'D:/items_test/js\\scripts', 104 | 'D:/items_test/js', 105 | 'D:\\items_test\\test/test', 106 | 'D:/items_test/test\\test\\test1', 107 | 'D:/items_test/test test\\test', 108 | 'D:\\' 109 | ]; 110 | instance.files = []; 111 | 112 | instance.removeUnnecessary(); 113 | 114 | expect(instance.directories).to.have.members([ 115 | 'D:\\' 116 | ]); 117 | expect(instance.files).to.have.members([]); 118 | }); 119 | 120 | it('should remove all folders except Unix root', function () { 121 | instance.directories = [ 122 | '/items_test/js/maps', 123 | '/items_test/test test', 124 | '/items_test/js/scripts', 125 | '/items_test/js', 126 | '/items_test/test/test', 127 | '/items_test/test/test/test1', 128 | '/items_test/test test/test', 129 | '/' 130 | ]; 131 | instance.files = []; 132 | 133 | instance.removeUnnecessary(); 134 | 135 | expect(instance.directories).to.have.members([ 136 | '/' 137 | ]); 138 | expect(instance.files).to.have.members([]); 139 | }); 140 | 141 | /* can't test it, because not allowed to access not custom folders. 142 | it('should keep folder on another local disk', function () { 143 | instance.directories = [ 144 | 'C:/test/test', 145 | 'D:/test' 146 | ]; 147 | instance.files = []; 148 | 149 | instance.removeUnnecessary(); 150 | 151 | expect(instance.directories).to.have.members([ 152 | 'C:/test/test', 153 | 'D:/test' 154 | ]); 155 | expect(instance.files).to.have.members([]); 156 | }); 157 | */ 158 | 159 | it('should distinguish absolute and relative folders', function () { 160 | instance.directories = [ 161 | './items_test/test/styles', 162 | '/items_test/test', 163 | 'items_test/test/test', 164 | '/items_test/test/styles', 165 | '/items_test/test/styles/map', 166 | 'items_test/test/styles/styles' 167 | ]; 168 | instance.files = []; 169 | 170 | instance.removeUnnecessary(); 171 | 172 | expect(instance.directories).to.have.members([ 173 | './items_test/test/styles', 174 | '/items_test/test', 175 | 'items_test/test/test' 176 | ]); 177 | expect(instance.files).to.have.members([]); 178 | }); 179 | 180 | it('should keep files if there is no any folders', function () { 181 | instance.directories = []; 182 | instance.files = [ 183 | '/items_test/dist/js/test.txt', 184 | '/items_test/text.txt', 185 | '/items_test/dist/js/scripts/test.js', 186 | '/items_test/dist/text/test.txt' 187 | ]; 188 | 189 | instance.removeUnnecessary(); 190 | 191 | expect(instance.directories).to.have.members([]); 192 | expect(instance.files).to.have.members([ 193 | '/items_test/dist/js/test.txt', 194 | '/items_test/text.txt', 195 | '/items_test/dist/js/scripts/test.js', 196 | '/items_test/dist/text/test.txt' 197 | ]); 198 | }); 199 | 200 | it('should remove nested files with forward slash', function () { 201 | instance.directories = [ 202 | '/items_test/js/scripts/js', 203 | '/items_test/js/scripts/maps', 204 | '/items_test/css' 205 | ]; 206 | instance.files = [ 207 | '/items_test/text.txt', 208 | '/items_test/js/scripts/js/test.js', 209 | '/items_test/js/scripts/js/test_2.js', 210 | '/items_test/js/scripts/maps/test.js', 211 | '/items_test/css/styles/1.css', 212 | '/items_test/css/maps/1.css', 213 | '/items_test/js/scripts/test.js', 214 | '/items_test/js/scripts/test.map' 215 | ]; 216 | 217 | instance.removeUnnecessary(); 218 | 219 | expect(instance.directories).to.have.members([ 220 | '/items_test/js/scripts/js', 221 | '/items_test/js/scripts/maps', 222 | '/items_test/css' 223 | ]); 224 | expect(instance.files).to.have.members([ 225 | '/items_test/text.txt', 226 | '/items_test/js/scripts/test.js', 227 | '/items_test/js/scripts/test.map' 228 | ]); 229 | }); 230 | 231 | it('should remove nested files with backward slash', function () { 232 | instance.directories = [ 233 | 'D:\\items_test\\js\\scripts', 234 | 'D:\\items_test\\js\\maps', 235 | 'D:\\items_test\\css\\maps', 236 | 'D:\\items_test\\css\\files', 237 | 'D:\\items_test\\test' 238 | ]; 239 | instance.files = [ 240 | 'D:\\items_test\\js\\test.js', 241 | 'D:\\items_test\\js\\scripts\\map.js', 242 | 'D:\\items_test\\js\\scripts\\style.js', 243 | 'D:\\items_test\\test.txt', 244 | 'D:\\items_test\\js\\maps\\test.js', 245 | 'D:\\items_test\\css\\style.css', 246 | 'D:\\items_test\\css\\maps\\folder.css' 247 | ]; 248 | 249 | instance.removeUnnecessary(); 250 | 251 | expect(instance.directories).to.have.members([ 252 | 'D:\\items_test\\js\\scripts', 253 | 'D:\\items_test\\js\\maps', 254 | 'D:\\items_test\\css\\maps', 255 | 'D:\\items_test\\css\\files', 256 | 'D:\\items_test\\test' 257 | ]); 258 | expect(instance.files).to.have.members([ 259 | 'D:\\items_test\\js\\test.js', 260 | 'D:\\items_test\\test.txt', 261 | 'D:\\items_test\\css\\style.css' 262 | ]); 263 | }); 264 | 265 | it('should remove nested files with mixed slash', function () { 266 | instance.directories = [ 267 | 'D:\\items_test/js\\scripts', 268 | 'D:/items_test\\js\\scripts/maps' 269 | ]; 270 | instance.files = [ 271 | 'D:\\items_test\\js/scripts/js\\test_2.js', 272 | 'D:/items_test/js\\scripts\\maps/test.js', 273 | 'D:\\items_test/text.txt' 274 | ]; 275 | 276 | instance.removeUnnecessary(); 277 | 278 | expect(instance.directories).to.have.members([ 279 | 'D:\\items_test/js\\scripts' 280 | ]); 281 | expect(instance.files).to.have.members([ 282 | 'D:\\items_test/text.txt' 283 | ]); 284 | }); 285 | 286 | it('should remove relative children files', function () { 287 | instance.directories = [ 288 | 'items_test/js/maps', 289 | 'items_test/js/scripts' 290 | ]; 291 | instance.files = [ 292 | 'items_test/js/1.js', 293 | 'items_test/js/maps/file.js', 294 | 'items_test/js/maps/file2.js', 295 | 'items_test/js/scripts/file.js', 296 | 'items_test/js/scripts/js.js', 297 | 'items_test/test.js' 298 | ]; 299 | 300 | instance.removeUnnecessary(); 301 | 302 | expect(instance.directories).to.have.members([ 303 | 'items_test/js/maps', 304 | 'items_test/js/scripts' 305 | ]); 306 | expect(instance.files).to.have.members([ 307 | 'items_test/js/1.js', 308 | 'items_test/test.js' 309 | ]); 310 | }); 311 | 312 | it('should remove all files with Windows root', function () { 313 | instance.directories = [ 314 | 'D:\\' 315 | ]; 316 | instance.files = [ 317 | 'D:\\items_test/text.txt', 318 | 'D:/items_test\\dist/js/test.txt', 319 | 'C:\\Windows/explorer.exe' 320 | ]; 321 | 322 | instance.removeUnnecessary(); 323 | 324 | expect(instance.directories).to.have.members([ 325 | 'D:\\' 326 | ]); 327 | expect(instance.files).to.have.members([ 328 | 'C:\\Windows/explorer.exe' 329 | ]); 330 | }); 331 | 332 | it('should remove all files with Unix root', function () { 333 | instance.directories = [ 334 | '/' 335 | ]; 336 | instance.files = [ 337 | '/items_test/dist/js/test.txt', 338 | '/items_test/text.txt', 339 | ]; 340 | 341 | instance.removeUnnecessary(); 342 | 343 | expect(instance.directories).to.have.members([ 344 | '/' 345 | ]); 346 | expect(instance.files).to.have.members([]); 347 | }); 348 | 349 | it('should distinguish absolute and relative files', function () { 350 | instance.directories = [ 351 | 'D:\\items_test\\test', 352 | 'D:\\items_test\\test_1\\test', 353 | '.\\items_test\\test_1\\test' 354 | ]; 355 | instance.files = [ 356 | 'items_test\\test_1\\test.txt', 357 | 'items_test\\test\\test.txt', 358 | 'D:\\items_test\\test.txt', 359 | 'D:\\items_test\\test\\test.txt', 360 | 'D:\\items_test\\test_1\\test.txt', 361 | '.\\items_test\\test_1\\test\\test.txt' 362 | ]; 363 | 364 | instance.removeUnnecessary(); 365 | 366 | expect(instance.directories).to.have.members([ 367 | 'D:\\items_test\\test', 368 | 'D:\\items_test\\test_1\\test', 369 | '.\\items_test\\test_1\\test' 370 | ]); 371 | expect(instance.files).to.have.members([ 372 | 'items_test\\test_1\\test.txt', 373 | 'items_test\\test\\test.txt', 374 | 'D:\\items_test\\test.txt', 375 | 'D:\\items_test\\test_1\\test.txt' 376 | ]); 377 | }); 378 | 379 | it('should remove both folders and files', function () { 380 | instance.directories = [ 381 | 'D:/items_test/dist/styles/css', 382 | 'D:/items_test/dist/js/scripts', 383 | 'D:/items_test/dist/styles', 384 | 'D:/items_test/test', 385 | 'C:/Windows' 386 | ]; 387 | instance.files = [ 388 | 'D:/items_test/dist/styles/popup.css', 389 | 'D:/items_test/dist/styles/popup.css.map', 390 | 'D:/items_test/dist/manifest.json', 391 | 'D:/items_test/text.txt', 392 | 'D:/items_test/dist/js/scripts/test.js' 393 | ]; 394 | 395 | instance.removeUnnecessary(); 396 | 397 | expect(instance.directories).to.have.members([ 398 | 'D:/items_test/dist/js/scripts', 399 | 'D:/items_test/dist/styles', 400 | 'D:/items_test/test', 401 | 'C:/Windows' 402 | ]); 403 | expect(instance.files).to.have.members([ 404 | 'D:/items_test/dist/manifest.json', 405 | 'D:/items_test/text.txt' 406 | ]); 407 | }); 408 | 409 | it('should remove both folders and files with mixed absolute and relative', function () { 410 | instance.directories = [ 411 | 'C:/Windows', 412 | 'C:/Windows/System32', 413 | './items_test/test/styles', 414 | 'items_test/test/test', 415 | './items_test/test/styles/styles', 416 | 'D:/items_test/test' 417 | ]; 418 | instance.files = [ 419 | 'C:/Windows/explorer.exe', 420 | 'items_test/test/test.txt', 421 | 'items_test/test/test_1/test.txt', 422 | './items_test/test/styles/style.css', 423 | './items_test/test/styles/test.txt', 424 | 'D:/items_test/text.txt', 425 | 'D:/items_test/test/test.txt' 426 | ]; 427 | 428 | instance.removeUnnecessary(); 429 | 430 | expect(instance.directories).to.have.members([ 431 | 'C:/Windows', 432 | './items_test/test/styles', 433 | 'items_test/test/test', 434 | 'D:/items_test/test' 435 | ]); 436 | expect(instance.files).to.have.members([ 437 | 'items_test/test/test.txt', 438 | 'items_test/test/test_1/test.txt', 439 | 'D:/items_test/text.txt' 440 | ]); 441 | }); 442 | 443 | it('should remove both folders and files with hard names', function () { 444 | instance.directories = [ 445 | '/items_test/pa)t-h (t [e]s {t}/folder', 446 | '/items_test/pa)t-h (t [e]s {t}/css', 447 | '/items_test/pa)t-h (t [e]s {t}/folder/test' 448 | ]; 449 | instance.files = [ 450 | '/items_test/pa)t-h (t [e]s {t}/test.txt', 451 | '/items_test/pa)t-h (t [e]s {t}/css/test.css', 452 | '/items_test/pa)t-h (t [e]s {t}/folder/test.txt', 453 | '/items_test/pa)t-h (t [e]s {t}/folder/test/test.bin' 454 | ]; 455 | 456 | instance.removeUnnecessary(); 457 | 458 | expect(instance.directories).to.have.members([ 459 | '/items_test/pa)t-h (t [e]s {t}/folder', 460 | '/items_test/pa)t-h (t [e]s {t}/css' 461 | ]); 462 | expect(instance.files).to.have.members([ 463 | '/items_test/pa)t-h (t [e]s {t}/test.txt' 464 | ]); 465 | }); 466 | }); 467 | }); 468 | }); 469 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Removing of folders and files for Webpack 3 |

4 | 5 |

6 | A plugin for webpack that removes files and folders before and after compilation. 7 |

8 | 9 | 10 | ## Content 11 | 12 | - [Content](#content) 13 | - [Installation](#installation) 14 | - [Support](#support) 15 | - [Usage](#usage) 16 | - [Notes](#notes) 17 | - [Symbolic links](#symbolic-links) 18 | - [Notes for Windows users](#notes-for-windows-users) 19 | - [Single backward slash](#single-backward-slash) 20 | - [Segment separator](#segment-separator) 21 | - [Per-drive working directory](#per-drive-working-directory) 22 | - [Parameters](#parameters) 23 | - [How to set](#how-to-set) 24 | - [Namespace](#namespace) 25 | - [Compilation modes](#compilation-modes) 26 | - [Examples](#examples) 27 | - [Version naming](#version-naming) 28 | - [Contribution](#contribution) 29 | - [License](#license) 30 | 31 | 32 | ## Installation 33 | 34 | - With `npm`: 35 | ```javascript 36 | npm install remove-files-webpack-plugin 37 | ``` 38 | 39 | - With `yarn`: 40 | ```javascript 41 | yarn add remove-files-webpack-plugin 42 | ``` 43 | 44 | 45 | ## Support 46 | 47 | The plugin works on any OS and webpack >= 2.2.0. 48 | 49 | 50 | ## Usage 51 | 52 | ```javascript 53 | const RemovePlugin = require('remove-files-webpack-plugin'); 54 | 55 | module.exports = { 56 | plugins: [ 57 | new RemovePlugin({ 58 | before: { 59 | // parameters for "before normal compilation" stage. 60 | }, 61 | watch: { 62 | // parameters for "before watch compilation" stage. 63 | }, 64 | after: { 65 | // parameters for "after normal and watch compilation" stage. 66 | } 67 | }) 68 | ] 69 | }; 70 | ``` 71 | 72 | 73 | ## Notes 74 | 75 | ### Symbolic links 76 | 77 | Symbolic links (soft links) will be treated as ordinary files. 78 | 79 | 80 | ## Notes for Windows users 81 | 82 | ### Single backward slash 83 | 84 | JavaScript uses it for escaping. If you want to use backward slash, then use double backward slash. Example: `C:\\Windows\\System32\\cmd.exe`. By the way, single forward slashes are also supported. 85 | 86 | ### Segment separator 87 | 88 | All paths that you get or see from the plugin will contain platform-specific segment separator (i.e. slash): `\\` on Windows and `/` on POSIX. So, for example, even if you passed folders or files with `/` as separator, `TestObject.method` will give you a path with `\\` as segment separator. 89 | 90 | ### Per-drive working directory 91 | 92 | From [Node.js documentation](https://nodejs.org/api/path.html#path_windows_vs_posix): 93 | > On Windows Node.js follows the concept of per-drive working directory. This behavior can be observed when using a drive path without a backslash. For example, `path.resolve('c:\\')` can potentially return a different result than `path.resolve('c:')`. For more information, see [this MSDN page](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#fully-qualified-vs-relative-paths). 94 | 95 | 96 | ## Parameters 97 | 98 | | Name | Type | Default | Namespace | Description | 99 | | :----------------------: | :------------------------------------------------------------------------------------------: | :---------: | :-------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 100 | | root | `string` | `.` | All | A root directory.
Not absolute paths will be appended to this.
Defaults to where `package.json` and `node_modules` are located. | 101 | | include | `string[]` | `[]` | All | A folders or files for removing. | 102 | | exclude | `string[]` | `[]` | All | A folders or files for excluding. | 103 | | test | `TestObject[]` | `[]` | All | A folders for testing. | 104 | | TestObject.folder | `string` | Required | All | A path to the folder (relative to `root`). | 105 | | TestObject.method | `(absolutePath: string) => boolean` | Required | All | A method that accepts an item path (`root` + folderPath + fileName) and
returns value that indicates should this item be removed or not.
| 106 | | TestObject.recursive | `boolean` | `false` | All | Apply this method to all items in subdirectories. | 107 | | beforeRemove | `(`
`absoluteFoldersPaths: string[],`
`absoluteFilesPaths: string[]`
`) => boolean` | `undefined` | All | If specified, will be called before removing.
Absolute paths of folders and files that
will be removed will be passed into this function.
If returned value is `true`,
then remove process will be canceled.
Will be not called if items for removing
not found, `emulate: true` or `skipFirstBuild: true`. | 108 | | afterRemove | `(`
`absoluteFoldersPaths: string[],`
`absoluteFilesPaths: string[]`
`) => void` | `undefined` | All | If specified, will be called after removing.
Absolute paths of folders and files
that have been removed will be passed into this function.
Will be not called if `emulate: true` or `skipFirstBuild: true`. | 109 | | trash | `boolean` | `false` | All | Move folders and files to the trash (recycle bin) instead of permanent removing.
**It is an async operation and you won't be able to control an execution chain along with other webpack plugins!**
`afterRemove` callback behavior is undefined (it can be executed before, during or after actual execution).
Requires Windows 8+, macOS 10.12+ or Linux. | 110 | | log | `boolean` | `true` | All | Print messages of "info" level
(example: "Which folders or files have been removed"). | 111 | | logWarning | `boolean` | `true` | All | Print messages of "warning" level
(example: "An items for removing not found"). | 112 | | logError | `boolean` | `false` | All | Print messages of "error" level
(example: "No such file or directory"). | 113 | | logDebug | `boolean` | `false` | All | Print messages of "debug" level
(used for debugging). | 114 | | emulate | `boolean` | `false` | All | Emulate remove process.
Print which folders and files will be removed
without actually removing them.
Ignores `log` parameter. | 115 | | allowRootAndOutside | `boolean` | `false` | All | Allow removing of `root` directory or outside `root` directory.
It is kind of safe mode.
**Don't turn it on if you don't know
what you actually do!** | 116 | | readWebpackConfiguration | `boolean` | `false` | All | Change parameters based on webpack configuration.
Following webpack parameters are supported: `stats` (controls logging).
These webpack parameters have priority over the plugin parameters.
See webpack documentation for more - https://webpack.js.org/configuration | 117 | | skipFirstBuild | `boolean` | `false` | `watch` | First build will be skipped. | 118 | | beforeForFirstBuild | `boolean` | `false` | `watch` | For first build `before` parameters will be applied,
for subsequent builds `watch` parameters will be applied. | | | | | | 119 | 120 | ### How to set 121 | 122 | You can pass these parameters into any of the following keys: `before`, `watch` or `after`. Each key is optional, but at least one should be specified. 123 | 124 | - `before` – executes once before "normal" compilation. 125 | - `watch` – executes every time before "watch" compilation. 126 | - `after` – executes once after "normal" compilation and every time after "watch" compilation. 127 | 128 | ### Namespace 129 | 130 | "Namespace" means where particular parameter will be applied. For example, "All" means particular parameter will work in any key (`before`, `watch`, `after`), `watch` means particular parameter will work only in `watch` key. 131 | 132 | ### Compilation modes 133 | 134 | - "normal" compilation means full compilation. 135 | - "watch" compilation means first build is a full compilation and subsequent builds is a short rebuilds of changed files. 136 | 137 | 138 | ## Examples 139 | 140 | ```javascript 141 | new RemovePlugin({ 142 | /** 143 | * Before compilation permanently removes 144 | * entire `./dist` folder. 145 | */ 146 | before: { 147 | include: [ 148 | './dist' 149 | ] 150 | } 151 | }) 152 | ``` 153 | 154 | ```javascript 155 | new RemovePlugin({ 156 | /** 157 | * Every time before "watch" compilation 158 | * permanently removes `./dist/js/entry.js` file. 159 | */ 160 | watch: { 161 | include: [ 162 | './dist/js/entry.js' 163 | ] 164 | } 165 | }) 166 | ``` 167 | 168 | ```javascript 169 | new RemovePlugin({ 170 | /** 171 | * After compilation moves both 172 | * `./dist/manifest.json` file and 173 | * `./dist/maps` folder to the trash. 174 | */ 175 | after: { 176 | root: './dist', 177 | include: [ 178 | 'manifest.json', 179 | 'maps' 180 | ], 181 | trash: true 182 | } 183 | }) 184 | ``` 185 | 186 | ```javascript 187 | new RemovePlugin({ 188 | /** 189 | * Before compilation permanently removes both 190 | * `./dist/manifest.json` file and `./dist/maps` folder. 191 | * Log only works for warnings and errors. 192 | */ 193 | before: { 194 | include: [ 195 | 'dist/manifest.json', 196 | './dist/maps' 197 | ], 198 | log: false, 199 | logWarning: true, 200 | logError: true, 201 | logDebug: false 202 | } 203 | }) 204 | ``` 205 | 206 | ```javascript 207 | new RemovePlugin({ 208 | /** 209 | * After compilation permanently removes 210 | * all maps files in `./dist/styles` folder and 211 | * all subfolders (e.g. `./dist/styles/header`). 212 | */ 213 | after: { 214 | test: [ 215 | { 216 | folder: 'dist/styles', 217 | method: (absoluteItemPath) => { 218 | return new RegExp(/\.map$/, 'm').test(absoluteItemPath); 219 | }, 220 | recursive: true 221 | } 222 | ] 223 | } 224 | }) 225 | ``` 226 | 227 | ```javascript 228 | new RemovePlugin({ 229 | /** 230 | * After compilation: 231 | * - permanently removes all css maps in `./dist/styles` folder. 232 | * - permanently removes all js maps in `./dist/scripts` folder and 233 | * all subfolders (e.g. `./dist/scripts/header`). 234 | */ 235 | after: { 236 | root: './dist', 237 | test: [ 238 | { 239 | folder: './styles', 240 | method: (absoluteItemPath) => { 241 | return new RegExp(/\.css\.map$/, 'm').test(absoluteItemPath); 242 | } 243 | }, 244 | { 245 | folder: './scripts', 246 | method: (absoluteItemPath) => { 247 | return new RegExp(/\.js\.map$/, 'm').test(absoluteItemPath); 248 | }, 249 | recursive: true 250 | } 251 | ] 252 | } 253 | }) 254 | ``` 255 | 256 | ```javascript 257 | new RemovePlugin({ 258 | /** 259 | * Before compilation permanently removes all 260 | * folders, subfolders and files from `./dist/maps` 261 | * except `./dist/maps/main.map.js` file. 262 | */ 263 | before: { 264 | root: './dist' 265 | /** 266 | * You should do like this 267 | * instead of `include: ['./maps']`. 268 | */ 269 | test: [ 270 | { 271 | folder: './maps', 272 | method: () => true, 273 | recursive: true 274 | } 275 | ], 276 | exclude: [ 277 | './maps/main.map.js' 278 | ] 279 | }, 280 | 281 | /** 282 | * After compilation permanently removes 283 | * all css maps in `./dist/styles` folder 284 | * except `popup.css.map` file. 285 | */ 286 | after: { 287 | test: [ 288 | { 289 | folder: 'dist/styles', 290 | method: (absoluteItemPath) => { 291 | return new RegExp(/\.css\.map$/, 'm').test(absoluteItemPath); 292 | } 293 | } 294 | ], 295 | exclude: [ 296 | 'dist/styles/popup.css.map' 297 | ] 298 | } 299 | }) 300 | ``` 301 | 302 | ```javascript 303 | new RemovePlugin({ 304 | /** 305 | * Before "normal" compilation permanently 306 | * removes entire `./dist` folder. 307 | */ 308 | before: { 309 | include: [ 310 | './dist' 311 | ] 312 | }, 313 | 314 | /** 315 | * Every time before compilation in "watch" 316 | * mode (`webpack --watch`) permanently removes JS files 317 | * with hash in the name (like "entry-vqlr39sdvl12.js"). 318 | */ 319 | watch: { 320 | test: [ 321 | { 322 | folder: './dist/js', 323 | method: (absPath) => new RegExp(/(.*)-([^-\\\/]+)\.js/).test(absPath) 324 | } 325 | ] 326 | }, 327 | 328 | /** 329 | * Once after "normal" compilation or every time 330 | * after "watch" compilation moves `./dist/log.txt` 331 | * file to the trash. 332 | */ 333 | after: { 334 | include: [ 335 | './dist/log.txt' 336 | ], 337 | trash: true 338 | } 339 | }) 340 | ``` 341 | 342 | ```javascript 343 | new RemovePlugin({ 344 | /** 345 | * Before compilation emulates remove process 346 | * for a file that is outside of the root directory. 347 | * That file will be moved to the trash in case of 348 | * not emulation. 349 | */ 350 | before: { 351 | root: '.', // "D:\\remove-files-webpack-plugin-master" 352 | include: [ 353 | "C:\\Desktop\\test.txt" 354 | ], 355 | trash: true, 356 | emulate: true, 357 | allowRootAndOutside: true 358 | } 359 | }) 360 | ``` 361 | 362 | ```javascript 363 | new RemovePlugin({ 364 | /** 365 | * After compilation grabs all files from 366 | * all subdirectories and decides should 367 | * remove process be continued or not. 368 | * If removed process is continued, 369 | * then logs results with custom logger. 370 | */ 371 | after: { 372 | root: './dist', 373 | test: [ 374 | { 375 | folder: '.', 376 | method: () => true, 377 | recursive: true 378 | } 379 | ], 380 | beforeRemove: (absoluteFoldersPaths, absoluteFilesPaths) => { 381 | // cancel removing if there at least one folder. 382 | if (absoluteFoldersPaths.length) { 383 | return true; 384 | } 385 | 386 | // cancel removing if there at least one `.txt` file. 387 | for (const item of absoluteFilesPaths) { 388 | if (item.includes('.txt')) { 389 | return true; 390 | } 391 | } 392 | }, 393 | afterRemove: (absoluteFoldersPaths, absoluteFilesPaths) => { 394 | // replacing plugin logger with custom logger. 395 | console.log('Successfully removed:'); 396 | console.log(`Folders – [${absoluteFoldersPaths}]`); 397 | console.log(`Files – [${absoluteFilesPaths}]`); 398 | }, 399 | log: false 400 | } 401 | }) 402 | ``` 403 | 404 | 405 | ## Version naming 406 | 407 | This project uses following structure for version naming: `..`. 408 | 409 | 410 | ## Contribution 411 | 412 | Feel free to use [issues](https://github.com/Amaimersion/remove-files-webpack-plugin/issues/new/choose). [Pull requests](https://github.com/Amaimersion/remove-files-webpack-plugin/compare) are also always welcome! 413 | 414 | 415 | ## License 416 | 417 | [MIT](https://github.com/Amaimersion/remove-files-webpack-plugin/blob/master/LICENSE). 418 | --------------------------------------------------------------------------------