From 42bec018539527959b3038cf1cdfb11b7e6fca05 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Wed, 3 Jul 2019 11:02:31 -0700 Subject: [PATCH 1/5] Use afterCompile hook for cleanOnceBeforeBuildPatterns Why: works better with other plugins so plugin order shouldn't matter --- src/clean-webpack-plugin.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/clean-webpack-plugin.ts b/src/clean-webpack-plugin.ts index 8ad2483..f3f578e 100644 --- a/src/clean-webpack-plugin.ts +++ b/src/clean-webpack-plugin.ts @@ -185,11 +185,14 @@ class CleanWebpackPlugin { if (this.cleanOnceBeforeBuildPatterns.length !== 0) { if (hooks) { - hooks.emit.tap('clean-webpack-plugin', (compilation) => { - this.handleInitial(compilation); - }); + hooks.afterCompile.tap( + 'clean-webpack-plugin', + (compilation) => { + this.handleInitial(compilation); + }, + ); } else { - compiler.plugin('emit', (compilation, callback) => { + compiler.plugin('after-compile', (compilation, callback) => { try { this.handleInitial(compilation); From e6cd500f54f7d494855bac4a73d52fe9e85aebd2 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Tue, 15 Oct 2019 10:19:54 -0700 Subject: [PATCH 2/5] Remove files async when possible --- src/clean-webpack-plugin.ts | 155 ++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 60 deletions(-) diff --git a/src/clean-webpack-plugin.ts b/src/clean-webpack-plugin.ts index f3f578e..19161c7 100644 --- a/src/clean-webpack-plugin.ts +++ b/src/clean-webpack-plugin.ts @@ -1,5 +1,5 @@ import path from 'path'; -import { sync as delSync } from 'del'; +import del from 'del'; import { Compiler, Stats, compilation as compilationType } from 'webpack'; type Compilation = compilationType.Compilation; @@ -85,6 +85,7 @@ class CleanWebpackPlugin { private currentAssets: string[]; private initialClean: boolean; private outputPath: string; + private useHooks: boolean | null; constructor(options: Options = {}) { if (isPlainObject(options) === false) { @@ -157,6 +158,7 @@ class CleanWebpackPlugin { this.initialClean = false; this.outputPath = ''; + this.useHooks = null; this.apply = this.apply.bind(this); this.handleInitial = this.handleInitial.bind(this); @@ -182,31 +184,33 @@ class CleanWebpackPlugin { * Check for hooks in-order to support old plugin system */ const hooks = compiler.hooks; + this.useHooks = hooks !== undefined; if (this.cleanOnceBeforeBuildPatterns.length !== 0) { - if (hooks) { - hooks.afterCompile.tap( + if (this.useHooks === true) { + hooks.afterCompile.tapPromise( 'clean-webpack-plugin', - (compilation) => { - this.handleInitial(compilation); - }, + this.handleInitial, ); } else { - compiler.plugin('after-compile', (compilation, callback) => { - try { - this.handleInitial(compilation); - - callback(); - } catch (error) { - callback(error); - } - }); + compiler.plugin( + 'after-compile', + async (compilation, callback) => { + try { + await this.handleInitial(compilation); + + callback(); + } catch (error) { + callback(error); + } + }, + ); } } - if (hooks) { - hooks.done.tap('clean-webpack-plugin', (stats) => { - this.handleDone(stats); + if (this.useHooks === true) { + hooks.done.tapPromise('clean-webpack-plugin', async (stats) => { + await this.handleDone(stats); }); } else { compiler.plugin('done', (stats) => { @@ -222,7 +226,7 @@ class CleanWebpackPlugin { * * Warning: It is recommended to initially clean your build directory outside of webpack to minimize unexpected behavior. */ - handleInitial(compilation: Compilation) { + async handleInitial(compilation: Compilation) { if (this.initialClean) { return; } @@ -239,7 +243,10 @@ class CleanWebpackPlugin { this.initialClean = true; - this.removeFiles(this.cleanOnceBeforeBuildPatterns); + await this.removeFiles({ + patterns: this.cleanOnceBeforeBuildPatterns, + sync: false, + }); } handleDone(stats: Stats) { @@ -304,55 +311,83 @@ class CleanWebpackPlugin { } if (removePatterns.length !== 0) { - this.removeFiles(removePatterns); + // eslint-disable-next-line consistent-return + return this.removeFiles({ + patterns: removePatterns, + sync: this.useHooks === false, + }); } } - removeFiles(patterns: string[]) { - try { - const deleted = delSync(patterns, { - force: this.dangerouslyAllowCleanPatternsOutsideProject, - // Change context to build directory - cwd: this.outputPath, - dryRun: this.dry, - dot: true, - ignore: this.protectWebpackAssets ? this.currentAssets : [], - }); + // eslint-disable-next-line class-methods-use-this + handleDelError(error: Error) { + const needsForce = /Cannot delete files\/folders outside the current working directory\./.test( + error.message, + ); + + if (needsForce) { + const message = + 'clean-webpack-plugin: Cannot delete files/folders outside the current working directory. Can be overridden with the `dangerouslyAllowCleanPatternsOutsideProject` option.'; + + throw new Error(message); + } + + throw error; + } + + logResult(deleted: string[]): void { + if (this.verbose === false) { + return; + } + + /** + * Log if verbose is enabled + */ + deleted.forEach((file) => { + const filename = path.relative(process.cwd(), file); + + const message = this.dry ? 'dry' : 'removed'; /** - * Log if verbose is enabled + * Use console.warn over .log + * https://github.com/webpack/webpack/issues/1904 + * https://github.com/johnagan/clean-webpack-plugin/issues/11 */ - if (this.verbose) { - deleted.forEach((file) => { - const filename = path.relative(process.cwd(), file); - - const message = this.dry ? 'dry' : 'removed'; - - /** - * Use console.warn over .log - * https://github.com/webpack/webpack/issues/1904 - * https://github.com/johnagan/clean-webpack-plugin/issues/11 - */ - // eslint-disable-next-line no-console - console.warn( - `clean-webpack-plugin: ${message} ${filename}`, - ); - }); - } - } catch (error) { - const needsForce = /Cannot delete files\/folders outside the current working directory\./.test( - error.message, - ); - - if (needsForce) { - const message = - 'clean-webpack-plugin: Cannot delete files/folders outside the current working directory. Can be overridden with the `dangerouslyAllowCleanPatternsOutsideProject` option.'; + // eslint-disable-next-line no-console + console.warn(`clean-webpack-plugin: ${message} ${filename}`); + }); + } - throw new Error(message); + removeFiles({ patterns, sync }: { patterns: string[]; sync: boolean }) { + const delOptions = { + force: this.dangerouslyAllowCleanPatternsOutsideProject, + // Change context to build directory + cwd: this.outputPath, + dryRun: this.dry, + dot: true, + ignore: this.protectWebpackAssets ? this.currentAssets : [], + }; + + // webpack v3 done plugin hook cannot be async. Use sync version until webpack v3 support is dropped. + if (sync === true) { + try { + const deleted = del.sync(patterns, delOptions); + this.logResult(deleted); + } catch (error) { + this.handleDelError(error); } - - throw error; } + + return ( + del(patterns, delOptions) + // eslint-disable-next-line promise/always-return + .then((deleted: string[]): void => { + this.logResult(deleted); + }) + .catch((error) => { + this.handleDelError(error); + }) + ); } } From dac8ff59a49707cb00f8c2fcc1b12e5bc1084124 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Tue, 15 Oct 2019 14:50:11 -0700 Subject: [PATCH 3/5] Move @types/webpack to devDependencies --- README.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0eef71c..14547eb 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ By default, this plugin will remove all files inside webpack's `output.path` dir `npm install --save-dev clean-webpack-plugin` +If you are using [Typescript](https://www.typescriptlang.org/), you might need to install `@types/webpack` and/or `@types/webpack-env` to your `devDependencies`. + ## Usage ```js diff --git a/package.json b/package.json index 91c7887..7529f93 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/jest": "^24.0.13", "@types/node": "^12.0.2", "@types/read-pkg-up": "^3.0.1", + "@types/webpack": "^4.39.3", "babel-jest": "^24.8.0", "codecov": "^3.5.0", "cross-env": "^5.2.0", @@ -77,7 +78,6 @@ "webpack": "^4.32.0" }, "dependencies": { - "@types/webpack": "^4.4.31", "del": "^4.1.1" } } From 59e626d12c59b74397fa9f8c20cef5cc307cb2b7 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Tue, 15 Oct 2019 14:58:06 -0700 Subject: [PATCH 4/5] Upgrade del to v5 --- package.json | 3 ++- src/clean-webpack-plugin.test.ts | 41 +++++++++++++++++++++----------- src/clean-webpack-plugin.ts | 9 +++---- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 7529f93..1260d32 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "webpack": "^4.32.0" }, "dependencies": { - "del": "^4.1.1" + "del": "^5.1.0", + "slash": "^3.0.0" } } diff --git a/src/clean-webpack-plugin.test.ts b/src/clean-webpack-plugin.test.ts index 97b164f..e9d5184 100644 --- a/src/clean-webpack-plugin.test.ts +++ b/src/clean-webpack-plugin.test.ts @@ -730,7 +730,7 @@ describe('cleanOnceBeforeBuildPatterns option', () => { ]); }); - test('handles the cleanOnceBeforeBuildPatterns outside of build directory', async () => { + test('cannot remove cleanOnceBeforeBuildPatterns outside of build directory without force', async () => { createSrcBundle(1); const outsideDistPath = 'build'; @@ -755,9 +755,13 @@ describe('cleanOnceBeforeBuildPatterns option', () => { plugins: [cleanWebpackPlugin], }); - await compiler.run(); + await expect(compiler.run()).rejects.toThrowErrorMatchingInlineSnapshot( + `"clean-webpack-plugin: Cannot delete files/directories outside webpack's output.path. Can be overridden with the \\"dangerouslyAllowCleanPatternsOutsideProject\\" option."`, + ); - expect(sandbox.getFileListSync(outsideDistPath)).toEqual([]); + expect(sandbox.getFileListSync(outsideDistPath)).toEqual([ + 'outside-file.js', + ]); }); }); @@ -903,7 +907,7 @@ describe('cleanAfterEveryBuildPatterns option', () => { ]); }); - test('handles the cleanAfterEveryBuildPatterns outside of webpack output directory', async () => { + test('cannot remove cleanAfterEveryBuildPatterns outside of webpack output directory without force', async () => { createSrcBundle(1); const outsideDistPath = 'build'; @@ -928,9 +932,18 @@ describe('cleanAfterEveryBuildPatterns option', () => { plugins: [cleanWebpackPlugin], }); - await compiler.run(); - - expect(sandbox.getFileListSync(outsideDistPath)).toEqual([]); + // only tests webpack 4+ because webpack 3 does not throw errors correctly in done plugin cycle + if (cleanWebpackPlugin.useHooks === true) { + await expect( + compiler.run(), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"clean-webpack-plugin: Cannot delete files/directories outside webpack's output.path. Can be overridden with the \\"dangerouslyAllowCleanPatternsOutsideProject\\" option."`, + ); + } + + expect(sandbox.getFileListSync(outsideDistPath)).toEqual([ + 'outside-file.js', + ]); }); }); @@ -963,7 +976,7 @@ describe('dangerouslyAllowCleanPatternsOutsideProject option', () => { }); await expect(compiler.run()).rejects.toThrowErrorMatchingInlineSnapshot( - `"clean-webpack-plugin: Cannot delete files/folders outside the current working directory. Can be overridden with the \`dangerouslyAllowCleanPatternsOutsideProject\` option."`, + `"clean-webpack-plugin: Cannot delete files/directories outside webpack's output.path. Can be overridden with the \\"dangerouslyAllowCleanPatternsOutsideProject\\" option."`, ); }); @@ -1010,12 +1023,12 @@ describe('dangerouslyAllowCleanPatternsOutsideProject option', () => { expect(cleanWebpackPlugin.dry).toEqual(true); expect(cleanWebpackPlugin.verbose).toEqual(true); expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "clean-webpack-plugin: dangerouslyAllowCleanPatternsOutsideProject requires dry: false to be explicitly set. Enabling dry mode", - ], -] -`); + Array [ + Array [ + "clean-webpack-plugin: dangerouslyAllowCleanPatternsOutsideProject requires dry: false to be explicitly set. Enabling dry mode", + ], + ] + `); }); test('dangerouslyAllowCleanPatternsOutsideProject: true dry: true', async () => { diff --git a/src/clean-webpack-plugin.ts b/src/clean-webpack-plugin.ts index 19161c7..2db1e3d 100644 --- a/src/clean-webpack-plugin.ts +++ b/src/clean-webpack-plugin.ts @@ -1,5 +1,6 @@ import path from 'path'; import del from 'del'; +import slash from 'slash'; import { Compiler, Stats, compilation as compilationType } from 'webpack'; type Compilation = compilationType.Compilation; @@ -275,7 +276,8 @@ class CleanWebpackPlugin { true, ).assets || []; const assetList = assets.map((asset: { name: string }) => { - return asset.name; + // enforce forward slashes because del's pattern matching works better with them + return slash(asset.name); }); /** @@ -321,13 +323,12 @@ class CleanWebpackPlugin { // eslint-disable-next-line class-methods-use-this handleDelError(error: Error) { - const needsForce = /Cannot delete files\/folders outside the current working directory\./.test( + const needsForce = /Cannot delete files\/directories outside the current working directory\./.test( error.message, ); if (needsForce) { - const message = - 'clean-webpack-plugin: Cannot delete files/folders outside the current working directory. Can be overridden with the `dangerouslyAllowCleanPatternsOutsideProject` option.'; + const message = `clean-webpack-plugin: Cannot delete files/directories outside webpack's output.path. Can be overridden with the "dangerouslyAllowCleanPatternsOutsideProject" option.`; throw new Error(message); } From 1717566a516bf459db1455e92aa73dc59526556a Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Tue, 15 Oct 2019 16:07:02 -0700 Subject: [PATCH 5/5] Update devDependencies and fix lint / typescript issues --- .eslintrc.js | 12 + dev-utils/get-webpack-version.js | 7 +- dev-utils/get-webpack-version.test.js | 8 +- dev-utils/node-version.js | 3 +- dev-utils/node-version.test.js | 8 +- dev-utils/test-supported-webpack-versions.js | 40 ++-- package.json | 48 ++-- src/clean-webpack-plugin.test.ts | 218 ++++++++++--------- src/clean-webpack-plugin.ts | 88 ++++---- 9 files changed, 230 insertions(+), 202 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 01f31be..178bafc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,19 @@ const eslint = { extends: '@chrisblossom/eslint-config', + rules: { + 'import/no-extraneous-dependencies': 'off', + }, overrides: [ + { + files: ['*.ts', '*.tsx', '.*.ts', '.*.tsx'], + rules: { + 'promise/prefer-await-to-then': 'off', + 'promise/always-return': 'off', + '@typescript-eslint/promise-function-async': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + }, + }, { files: ['dev-utils/**/*.js', 'dev-utils/**/.*.js'], parserOptions: { diff --git a/dev-utils/get-webpack-version.js b/dev-utils/get-webpack-version.js index cc6ff6a..4e8efd9 100644 --- a/dev-utils/get-webpack-version.js +++ b/dev-utils/get-webpack-version.js @@ -9,10 +9,11 @@ function getWebpackVersion() { const webpackPath = require.resolve('webpack'); const { dir } = path.parse(webpackPath); - const webpackPackageJson = readPkgUp.sync({ cwd: dir, normalize: false }); + const webpackPackageJson = + readPkgUp.sync({ cwd: dir, normalize: false }) || {}; - const version = webpackPackageJson.package.version - ? webpackPackageJson.package.version + const version = webpackPackageJson.packageJson.version + ? webpackPackageJson.packageJson.version : null; return version; diff --git a/dev-utils/get-webpack-version.test.js b/dev-utils/get-webpack-version.test.js index 7ab294e..ad415ac 100644 --- a/dev-utils/get-webpack-version.test.js +++ b/dev-utils/get-webpack-version.test.js @@ -5,7 +5,7 @@ const getWebpackVersionTest = () => require('./get-webpack-version')(); describe('webpackVersion', () => { test('returns major only and is type number', () => { jest.doMock('read-pkg-up', () => ({ - sync: () => ({ package: { version: '4.29.0' } }), + sync: () => ({ packageJson: { version: '4.29.0' } }), })); const version = getWebpackVersionTest(); @@ -14,7 +14,7 @@ describe('webpackVersion', () => { test('handles alpha', () => { jest.doMock('read-pkg-up', () => ({ - sync: () => ({ package: { version: '5.0.0-alpha.8' } }), + sync: () => ({ packageJson: { version: '5.0.0-alpha.8' } }), })); const version = getWebpackVersionTest(); @@ -22,7 +22,9 @@ describe('webpackVersion', () => { }); test('returns null if no version found', () => { - jest.doMock('read-pkg-up', () => ({ sync: () => ({ package: {} }) })); + jest.doMock('read-pkg-up', () => ({ + sync: () => ({ packageJson: {} }), + })); const version = getWebpackVersionTest(); expect(version).toEqual(null); diff --git a/dev-utils/node-version.js b/dev-utils/node-version.js index f22805e..20206c2 100644 --- a/dev-utils/node-version.js +++ b/dev-utils/node-version.js @@ -5,7 +5,8 @@ const semver = require('semver'); function getNodeVersion() { const packageJson = - readPkgUp.sync({ cwd: process.cwd(), normalize: false }).package || {}; + readPkgUp.sync({ cwd: process.cwd(), normalize: false }).packageJson || + {}; const engines = packageJson.engines || {}; const node = engines.node || '8.9.0'; diff --git a/dev-utils/node-version.test.js b/dev-utils/node-version.test.js index 097d248..cd9edf0 100644 --- a/dev-utils/node-version.test.js +++ b/dev-utils/node-version.test.js @@ -11,7 +11,7 @@ test('handles undefined pkg', () => { }); test('handles undefined engines', () => { - jest.doMock('read-pkg-up', () => ({ sync: () => ({ package: {} }) })); + jest.doMock('read-pkg-up', () => ({ sync: () => ({ packageJson: {} }) })); const nodeVersion = require('./node-version'); @@ -20,7 +20,7 @@ test('handles undefined engines', () => { test('handles undefined node', () => { jest.doMock('read-pkg-up', () => ({ - sync: () => ({ package: { engines: { npm: '^5.0.0' } } }), + sync: () => ({ packageJson: { engines: { npm: '^5.0.0' } } }), })); const nodeVersion = require('./node-version'); @@ -30,7 +30,7 @@ test('handles undefined node', () => { test('handles non-digit characters', () => { jest.doMock('read-pkg-up', () => ({ - sync: () => ({ package: { engines: { node: '>=10.0.0' } } }), + sync: () => ({ packageJson: { engines: { node: '>=10.0.0' } } }), })); const nodeVersion = require('./node-version'); @@ -40,7 +40,7 @@ test('handles non-digit characters', () => { test('handles empty node', () => { jest.doMock('read-pkg-up', () => ({ - sync: () => ({ package: { engines: { node: '' } } }), + sync: () => ({ packageJson: { engines: { node: '' } } }), })); const nodeVersion = require('./node-version'); diff --git a/dev-utils/test-supported-webpack-versions.js b/dev-utils/test-supported-webpack-versions.js index 4999d2f..00a675b 100755 --- a/dev-utils/test-supported-webpack-versions.js +++ b/dev-utils/test-supported-webpack-versions.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -/* eslint-disable arrow-body-style,no-param-reassign,promise/always-return */ +/* eslint-disable no-param-reassign,no-console */ 'use strict'; @@ -40,10 +40,10 @@ const webpackTestTasks = supported.map((version) => { const npmInstallTaskTitle = `npm ${npmCommandArgs.join(' ')}`; const npmInstallTask = { title: npmInstallTaskTitle, - task: (ctx, task) => - execa('npm', npmCommandArgs).then(() => { - task.title = `${npmInstallTaskTitle} (${getWebpackVersion()})`; - }), + task: async (ctx, task) => { + await execa('npm', npmCommandArgs); + task.title = `${npmInstallTaskTitle} (${getWebpackVersion()})`; + }, }; const jestCommandArgs = @@ -69,13 +69,13 @@ const webpackTestTasks = supported.map((version) => { if (ciEnabled === true) { const codecovTask = { title: 'codecov', - task: () => - execa('codecov', [], { + task: async () => { + const { stdout } = await execa('codecov', [], { env: { FORCE_COLOR: true }, - }).then(({ stdout }) => { - // eslint-disable-next-line no-console - console.log(stdout); - }), + }); + + console.log(stdout); + }, }; testWebpackVersionTask.push(codecovTask); @@ -83,7 +83,9 @@ const webpackTestTasks = supported.map((version) => { return { title: `webpack@${version}`, - task: () => new Listr(testWebpackVersionTask), + task: () => { + return new Listr(testWebpackVersionTask); + }, skip, }; }); @@ -104,14 +106,14 @@ tasks const packageJsonWebpackVersion = readPkgUp.sync({ cwd: process.cwd(), normalize: false, - }).package.devDependencies.webpack; + }).packageJson.devDependencies.webpack; return new Listr( [ { title: `npm install --no-save webpack@${packageJsonWebpackVersion}`, - task: () => - execa( + task: () => { + return execa( 'npm', [ 'install', @@ -119,15 +121,17 @@ tasks `webpack@${packageJsonWebpackVersion}`, ], { env: { FORCE_COLOR: true } }, - ), - skip: () => ciEnabled === true, + ); + }, + skip: () => { + return ciEnabled === true; + }, }, ], listrOptions, ).run(); }) .catch((error) => { - // eslint-disable-next-line no-console console.error(error.message); // eslint-disable-next-line no-process-exit diff --git a/package.json b/package.json index 1260d32..96a983f 100644 --- a/package.json +++ b/package.json @@ -46,36 +46,32 @@ "prepublishOnly": "npm run build && npm run lint && npm run typescript && npm run test.all", "release": "np" }, - "peerDependencies": { - "webpack": "*" - }, "devDependencies": { - "@babel/cli": "^7.4.4", - "@babel/core": "^7.4.4", - "@babel/preset-env": "^7.4.4", - "@babel/preset-typescript": "^7.3.3", - "@chrisblossom/eslint-config": "^5.0.0", - "@types/jest": "^24.0.13", - "@types/node": "^12.0.2", - "@types/read-pkg-up": "^3.0.1", + "@babel/cli": "^7.6.4", + "@babel/core": "^7.6.4", + "@babel/preset-env": "^7.6.3", + "@babel/preset-typescript": "^7.6.0", + "@chrisblossom/eslint-config": "^6.1.5", + "@types/jest": "^24.0.19", + "@types/node": "^12.11.1", "@types/webpack": "^4.39.3", - "babel-jest": "^24.8.0", - "codecov": "^3.5.0", - "cross-env": "^5.2.0", - "del-cli": "^1.1.0", + "babel-jest": "^24.9.0", + "codecov": "^3.6.1", + "cross-env": "^6.0.3", + "del-cli": "^3.0.0", "eslint": "^5.16.0", - "execa": "^1.0.0", - "husky": "^2.3.0", - "jest": "^24.8.0", - "lint-staged": "^8.1.7", + "execa": "^3.1.0", + "husky": "^3.0.9", + "jest": "^24.9.0", + "lint-staged": "^9.4.2", "listr": "^0.14.3", - "np": "^5.0.2", - "prettier": "^1.17.1", - "read-pkg-up": "^6.0.0", - "semver": "^6.0.0", - "temp-sandbox": "^3.0.0", - "typescript": "^3.4.5", - "webpack": "^4.32.0" + "np": "^5.1.1", + "prettier": "^1.18.2", + "read-pkg-up": "^7.0.0", + "semver": "^6.3.0", + "temp-sandbox": "^4.0.1", + "typescript": "^3.6.4", + "webpack": "^4.41.2" }, "dependencies": { "del": "^5.1.0", diff --git a/src/clean-webpack-plugin.test.ts b/src/clean-webpack-plugin.test.ts index e9d5184..4cdc487 100644 --- a/src/clean-webpack-plugin.test.ts +++ b/src/clean-webpack-plugin.test.ts @@ -29,9 +29,10 @@ function webpack(options: Configuration = {}) { const compiler = webpackActual(options); - const runAsync = () => - new Promise((resolve, reject) => { + const runAsync = async () => { + return new Promise((resolve, reject) => { compiler.run((error: Error, stats: Stats) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (error || stats.hasErrors()) { reject(error); @@ -41,6 +42,7 @@ function webpack(options: Configuration = {}) { resolve(stats); }); }); + }; return { ...compiler, run: runAsync }; } @@ -95,6 +97,7 @@ function createStaticFiles() { let consoleSpy: any; const cwd = process.cwd(); + beforeEach(() => { process.chdir(sandbox.dir); @@ -179,7 +182,7 @@ test('removes initial files by default', async () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); - expect(sandbox.getFileListSync(outputPathFull)).toEqual(['bundle.js']); + expect(sandbox.getFileListSync(outputPathFull)).toEqual(['dist/bundle.js']); }); test('removes nested files', async () => { @@ -216,8 +219,8 @@ test('removes nested files', async () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - 'js/bundle.js', - 'js/chunks/1.bundle.js', + 'dist/js/bundle.js', + 'dist/js/chunks/1.bundle.js', ]); }); @@ -249,10 +252,10 @@ test('removes map files', async () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - '1.bundle.js.map', - 'bundle.js', - 'bundle.js.map', + 'dist/1.bundle.js', + 'dist/1.bundle.js.map', + 'dist/bundle.js', + 'dist/bundle.js.map', ]); createSrcBundle(1); @@ -265,8 +268,8 @@ test('removes map files', async () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - 'bundle.js', - 'bundle.js.map', + 'dist/bundle.js', + 'dist/bundle.js.map', ]); }); @@ -305,8 +308,8 @@ describe('cleanStaleWebpackAssets option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); }); @@ -343,7 +346,9 @@ describe('cleanStaleWebpackAssets option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); - expect(sandbox.getFileListSync(outputPathFull)).toEqual(['bundle.js']); + expect(sandbox.getFileListSync(outputPathFull)).toEqual([ + 'dist/bundle.js', + ]); }); test('removes assets by default', async () => { @@ -377,7 +382,9 @@ describe('cleanStaleWebpackAssets option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); - expect(sandbox.getFileListSync(outputPathFull)).toEqual(['bundle.js']); + expect(sandbox.getFileListSync(outputPathFull)).toEqual([ + 'dist/bundle.js', + ]); }); }); @@ -411,8 +418,8 @@ describe('protectWebpackAssets option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); createSrcBundle(1); @@ -423,10 +430,10 @@ describe('protectWebpackAssets option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -458,8 +465,8 @@ describe('protectWebpackAssets option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); createSrcBundle(1); @@ -470,10 +477,10 @@ describe('protectWebpackAssets option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -506,7 +513,7 @@ describe('protectWebpackAssets option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', + 'dist/1.bundle.js', ]); createSrcBundle(1); @@ -517,9 +524,9 @@ describe('protectWebpackAssets option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/static1.js', + 'dist/static2.txt', ]); }); }); @@ -556,10 +563,10 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); createSrcBundle(2); @@ -572,11 +579,11 @@ describe('cleanOnceBeforeBuildPatterns option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - '1.bundle.js', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/1.bundle.js', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); expect(removeFilesSpy).not.toHaveBeenCalled(); @@ -588,9 +595,9 @@ describe('cleanOnceBeforeBuildPatterns option', () => { const initialBuildFiles = sandbox.getFileListSync(outputPathFull); expect(initialBuildFiles).toEqual([ - '.hidden.file', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/static1.js', + 'dist/static2.txt', ]); const cleanWebpackPlugin = new CleanWebpackPlugin({ @@ -613,7 +620,9 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); - expect(sandbox.getFileListSync(outputPathFull)).toEqual(['bundle.js']); + expect(sandbox.getFileListSync(outputPathFull)).toEqual([ + 'dist/bundle.js', + ]); createStaticFiles(); @@ -622,10 +631,10 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -635,9 +644,9 @@ describe('cleanOnceBeforeBuildPatterns option', () => { const initialBuildFiles = sandbox.getFileListSync(outputPathFull); expect(initialBuildFiles).toEqual([ - '.hidden.file', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/static1.js', + 'dist/static2.txt', ]); const cleanWebpackPlugin = new CleanWebpackPlugin({ @@ -661,9 +670,9 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', ]); createStaticFiles(); @@ -673,10 +682,10 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -686,9 +695,9 @@ describe('cleanOnceBeforeBuildPatterns option', () => { const initialBuildFiles = sandbox.getFileListSync(outputPathFull); expect(initialBuildFiles).toEqual([ - '.hidden.file', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/static1.js', + 'dist/static2.txt', ]); const cleanWebpackPlugin = new CleanWebpackPlugin({ @@ -712,8 +721,8 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - 'bundle.js', - 'static2.txt', + 'dist/bundle.js', + 'dist/static2.txt', ]); createStaticFiles(); @@ -723,10 +732,10 @@ describe('cleanOnceBeforeBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -737,7 +746,7 @@ describe('cleanOnceBeforeBuildPatterns option', () => { sandbox.createFileSync('build/outside-file.js', '// outside-file.js'); const initialOutsideFiles = sandbox.getFileListSync(outsideDistPath); - expect(initialOutsideFiles).toEqual(['outside-file.js']); + expect(initialOutsideFiles).toEqual(['build/outside-file.js']); const cleanWebpackPlugin = new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [ @@ -760,7 +769,7 @@ describe('cleanOnceBeforeBuildPatterns option', () => { ); expect(sandbox.getFileListSync(outsideDistPath)).toEqual([ - 'outside-file.js', + 'build/outside-file.js', ]); }); }); @@ -794,8 +803,8 @@ describe('cleanAfterEveryBuildPatterns option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); createSrcBundle(1); @@ -806,9 +815,9 @@ describe('cleanAfterEveryBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', ]); }); @@ -840,8 +849,8 @@ describe('cleanAfterEveryBuildPatterns option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); createSrcBundle(1); @@ -852,10 +861,10 @@ describe('cleanAfterEveryBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -887,8 +896,8 @@ describe('cleanAfterEveryBuildPatterns option', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); createSrcBundle(1); @@ -899,11 +908,11 @@ describe('cleanAfterEveryBuildPatterns option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '.hidden.file', - '1.bundle.js', - 'bundle.js', - 'static1.js', - 'static2.txt', + 'dist/.hidden.file', + 'dist/1.bundle.js', + 'dist/bundle.js', + 'dist/static1.js', + 'dist/static2.txt', ]); }); @@ -914,7 +923,7 @@ describe('cleanAfterEveryBuildPatterns option', () => { sandbox.createFileSync('build/outside-file.js', '// outside-file.js'); const initialOutsideFiles = sandbox.getFileListSync(outsideDistPath); - expect(initialOutsideFiles).toEqual(['outside-file.js']); + expect(initialOutsideFiles).toEqual(['build/outside-file.js']); const cleanWebpackPlugin = new CleanWebpackPlugin({ cleanAfterEveryBuildPatterns: [ @@ -933,6 +942,7 @@ describe('cleanAfterEveryBuildPatterns option', () => { }); // only tests webpack 4+ because webpack 3 does not throw errors correctly in done plugin cycle + // eslint-disable-next-line jest/no-if if (cleanWebpackPlugin.useHooks === true) { await expect( compiler.run(), @@ -942,7 +952,7 @@ describe('cleanAfterEveryBuildPatterns option', () => { } expect(sandbox.getFileListSync(outsideDistPath)).toEqual([ - 'outside-file.js', + 'build/outside-file.js', ]); }); }); @@ -956,7 +966,7 @@ describe('dangerouslyAllowCleanPatternsOutsideProject option', () => { sandbox.createFileSync('build/outside-file.js', '// outside-file.js'); const initialOutsideFiles = sandbox.getFileListSync(outsideDistPath); - expect(initialOutsideFiles).toEqual(['outside-file.js']); + expect(initialOutsideFiles).toEqual(['build/outside-file.js']); const cleanWebpackPlugin = new CleanWebpackPlugin({ // Use cleanOnceBeforeBuildPatterns because webpack 2/3 doesn't handle errors in done lifecycle correctly @@ -988,7 +998,7 @@ describe('dangerouslyAllowCleanPatternsOutsideProject option', () => { sandbox.createFileSync('build/outside-file.js', '// outside-file.js'); const initialOutsideFiles = sandbox.getFileListSync(outsideDistPath); - expect(initialOutsideFiles).toEqual(['outside-file.js']); + expect(initialOutsideFiles).toEqual(['build/outside-file.js']); const cleanWebpackPlugin = new CleanWebpackPlugin({ dangerouslyAllowCleanPatternsOutsideProject: true, @@ -1015,7 +1025,7 @@ describe('dangerouslyAllowCleanPatternsOutsideProject option', () => { expect(sandbox.getFileListSync(outsideDistPath)).toEqual([]); }); - test('dangerouslyAllowCleanPatternsOutsideProject: true require dry to be explicitly set', async () => { + test('dangerouslyAllowCleanPatternsOutsideProject: true require dry to be explicitly set', () => { const cleanWebpackPlugin = new CleanWebpackPlugin({ dangerouslyAllowCleanPatternsOutsideProject: true, }); @@ -1031,7 +1041,7 @@ describe('dangerouslyAllowCleanPatternsOutsideProject option', () => { `); }); - test('dangerouslyAllowCleanPatternsOutsideProject: true dry: true', async () => { + test('dangerouslyAllowCleanPatternsOutsideProject: true dry: true', () => { const cleanWebpackPlugin = new CleanWebpackPlugin({ dangerouslyAllowCleanPatternsOutsideProject: true, dry: true, @@ -1080,8 +1090,8 @@ describe('dry option', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['bundle.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); expect(consoleSpy).toHaveBeenCalledWith( @@ -1184,8 +1194,8 @@ describe('webpack errors', () => { }); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); expect(consoleSpy.mock.calls).toEqual([]); @@ -1201,8 +1211,8 @@ describe('webpack errors', () => { } catch (error) {} expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); expect(cleanWebpackPlugin.currentAssets).toEqual([]); @@ -1239,8 +1249,8 @@ describe('webpack errors', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); expect(consoleSpy.mock.calls).toEqual([]); @@ -1265,8 +1275,8 @@ describe('webpack errors', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); }); @@ -1326,7 +1336,7 @@ describe('webpack >= 4 only', () => { expect(cleanWebpackPlugin.currentAssets).toEqual(['main.js']); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - 'main.js', + 'dist/main.js', ]); }); } diff --git a/src/clean-webpack-plugin.ts b/src/clean-webpack-plugin.ts index 2db1e3d..dd76928 100644 --- a/src/clean-webpack-plugin.ts +++ b/src/clean-webpack-plugin.ts @@ -88,14 +88,14 @@ class CleanWebpackPlugin { private outputPath: string; private useHooks: boolean | null; - constructor(options: Options = {}) { + public constructor(options: Options = {}) { if (isPlainObject(options) === false) { throw new Error(`clean-webpack-plugin only accepts an options object. See: https://github.com/johnagan/clean-webpack-plugin#options-and-defaults-optional`); } // @ts-ignore - if (options.allowExternal) { + if (options.allowExternal !== undefined) { throw new Error( 'clean-webpack-plugin: `allowExternal` option no longer supported. Use `dangerouslyAllowCleanPatternsOutsideProject`', ); @@ -167,8 +167,11 @@ class CleanWebpackPlugin { this.removeFiles = this.removeFiles.bind(this); } - apply(compiler: Compiler) { - if (!compiler.options.output || !compiler.options.output.path) { + public apply(compiler: Compiler): void { + if ( + compiler.options.output === undefined || + compiler.options.output.path === undefined + ) { // eslint-disable-next-line no-console console.warn( 'clean-webpack-plugin: options.output.path not defined. Plugin disabled...', @@ -191,21 +194,22 @@ class CleanWebpackPlugin { if (this.useHooks === true) { hooks.afterCompile.tapPromise( 'clean-webpack-plugin', - this.handleInitial, - ); - } else { - compiler.plugin( - 'after-compile', - async (compilation, callback) => { - try { - await this.handleInitial(compilation); - - callback(); - } catch (error) { - callback(error); - } + async (compilation) => { + await this.handleInitial(compilation); }, ); + } else { + /* eslint-disable @typescript-eslint/no-floating-promises,promise/prefer-await-to-callbacks */ + compiler.plugin('after-compile', (compilation, callback) => { + try { + this.handleInitial(compilation); + + callback(); + } catch (error) { + callback(error); + } + }); + /* eslint-enable @typescript-eslint/no-floating-promises,promise/prefer-await-to-callbacks */ } } @@ -215,6 +219,7 @@ class CleanWebpackPlugin { }); } else { compiler.plugin('done', (stats) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.handleDone(stats); }); } @@ -227,7 +232,7 @@ class CleanWebpackPlugin { * * Warning: It is recommended to initially clean your build directory outside of webpack to minimize unexpected behavior. */ - async handleInitial(compilation: Compilation) { + private handleInitial(compilation: Compilation) { if (this.initialClean) { return; } @@ -244,13 +249,13 @@ class CleanWebpackPlugin { this.initialClean = true; - await this.removeFiles({ + // eslint-disable-next-line consistent-return + return this.removeFiles({ patterns: this.cleanOnceBeforeBuildPatterns, - sync: false, }); } - handleDone(stats: Stats) { + private handleDone(stats: Stats) { /** * Do nothing if there is a webpack error */ @@ -269,6 +274,7 @@ class CleanWebpackPlugin { * Fetch Webpack's output asset files */ const assets = + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions stats.toJson( { assets: true, @@ -294,7 +300,9 @@ class CleanWebpackPlugin { /** * Save assets for next compilation */ - this.currentAssets = assetList.sort(); + this.currentAssets = assetList.sort((a, b) => { + return a.localeCompare(b); + }); const removePatterns = []; @@ -314,17 +322,14 @@ class CleanWebpackPlugin { if (removePatterns.length !== 0) { // eslint-disable-next-line consistent-return - return this.removeFiles({ - patterns: removePatterns, - sync: this.useHooks === false, - }); + return this.removeFiles({ patterns: removePatterns }); } } // eslint-disable-next-line class-methods-use-this - handleDelError(error: Error) { - const needsForce = /Cannot delete files\/directories outside the current working directory\./.test( - error.message, + private handleDelError(error: Error) { + const needsForce = error.message.includes( + 'Cannot delete files/directories outside the current working directory.', ); if (needsForce) { @@ -336,7 +341,7 @@ class CleanWebpackPlugin { throw error; } - logResult(deleted: string[]): void { + private logResult(deleted: string[]): void { if (this.verbose === false) { return; } @@ -359,7 +364,7 @@ class CleanWebpackPlugin { }); } - removeFiles({ patterns, sync }: { patterns: string[]; sync: boolean }) { + private removeFiles({ patterns }: { patterns: string[] }) { const delOptions = { force: this.dangerouslyAllowCleanPatternsOutsideProject, // Change context to build directory @@ -369,8 +374,8 @@ class CleanWebpackPlugin { ignore: this.protectWebpackAssets ? this.currentAssets : [], }; - // webpack v3 done plugin hook cannot be async. Use sync version until webpack v3 support is dropped. - if (sync === true) { + // webpack v3 done plugin hook cannot be async. + if (this.useHooks === false) { try { const deleted = del.sync(patterns, delOptions); this.logResult(deleted); @@ -379,16 +384,13 @@ class CleanWebpackPlugin { } } - return ( - del(patterns, delOptions) - // eslint-disable-next-line promise/always-return - .then((deleted: string[]): void => { - this.logResult(deleted); - }) - .catch((error) => { - this.handleDelError(error); - }) - ); + return del(patterns, delOptions) + .then((deleted: string[]): void => { + this.logResult(deleted); + }) + .catch((error) => { + this.handleDelError(error); + }); } }