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/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/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 91c7887..96a983f 100644 --- a/package.json +++ b/package.json @@ -46,38 +46,35 @@ "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-jest": "^24.8.0", - "codecov": "^3.5.0", - "cross-env": "^5.2.0", - "del-cli": "^1.1.0", + "@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.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": { - "@types/webpack": "^4.4.31", - "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..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,21 +732,21 @@ 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', ]); }); - 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'; 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: [ @@ -755,9 +764,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([ + 'build/outside-file.js', + ]); }); }); @@ -790,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); @@ -802,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', ]); }); @@ -836,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); @@ -848,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', ]); }); @@ -883,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); @@ -895,22 +908,22 @@ 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', ]); }); - 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'; 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: [ @@ -928,9 +941,19 @@ 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 + // eslint-disable-next-line jest/no-if + 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([ + 'build/outside-file.js', + ]); }); }); @@ -943,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 @@ -963,7 +986,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."`, ); }); @@ -975,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, @@ -1002,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, }); @@ -1010,15 +1033,15 @@ 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 () => { + test('dangerouslyAllowCleanPatternsOutsideProject: true dry: true', () => { const cleanWebpackPlugin = new CleanWebpackPlugin({ dangerouslyAllowCleanPatternsOutsideProject: true, dry: true, @@ -1067,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( @@ -1171,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([]); @@ -1188,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([]); @@ -1226,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([]); @@ -1252,8 +1275,8 @@ describe('webpack errors', () => { ]); expect(sandbox.getFileListSync(outputPathFull)).toEqual([ - '1.bundle.js', - 'bundle.js', + 'dist/1.bundle.js', + 'dist/bundle.js', ]); }); @@ -1313,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 8ad2483..dd76928 100644 --- a/src/clean-webpack-plugin.ts +++ b/src/clean-webpack-plugin.ts @@ -1,5 +1,6 @@ import path from 'path'; -import { sync as delSync } from 'del'; +import del from 'del'; +import slash from 'slash'; import { Compiler, Stats, compilation as compilationType } from 'webpack'; type Compilation = compilationType.Compilation; @@ -85,15 +86,16 @@ class CleanWebpackPlugin { private currentAssets: string[]; private initialClean: boolean; 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`', ); @@ -157,6 +159,7 @@ class CleanWebpackPlugin { this.initialClean = false; this.outputPath = ''; + this.useHooks = null; this.apply = this.apply.bind(this); this.handleInitial = this.handleInitial.bind(this); @@ -164,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...', @@ -182,14 +188,19 @@ 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.emit.tap('clean-webpack-plugin', (compilation) => { - this.handleInitial(compilation); - }); + if (this.useHooks === true) { + hooks.afterCompile.tapPromise( + 'clean-webpack-plugin', + async (compilation) => { + await this.handleInitial(compilation); + }, + ); } else { - compiler.plugin('emit', (compilation, callback) => { + /* eslint-disable @typescript-eslint/no-floating-promises,promise/prefer-await-to-callbacks */ + compiler.plugin('after-compile', (compilation, callback) => { try { this.handleInitial(compilation); @@ -198,15 +209,17 @@ class CleanWebpackPlugin { callback(error); } }); + /* eslint-enable @typescript-eslint/no-floating-promises,promise/prefer-await-to-callbacks */ } } - 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) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.handleDone(stats); }); } @@ -219,7 +232,7 @@ class CleanWebpackPlugin { * * Warning: It is recommended to initially clean your build directory outside of webpack to minimize unexpected behavior. */ - handleInitial(compilation: Compilation) { + private handleInitial(compilation: Compilation) { if (this.initialClean) { return; } @@ -236,10 +249,13 @@ class CleanWebpackPlugin { this.initialClean = true; - this.removeFiles(this.cleanOnceBeforeBuildPatterns); + // eslint-disable-next-line consistent-return + return this.removeFiles({ + patterns: this.cleanOnceBeforeBuildPatterns, + }); } - handleDone(stats: Stats) { + private handleDone(stats: Stats) { /** * Do nothing if there is a webpack error */ @@ -258,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, @@ -265,7 +282,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); }); /** @@ -282,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 = []; @@ -301,55 +321,76 @@ class CleanWebpackPlugin { } if (removePatterns.length !== 0) { - this.removeFiles(removePatterns); + // eslint-disable-next-line consistent-return + return this.removeFiles({ patterns: removePatterns }); } } - 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 + private handleDelError(error: Error) { + const needsForce = error.message.includes( + 'Cannot delete files/directories outside the current working directory.', + ); + + if (needsForce) { + 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); + } + + throw error; + } + + private 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); + private removeFiles({ patterns }: { patterns: string[] }) { + 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. + if (this.useHooks === false) { + try { + const deleted = del.sync(patterns, delOptions); + this.logResult(deleted); + } catch (error) { + this.handleDelError(error); } - - throw error; } + + return del(patterns, delOptions) + .then((deleted: string[]): void => { + this.logResult(deleted); + }) + .catch((error) => { + this.handleDelError(error); + }); } }