├── .eslintignore ├── .prettierignore ├── .prettierrc ├── tests ├── integration │ ├── fixtures │ │ ├── .gitignore │ │ ├── webpack │ │ │ ├── basic │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── cache │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── banner-plugin │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── multi-compiler │ │ │ │ ├── entry.js │ │ │ │ ├── entry2.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── no-config │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── loader-path │ │ │ │ ├── exec.js │ │ │ │ ├── main.js │ │ │ │ ├── webpack.config.js │ │ │ │ └── Gruntfile.js │ │ │ ├── multi-compiler-verbose │ │ │ │ ├── entry.js │ │ │ │ ├── entry2.js │ │ │ │ ├── exec.js │ │ │ │ ├── options.json │ │ │ │ └── Gruntfile.js │ │ │ ├── split-chunks-issue-169 │ │ │ │ ├── uniquerandomfolder │ │ │ │ │ └── file.js │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── watch-not-deprecated │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── error │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ ├── circular-reference-in-plugin-options │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ │ └── error-without-stats │ │ │ │ ├── entry.js │ │ │ │ ├── exec.js │ │ │ │ └── Gruntfile.js │ │ └── webpack-dev-server │ │ │ └── basic │ │ │ ├── entry.js │ │ │ ├── exec.js │ │ │ └── Gruntfile.js │ └── fixtures.test.js └── utils │ ├── loadGruntWebpackTasks.js │ └── assertGrunt.js ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── release.yml │ └── build.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── babel.config.js ├── .gitpod.yml ├── src ├── options │ ├── __mocks__ │ │ └── TestOptionHelper.js │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── OptionHelper.test.js.snap │ │ │ ├── WebpackDevServerOptionHelper.test.js.snap │ │ │ └── default.test.js.snap │ │ ├── WebpackOptionHelper.test.js │ │ ├── WebpackDevServerOptionHelper.test.js │ │ ├── OptionHelper.test.js │ │ └── default.test.js │ ├── WebpackOptionHelper.js │ ├── WebpackDevServerOptionHelper.js │ ├── default.js │ └── OptionHelper.js └── plugins │ └── ProgressPluginFactory.js ├── .editorconfig ├── .eslintrc.json ├── LICENSE ├── package.json ├── tasks ├── webpack-dev-server.js └── webpack.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tests 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tests/integration/fixtures/**/* 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } 4 | -------------------------------------------------------------------------------- /tests/integration/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | output.js 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: webpack 2 | github: [danez] 3 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/basic/entry.js: -------------------------------------------------------------------------------- 1 | console.log(okey); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/cache/entry.js: -------------------------------------------------------------------------------- 1 | console.log(okey); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/banner-plugin/entry.js: -------------------------------------------------------------------------------- 1 | console.log(''); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler/entry.js: -------------------------------------------------------------------------------- 1 | console.log(ok); -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler/entry2.js: -------------------------------------------------------------------------------- 1 | console.log(ok); -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/no-config/entry.js: -------------------------------------------------------------------------------- 1 | console.log(okey); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack-dev-server/basic/entry.js: -------------------------------------------------------------------------------- 1 | console.log(okey); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/loader-path/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler-verbose/entry.js: -------------------------------------------------------------------------------- 1 | console.log(ok); -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler-verbose/entry2.js: -------------------------------------------------------------------------------- 1 | console.log(ok); -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/split-chunks-issue-169/uniquerandomfolder/file.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/watch-not-deprecated/entry.js: -------------------------------------------------------------------------------- 1 | console.log(okey); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/error/entry.js: -------------------------------------------------------------------------------- 1 | var a = require('./b'); 2 | console.log(a); 3 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler-verbose/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | npm-debug.log 5 | tests/integration/tmp 6 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/circular-reference-in-plugin-options/entry.js: -------------------------------------------------------------------------------- 1 | console.log("works"); 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/error-without-stats/entry.js: -------------------------------------------------------------------------------- 1 | var a = require('./b'); 2 | console.log(a); 3 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler-verbose/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "args": ["webpack", "--verbose"] 3 | } -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/split-chunks-issue-169/entry.js: -------------------------------------------------------------------------------- 1 | require("./uniquerandomfolder/file"); 2 | console.log(okey); 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | presets: [["@babel/env", { targets: { node: "current" } }]], 5 | }; 6 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/loader-path/main.js: -------------------------------------------------------------------------------- 1 | var test = function(a) { 2 | console.log(a); 3 | } 4 | 5 | test('hello world'); 6 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: pnpm install 3 | vscode: 4 | extensions: 5 | - dbaeumer.vscode-eslint 6 | - esbenp.prettier-vscode 7 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/no-config/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.failed(); 2 | 3 | expect(stdout).toMatch(/No configuration was found for webpack./); 4 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/error/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.failed(); 2 | 3 | expect(stdout).toMatch(/Can't resolve '\.\/b'/); 4 | expect(stdout).toMatch(/\.\/entry\.js 1:8-22/); 5 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/error-without-stats/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.failed(); 2 | 3 | expect(stdout).toMatch(/Can't resolve '\.\/b'/); 4 | expect(stdout).toMatch(/\.\/entry\.js 1:8-22/); 5 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/cache/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | const content = fs.readFileSync(path.join(cwd, 'output.js'), 'utf-8'); 4 | expect(content).toMatch(/console\.log\("dokey"\)/); 5 | 6 | -------------------------------------------------------------------------------- /tests/utils/loadGruntWebpackTasks.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function loadGruntWebpackTasks(grunt) { 4 | grunt.loadTasks(path.join(__dirname, '../../tasks')); 5 | } 6 | 7 | module.exports = loadGruntWebpackTasks; 8 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/circular-reference-in-plugin-options/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | // const content = fs.readFileSync(path.join(cwd, 'output.js'), 'utf-8'); 4 | // t.regex(content, /console\.log\("dokey"\);/); 5 | 6 | -------------------------------------------------------------------------------- /src/options/__mocks__/TestOptionHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const OptionHelper = require("../OptionHelper"); 4 | 5 | class TestOptionHelper extends OptionHelper { 6 | getDefaultOptions() { 7 | return {}; 8 | } 9 | } 10 | 11 | module.exports = TestOptionHelper; 12 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/no-config/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 3 | 4 | module.exports = function (grunt) { 5 | grunt.initConfig({}); 6 | 7 | loadGruntWebpackTasks(grunt); 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [*.js] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{package.json,.babelrc,.eslintrc}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack-dev-server/basic/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.timeout(); 2 | 3 | // stats should be displayed by default 4 | expect(stdout).toMatch(/output\.js/); 5 | expect(stdout).toMatch(/\[emitted\]/); 6 | expect(stdout).toMatch(/main/); 7 | expect(stderr).toMatch(/\[webpack-dev-server\] Project is running at:/); 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/options/__tests__/__snapshots__/OptionHelper.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`OptionHelper supports Promise as options 1`] = ` 4 | { 5 | "mode": "production", 6 | } 7 | `; 8 | 9 | exports[`OptionHelper supports functions as options 1`] = ` 10 | { 11 | "mode": "production", 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/watch-not-deprecated/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.timeout(); 2 | 3 | const content = fs.readFileSync(path.join(cwd, "output.js"), "utf-8"); 4 | expect(content).toMatch(/console\.log\("dokey"\)/); 5 | 6 | expect(stdout).not.toMatch(/DEP_WEBPACK_WATCH_WITHOUT_CALLBACK/); 7 | expect(stderr).not.toMatch(/DEP_WEBPACK_WATCH_WITHOUT_CALLBACK/); 8 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/basic/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | const content = fs.readFileSync(path.join(cwd, 'output.js'), 'utf-8'); 4 | expect(content).toMatch(/console\.log\("dokey"\)/); 5 | 6 | // stats should be displayed by default 7 | expect(stdout).toMatch(/output\.js/); 8 | expect(stdout).toMatch(/\[emitted\]/); 9 | expect(stdout).toMatch(/main/); 10 | 11 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/loader-path/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const config = { 4 | mode: "none", 5 | entry: { 6 | main: "./main.js", 7 | }, 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.js/, 12 | use: ["babel-loader"], 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/loader-path/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 2 | 3 | module.exports = function (grunt) { 4 | grunt.initConfig({ 5 | webpack: { 6 | options: require('./webpack.config.js'), 7 | dev: {} 8 | } 9 | }); 10 | 11 | loadGruntWebpackTasks(grunt); 12 | }; 13 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/banner-plugin/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | const content = fs.readFileSync(path.join(cwd, "output.js"), "utf-8"); 4 | expect(content).toMatch(/\/\*! Webpack - Version 6.55.345 dated/); 5 | 6 | // stats should be displayed by default 7 | expect(stdout).toMatch(/output\.js/); 8 | expect(stdout).toMatch(/\[emitted\]/); 9 | expect(stdout).toMatch(/main/); 10 | -------------------------------------------------------------------------------- /src/plugins/ProgressPluginFactory.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ProgressPlugin = require("webpack/lib/ProgressPlugin"); 4 | 5 | class ProgressPluginFactory { 6 | constructor(grunt) { 7 | this.grunt = grunt; 8 | } 9 | 10 | addPlugin(compiler, options) { 11 | new ProgressPlugin({ profile: options.profile }).apply(compiler); 12 | } 13 | } 14 | 15 | module.exports = ProgressPluginFactory; 16 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/split-chunks-issue-169/exec.js: -------------------------------------------------------------------------------- 1 | assertGrunt.success(); 2 | 3 | const content = fs.readFileSync(path.join(cwd, 'main.bundle.js'), 'utf-8'); 4 | expect(content).toMatch(/console\.log\("dokey"\)/); 5 | 6 | // stats should be displayed by default 7 | expect(stdout).toMatch(/\[emitted\]/); 8 | expect(stdout).toMatch(/main\.bundle\.js/); 9 | expect(stdout).toMatch(/common\.bundle\.js/); 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ----------------- | --- 3 | | Bug fix? | yes/no 4 | | Breaking change? | yes/no 5 | | New feature? | yes/no 6 | | Deprecations? | yes/no 7 | | Tests added? | yes/no 8 | | Docs updated? | yes/no 9 | | Fixed tickets | comma-separated list of tickets fixed by the PR, if any 10 | | License | MIT 11 | 12 | 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "extends": ["eslint:recommended", "webpack", "plugin:prettier/recommended"], 4 | "parserOptions": { 5 | "ecmaVersion": 2019, 6 | "sourceType": "script" 7 | }, 8 | "env": { 9 | "node": true 10 | }, 11 | "rules": { 12 | "no-plusplus": "off", 13 | "global-require": "off", 14 | "class-methods-use-this": "off", 15 | "no-param-reassign": "off", 16 | "no-underscore-dangle": "off", 17 | "no-undefined": "off" 18 | }, 19 | "overrides": [ 20 | { "files": "**/*.test.js", "parserOptions": { "sourceType": "module" } } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/options/__tests__/WebpackOptionHelper.test.js: -------------------------------------------------------------------------------- 1 | import WebpackOptionHelper from "../WebpackOptionHelper"; 2 | 3 | describe("WebpackOptionHelper", () => { 4 | test("watch options is part of webpack options", () => { 5 | const options = { 6 | watch: true, 7 | watchOptions: { aggregateTimeout: 300, poll: 1000 }, 8 | }; 9 | const helper = new WebpackOptionHelper(); 10 | 11 | helper.getOptions = () => options; 12 | 13 | const result = helper.getWebpackOptions(); 14 | 15 | expect(result).toEqual({ 16 | watchOptions: { aggregateTimeout: 300, poll: 1000 }, 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/utils/assertGrunt.js: -------------------------------------------------------------------------------- 1 | module.exports = function (returnCode, stdout, timeout) { 2 | return { 3 | success: function assertGruntSuccess() { 4 | expect(timeout).toBe(false); 5 | expect(returnCode).toBe(0); 6 | expect(stdout).not.toMatch(/ERROR/i); 7 | }, 8 | failed: function assertGruntFailed() { 9 | expect(timeout).toBe(false); 10 | expect(returnCode).toBeGreaterThan(0); 11 | expect(stdout).toMatch(/ERROR/i); 12 | }, 13 | timeout: function assertGruntTimeout() { 14 | expect(timeout).toBe(true); 15 | expect(stdout).not.toMatch(/ERROR/i); 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/options/WebpackOptionHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const defaults = require("./default"); 4 | const OptionHelper = require("./OptionHelper"); 5 | 6 | class WebpackOptionHelper extends OptionHelper { 7 | getDefaultOptions() { 8 | return Object.assign({}, defaults.gruntOptions, defaults.webpackOptions); 9 | } 10 | 11 | getWebpackOptions() { 12 | const options = this.getOptions(); 13 | 14 | if (Array.isArray(options)) { 15 | return options.map((opt) => this.filterGruntOptions(opt)); 16 | } 17 | 18 | return this.filterGruntOptions(options); 19 | } 20 | } 21 | 22 | module.exports = WebpackOptionHelper; 23 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/error/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | test: { 9 | mode: "none", 10 | entry: path.join(__dirname, "entry"), 11 | output: { 12 | path: __dirname, 13 | filename: "output.js", 14 | }, 15 | stats:{ 16 | colors: false 17 | } 18 | }, 19 | }, 20 | }); 21 | 22 | loadGruntWebpackTasks(grunt); 23 | }; 24 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/error-without-stats/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | test: { 9 | mode: "none", 10 | stats: false, 11 | entry: path.join(__dirname, "entry"), 12 | output: { 13 | path: __dirname, 14 | filename: "output.js", 15 | }, 16 | stats:{ 17 | colors: false 18 | } 19 | }, 20 | }, 21 | }); 22 | 23 | loadGruntWebpackTasks(grunt); 24 | }; 25 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/basic/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | test: { 9 | mode: "none", 10 | entry: path.join(__dirname, "entry"), 11 | output: { 12 | path: __dirname, 13 | filename: "output.js", 14 | }, 15 | plugins: [ 16 | new webpack.DefinePlugin({ okey: JSON.stringify("dokey") }), 17 | ], 18 | }, 19 | }, 20 | }); 21 | 22 | loadGruntWebpackTasks(grunt); 23 | }; 24 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/cache/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | test: { 9 | mode: "none", 10 | cache: true, 11 | entry: path.join(__dirname, "entry"), 12 | output: { 13 | path: __dirname, 14 | filename: "output.js", 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({ okey: JSON.stringify("dokey") }), 18 | ], 19 | }, 20 | }, 21 | }); 22 | 23 | loadGruntWebpackTasks(grunt); 24 | }; 25 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/watch-not-deprecated/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | test: { 9 | watch: true, 10 | mode: "none", 11 | entry: path.join(__dirname, "entry"), 12 | output: { 13 | path: __dirname, 14 | filename: "output.js", 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({ okey: JSON.stringify("dokey") }), 18 | ], 19 | }, 20 | }, 21 | }); 22 | 23 | loadGruntWebpackTasks(grunt); 24 | }; 25 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/circular-reference-in-plugin-options/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const loadGruntWebpackTasks = require("../../../../utils/loadGruntWebpackTasks"); 4 | 5 | const plugin = new webpack.DefinePlugin({ test: JSON.stringify("test") }); 6 | plugin.circle = plugin; 7 | 8 | module.exports = function (grunt) { 9 | grunt.initConfig({ 10 | webpack: { 11 | test: { 12 | mode: "none", 13 | entry: path.join(__dirname, "entry"), 14 | output: { 15 | path: __dirname, 16 | filename: "output.js", 17 | }, 18 | plugins: [plugin], 19 | }, 20 | }, 21 | }); 22 | 23 | loadGruntWebpackTasks(grunt); 24 | }; 25 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack-dev-server/basic/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | "webpack-dev-server": { 8 | test: { 9 | devServer: { 10 | open: false 11 | }, 12 | mode: "none", 13 | entry: path.join(__dirname, "entry"), 14 | output: { 15 | path: __dirname, 16 | filename: "output.js", 17 | }, 18 | plugins: [ 19 | new webpack.DefinePlugin({ okey: JSON.stringify("dokey") }), 20 | ], 21 | }, 22 | }, 23 | }); 24 | 25 | loadGruntWebpackTasks(grunt); 26 | }; 27 | -------------------------------------------------------------------------------- /src/options/__tests__/__snapshots__/WebpackDevServerOptionHelper.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`WebpackDevServerOptionHelper host is localhost by default 1`] = ` 4 | { 5 | "devServer": { 6 | "host": "localhost", 7 | }, 8 | "stats": { 9 | "cached": false, 10 | "cachedAssets": false, 11 | "colors": true, 12 | }, 13 | } 14 | `; 15 | 16 | exports[`WebpackDevServerOptionHelper host is localhost by default 2`] = ` 17 | { 18 | "host": "localhost", 19 | } 20 | `; 21 | 22 | exports[`WebpackDevServerOptionHelper supports array config for webpack 1`] = ` 23 | [ 24 | { 25 | "devServer": { 26 | "port": 8080, 27 | }, 28 | }, 29 | { 30 | "devServer": { 31 | "port": 9090, 32 | }, 33 | }, 34 | ] 35 | `; 36 | 37 | exports[`WebpackDevServerOptionHelper supports array config for webpack 2`] = ` 38 | { 39 | "port": 9090, 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/banner-plugin/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | name: "Webpack", 8 | outputFileName: "output.js", 9 | pkg: { 10 | copyright: '<%= name %>', 11 | version: '6.55.345', 12 | }, 13 | webpack: { 14 | test: (config) =>({ 15 | mode: "none", 16 | entry: path.join(__dirname, "entry"), 17 | output: { 18 | path: __dirname, 19 | filename: "<%= outputFileName %>", 20 | }, 21 | plugins: [ 22 | new webpack.BannerPlugin({ 23 | banner: `/*! ${config.pkg.copyright} - Version ${config.pkg.version} dated ${grunt.template.today()} */`, 24 | raw: true, 25 | }), 26 | ], 27 | }), 28 | }, 29 | }); 30 | 31 | loadGruntWebpackTasks(grunt); 32 | }; 33 | -------------------------------------------------------------------------------- /src/options/__tests__/WebpackDevServerOptionHelper.test.js: -------------------------------------------------------------------------------- 1 | import WebpackDevServerOptionHelper from "../WebpackDevServerOptionHelper"; 2 | 3 | describe("WebpackDevServerOptionHelper", () => { 4 | test("host is localhost by default", () => { 5 | const options = {}; 6 | const helper = new WebpackDevServerOptionHelper(); 7 | 8 | helper.readRawConfig = () => options; 9 | 10 | helper.preloadOptions(() => { 11 | expect(helper.getWebpackOptions()).toMatchSnapshot(); 12 | expect(helper.getWebpackDevServerOptions()).toMatchSnapshot(); 13 | }); 14 | }); 15 | 16 | test("supports array config for webpack", () => { 17 | const options = [ 18 | { devServer: { port: 8080 } }, 19 | { devServer: { port: 9090 } }, 20 | ]; 21 | const helper = new WebpackDevServerOptionHelper(); 22 | 23 | helper.getOptions = () => options; 24 | 25 | expect(helper.getWebpackOptions()).toMatchSnapshot(); 26 | expect(helper.getWebpackDevServerOptions()).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write # to be able to publish a GitHub release 10 | issues: write # to be able to comment on released issues 11 | pull-requests: write # to be able to comment on released pull requests 12 | id-token: write # to enable use of OIDC for trusted publishing and npm provenance 13 | 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - uses: pnpm/action-setup@v2 26 | name: Install pnpm 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: "lts/*" 32 | cache: pnpm 33 | 34 | - name: Release 35 | run: pnpm dlx semantic-release --branches main 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const loadGruntWebpackTasks = require("../../../../utils/loadGruntWebpackTasks"); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | options: { 9 | optimization: { 10 | minimize: true, 11 | }, 12 | }, 13 | test: [ 14 | { 15 | mode: "none", 16 | entry: path.join(__dirname, "entry"), 17 | output: { 18 | path: __dirname, 19 | filename: "bundle.js", 20 | }, 21 | plugins: [new webpack.BannerPlugin("this is a banner")], 22 | }, 23 | { 24 | mode: "none", 25 | entry: path.join(__dirname, "entry2"), 26 | output: { 27 | path: __dirname, 28 | filename: "bundle2.js", 29 | }, 30 | }, 31 | ], 32 | }, 33 | }); 34 | 35 | loadGruntWebpackTasks(grunt); 36 | }; 37 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/multi-compiler-verbose/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const loadGruntWebpackTasks = require("../../../../utils/loadGruntWebpackTasks"); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | options: { 9 | optimization: { 10 | minimize: true, 11 | }, 12 | }, 13 | test: [ 14 | { 15 | mode: "none", 16 | entry: path.join(__dirname, "entry"), 17 | output: { 18 | path: __dirname, 19 | filename: "bundle.js", 20 | }, 21 | plugins: [new webpack.BannerPlugin("this is a banner")], 22 | }, 23 | { 24 | mode: "none", 25 | entry: path.join(__dirname, "entry2"), 26 | output: { 27 | path: __dirname, 28 | filename: "bundle2.js", 29 | }, 30 | }, 31 | ], 32 | }, 33 | }); 34 | 35 | loadGruntWebpackTasks(grunt); 36 | }; 37 | -------------------------------------------------------------------------------- /src/options/WebpackDevServerOptionHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const defaults = require("./default"); 4 | const OptionHelper = require("./OptionHelper"); 5 | 6 | class WebpackOptionHelper extends OptionHelper { 7 | getDefaultOptions() { 8 | return Object.assign( 9 | {}, 10 | defaults.gruntOptions, 11 | defaults.webpackDevServerOptions, 12 | ); 13 | } 14 | 15 | getWebpackOptions() { 16 | const options = this.getOptions(); 17 | 18 | if (Array.isArray(options)) { 19 | return options.map((opt) => this.filterGruntOptions(opt)); 20 | } 21 | 22 | return this.filterGruntOptions(options); 23 | } 24 | 25 | getWebpackDevServerOptions() { 26 | const options = this.getOptions(); 27 | 28 | if (Array.isArray(options)) { 29 | return options.reduce( 30 | (previous, current) => ({ 31 | ...previous, 32 | ...current.devServer, 33 | }), 34 | {}, 35 | ); 36 | } 37 | 38 | return options.devServer; 39 | } 40 | } 41 | 42 | module.exports = WebpackOptionHelper; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/options/__tests__/__snapshots__/default.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`default options mergeOptions all single objects 1`] = ` 4 | { 5 | "default": 1, 6 | "options": 1, 7 | "target": 1, 8 | } 9 | `; 10 | 11 | exports[`default options mergeOptions merges sub arrays 1`] = ` 12 | { 13 | "array": [ 14 | 1, 15 | 2, 16 | 3, 17 | ], 18 | } 19 | `; 20 | 21 | exports[`default options mergeOptions options and target as array 1`] = ` 22 | [ 23 | { 24 | "default": 1, 25 | "options1": 1, 26 | "target1": 1, 27 | }, 28 | { 29 | "default": 1, 30 | "options2": 1, 31 | "target2": 1, 32 | }, 33 | ] 34 | `; 35 | 36 | exports[`default options mergeOptions options as array 1`] = ` 37 | [ 38 | { 39 | "default": 1, 40 | "options1": 1, 41 | "target": 1, 42 | }, 43 | { 44 | "default": 1, 45 | "options2": 1, 46 | "target": 1, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`default options mergeOptions target as array 1`] = ` 52 | [ 53 | { 54 | "default": 1, 55 | "options": 1, 56 | "target1": 1, 57 | }, 58 | { 59 | "default": 1, 60 | "options": 1, 61 | "target2": 1, 62 | }, 63 | ] 64 | `; 65 | -------------------------------------------------------------------------------- /src/options/__tests__/OptionHelper.test.js: -------------------------------------------------------------------------------- 1 | import OptionHelper from "../__mocks__/TestOptionHelper"; 2 | 3 | describe("OptionHelper", () => { 4 | test("supports functions as options", () => { 5 | const options = () => ({ mode: "production" }); 6 | const helper = new OptionHelper({ 7 | config: { 8 | getRaw() { 9 | return options; 10 | }, 11 | get() { 12 | return options; 13 | }, 14 | process(value) { 15 | return value; 16 | }, 17 | }, 18 | }); 19 | 20 | helper.preloadOptions(() => { 21 | expect(helper.getOptions()).toMatchSnapshot(); 22 | }); 23 | }); 24 | 25 | test("supports Promise as options", () => { 26 | const options = Promise.resolve({ 27 | mode: "production", 28 | }); 29 | const helper = new OptionHelper({ 30 | config: { 31 | getRaw() { 32 | return options; 33 | }, 34 | get() { 35 | return options; 36 | }, 37 | process(value) { 38 | return value; 39 | }, 40 | }, 41 | }); 42 | 43 | helper.preloadOptions(() => { 44 | expect(helper.getOptions()).toMatchSnapshot(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/integration/fixtures/webpack/split-chunks-issue-169/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const loadGruntWebpackTasks = require('../../../../utils/loadGruntWebpackTasks'); 4 | 5 | module.exports = function (grunt) { 6 | grunt.initConfig({ 7 | webpack: { 8 | test: { 9 | mode: "none", 10 | entry: path.join(__dirname, "entry"), 11 | output: { 12 | path: __dirname, 13 | filename: "[name].bundle.js", 14 | }, 15 | plugins: [ 16 | new webpack.DefinePlugin({ okey: JSON.stringify("dokey") }), 17 | ], 18 | optimization: { 19 | splitChunks: { 20 | cacheGroups: { 21 | common: { 22 | test: function (module) { 23 | if (module.resource) { 24 | return module.resource.includes('/uniquerandomfolder/'); 25 | } 26 | }, 27 | name: 'common', 28 | minChunks: 1, 29 | minSize: 0, 30 | chunks: 'all' 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | }, 37 | }); 38 | 39 | loadGruntWebpackTasks(grunt); 40 | }; 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | tests: 13 | name: Unit tests 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: ["18", "20", "22", "24"] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - uses: pnpm/action-setup@v2 25 | name: Install pnpm 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: pnpm 32 | 33 | - name: Install dependencies 34 | run: pnpm install --frozen-lockfile 35 | 36 | - name: Tests 37 | run: pnpm test 38 | 39 | lint: 40 | name: Lint 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v3 46 | 47 | - uses: pnpm/action-setup@v2 48 | name: Install pnpm 49 | 50 | - name: Setup Node.js 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: "lts/*" 54 | cache: pnpm 55 | 56 | - name: Install dependencies 57 | run: pnpm install --frozen-lockfile 58 | 59 | - name: Lint 60 | run: pnpm lint 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-webpack", 3 | "description": "Use webpack with grunt.", 4 | "version": "7.0.1", 5 | "homepage": "https://github.com/webpack/grunt-webpack", 6 | "author": { 7 | "name": "Tobias Koppers @sokra" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/webpack/grunt-webpack.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/webpack/grunt-webpack/issues" 15 | }, 16 | "publishConfig": { 17 | "registry": "https://registry.npmjs.org/", 18 | "tag": "latest", 19 | "provenance": true 20 | }, 21 | "packageManager": "pnpm@10.23.0", 22 | "funding": { 23 | "type": "opencollective", 24 | "url": "https://opencollective.com/webpack" 25 | }, 26 | "engines": { 27 | "node": ">=18.19.0" 28 | }, 29 | "files": [ 30 | "src", 31 | "tasks" 32 | ], 33 | "peerDependencies": { 34 | "webpack": "^5.0.0" 35 | }, 36 | "license": "MIT", 37 | "keywords": [ 38 | "gruntplugin", 39 | "webpack", 40 | "grunt" 41 | ], 42 | "scripts": { 43 | "lint": "eslint ./src ./tasks --report-unused-disable-directives", 44 | "test": "jest" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.28.5", 48 | "@babel/preset-env": "^7.28.5", 49 | "babel-loader": "^9.2.1", 50 | "eslint": "^8.57.1", 51 | "eslint-config-prettier": "^8.10.2", 52 | "eslint-config-webpack": "^1.2.5", 53 | "eslint-plugin-import": "^2.32.0", 54 | "eslint-plugin-prettier": "^5.5.4", 55 | "fast-glob": "^3.3.3", 56 | "fs-extra": "^11.3.2", 57 | "grunt": "^1.6.1", 58 | "jest": "^30.2.0", 59 | "prettier": "^3.7.4", 60 | "webpack": "^5.103.0", 61 | "webpack-dev-server": "^5.2.2" 62 | }, 63 | "dependencies": { 64 | "deep-for-each": "^3.0.0", 65 | "webpack-merge": "^6.0.1" 66 | }, 67 | "jest": { 68 | "collectCoverageFrom": [ 69 | "src/**/*.js", 70 | "tasks/**/*.js" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | ### Webpack/Grunt Configuration (webpack.config.js, webpack part of Gruntfile.js) 16 | 17 | 18 | ```json 19 | { 20 | "your": { "config": "here" } 21 | } 22 | ``` 23 | 24 | ### Expected Behavior 25 | 26 | 27 | 28 | ### Current Behavior 29 | 30 | 31 | 32 | ### Possible Solution 33 | 34 | 35 | 36 | ### Context 37 | 38 | 39 | 40 | ### Your Environment 41 | 42 | 43 | | software | version 44 | | ---------------- | ------- 45 | | webpack | 46 | | grunt-webpack | 47 | | grunt | 48 | | node | 49 | | npm | 50 | | Operating System | 51 | -------------------------------------------------------------------------------- /src/options/default.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { merge } = require("webpack-merge"); 4 | 5 | const gruntOptions = { 6 | failOnError: (options) => { 7 | // if watch enabled default to failOnError false 8 | return Array.isArray(options) 9 | ? options.every((option) => !option.watch) 10 | : !options.watch; 11 | }, 12 | progress: process.stdout.isTTY, 13 | storeStatsTo: null, 14 | keepalive: (options) => { 15 | // if watch enabled default to keepalive true 16 | return Array.isArray(options) 17 | ? options.some((option) => option.watch) 18 | : !!options.watch; 19 | }, 20 | watch: null, 21 | }; 22 | 23 | const webpackOptions = { 24 | stats: { 25 | cached: false, 26 | cachedAssets: false, 27 | colors: true, 28 | }, 29 | }; 30 | 31 | const webpackDevServerOptions = { 32 | devServer: { 33 | host: "localhost", 34 | }, 35 | stats: { 36 | cached: false, 37 | cachedAssets: false, 38 | colors: true, 39 | }, 40 | }; 41 | 42 | function mergeOptions(defaultOptions, options, targetOptions) { 43 | if (Array.isArray(targetOptions) && Array.isArray(options)) { 44 | if (targetOptions.length !== options.length) { 45 | throw new Error( 46 | "Cannot have both `options` and `target` be an array with different length. " + 47 | "If using arrays for both please ensure they are the same size.", 48 | ); 49 | } 50 | return targetOptions.map((opt, index) => 51 | merge(defaultOptions, options[index], opt), 52 | ); 53 | } 54 | 55 | if (Array.isArray(targetOptions)) { 56 | return targetOptions.map((opt) => merge(defaultOptions, options, opt)); 57 | } else if (Array.isArray(options)) { 58 | return options.map((opt) => merge(defaultOptions, opt, targetOptions)); 59 | } 60 | 61 | return merge(defaultOptions, options, targetOptions); 62 | } 63 | 64 | exports.gruntOptions = gruntOptions; 65 | exports.webpackOptions = webpackOptions; 66 | exports.webpackDevServerOptions = webpackDevServerOptions; 67 | 68 | exports.mergeOptions = mergeOptions; 69 | -------------------------------------------------------------------------------- /src/options/OptionHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const deepForEach = require("deep-for-each"); 4 | const defaults = require("./default"); 5 | 6 | class OptionHelper { 7 | constructor(grunt, taskName, target) { 8 | this.grunt = grunt; 9 | this.taskName = taskName; 10 | this.target = target; 11 | } 12 | 13 | preloadOptions(done) { 14 | if (!this.options) { 15 | this.generateOptions() 16 | .then((options) => { 17 | this.options = options; 18 | done(); 19 | }) 20 | .catch((error) => { 21 | // eslint-disable-next-line no-console 22 | console.error(error); 23 | done(); 24 | }); 25 | } else { 26 | process.nextTick(done); 27 | } 28 | } 29 | 30 | async generateOptions() { 31 | const baseOptions = await this.readRawConfig([this.taskName, "options"]); 32 | const targetOptions = await this.readRawConfig([ 33 | this.taskName, 34 | this.target, 35 | ]); 36 | 37 | return defaults.mergeOptions( 38 | this.getDefaultOptions(), 39 | baseOptions, 40 | targetOptions, 41 | ); 42 | } 43 | 44 | getOptions() { 45 | if (!this.options) { 46 | throw new Error("Options need to be preloaded with `preloadOptions()`"); 47 | } 48 | 49 | return this.options; 50 | } 51 | 52 | get(name) { 53 | const options = this.getOptions(); 54 | let option; 55 | 56 | if (Array.isArray(options)) { 57 | let value; 58 | options.some((opt) => { 59 | value = opt[name]; 60 | return value != null; 61 | }); 62 | 63 | option = value; 64 | } else { 65 | option = options[name]; 66 | } 67 | 68 | return typeof option === "function" ? option(options) : option; 69 | } 70 | 71 | async readRawConfig(ns) { 72 | let obj = this.grunt.config.getRaw(ns) || {}; 73 | 74 | if (typeof obj === "function") { 75 | obj = obj(this.grunt.config.get()); 76 | } 77 | 78 | // Might be a Promise 79 | const options = await obj; 80 | 81 | deepForEach(options, (value, key, parent) => { 82 | if (typeof value === "string") { 83 | parent[key] = this.grunt.config.process(value); 84 | } 85 | }); 86 | 87 | return options; 88 | } 89 | 90 | filterGruntOptions(options) { 91 | const result = Object.assign({}, options); 92 | Object.keys(defaults.gruntOptions).forEach((key) => delete result[key]); 93 | 94 | return result; 95 | } 96 | } 97 | 98 | module.exports = OptionHelper; 99 | -------------------------------------------------------------------------------- /tasks/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const webpack = require("webpack"); 4 | const OptionHelper = require("../src/options/WebpackDevServerOptionHelper"); 5 | const ProgressPluginFactory = require("../src/plugins/ProgressPluginFactory"); 6 | 7 | module.exports = (grunt) => { 8 | let WebpackDevServer; 9 | try { 10 | // eslint-disable-next-line import/no-extraneous-dependencies 11 | WebpackDevServer = require("webpack-dev-server"); 12 | } catch (err) { 13 | grunt.registerTask( 14 | "webpack-dev-server", 15 | "webpack-dev-server not installed.", 16 | () => { 17 | grunt.fail.fatal( 18 | `webpack-dev-server is currently not installed, this task will do nothing. 19 | 20 | To fix this problem install webpack-dev-server by doing 21 | npm install --save-dev webpack-dev-server 22 | or similar with you favorite package manager. 23 | `, 24 | ); 25 | }, 26 | ); 27 | return; 28 | } 29 | 30 | const processPluginFactory = new ProgressPluginFactory(grunt); 31 | 32 | grunt.registerTask( 33 | "webpack-dev-server", 34 | "Start a webpack-dev-server.", 35 | function webpackDevServerTask(cliTarget) { 36 | const done = this.async(); 37 | 38 | let targets; 39 | if (cliTarget) { 40 | targets = [cliTarget]; 41 | } else { 42 | const config = grunt.config.getRaw([this.name]); 43 | targets = config ? Object.keys(config) : []; 44 | } 45 | 46 | let runningTargetCount = targets.length; 47 | 48 | if (runningTargetCount === 0) { 49 | done( 50 | new Error( 51 | "No configuration was found for webpack-dev-server. For further assistance on how to create the config refer to https://github.com/webpack/grunt-webpack/blob/main/README.md#grunt-webpack", 52 | ), 53 | ); 54 | return; 55 | } 56 | 57 | targets.forEach((target) => { 58 | if (target === "options") { 59 | runningTargetCount--; 60 | return; 61 | } 62 | 63 | const optionHelper = new OptionHelper(grunt, this.name, target); 64 | optionHelper.preloadOptions(() => { 65 | const opts = optionHelper.getOptions(); 66 | const webpackOptions = optionHelper.getWebpackOptions(); 67 | 68 | const compiler = webpack(webpackOptions); 69 | if (opts.progress) { 70 | processPluginFactory.addPlugin(compiler, webpackOptions); 71 | } 72 | 73 | const server = new WebpackDevServer( 74 | optionHelper.getWebpackDevServerOptions(), 75 | compiler, 76 | ); 77 | 78 | server.startCallback(() => {}); 79 | }); 80 | }); 81 | }, 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/options/__tests__/default.test.js: -------------------------------------------------------------------------------- 1 | import { gruntOptions, mergeOptions } from "../default"; 2 | 3 | describe("default options", () => { 4 | describe("keepalive", () => { 5 | test("keepalive without watch", () => { 6 | expect(gruntOptions.keepalive({})).toBe(false); 7 | }); 8 | 9 | test("keepalive with watch true", () => { 10 | expect(gruntOptions.keepalive({ watch: true })).toBe(true); 11 | }); 12 | 13 | test("keepalive with watch false", () => { 14 | expect(gruntOptions.keepalive({ watch: false })).toBe(false); 15 | }); 16 | 17 | test("keepalive without watch in array", () => { 18 | expect(gruntOptions.keepalive([{}])).toBe(false); 19 | }); 20 | 21 | test("keepalive with watch true in array", () => { 22 | expect(gruntOptions.keepalive([{ watch: true }])).toBe(true); 23 | }); 24 | 25 | test("keepalive with watch false in array", () => { 26 | expect(gruntOptions.keepalive([{ watch: false }])).toBe(false); 27 | }); 28 | }); 29 | 30 | describe("failOnError", () => { 31 | test("failOnError without watch", () => { 32 | expect(gruntOptions.failOnError({})).toBe(true); 33 | }); 34 | 35 | test("failOnError with watch true", () => { 36 | expect(gruntOptions.failOnError({ watch: true })).toBe(false); 37 | }); 38 | 39 | test("failOnError with watch false", () => { 40 | expect(gruntOptions.failOnError({ watch: false })).toBe(true); 41 | }); 42 | 43 | test("failOnError without watch in array", () => { 44 | expect(gruntOptions.failOnError([{}])).toBe(true); 45 | }); 46 | 47 | test("failOnError with watch true in array", () => { 48 | expect(gruntOptions.failOnError([{ watch: true }, {}])).toBe(false); 49 | }); 50 | 51 | test("failOnError with watch false in array", () => { 52 | expect(gruntOptions.failOnError([{ watch: false }, {}, {}])).toBe(true); 53 | }); 54 | }); 55 | 56 | describe("mergeOptions", () => { 57 | test("all single objects", () => { 58 | expect( 59 | mergeOptions({ default: 1 }, { options: 1 }, { target: 1 }), 60 | ).toMatchSnapshot(); 61 | }); 62 | 63 | test("merges sub arrays", () => { 64 | expect( 65 | mergeOptions({ array: [1] }, { array: [2] }, { array: [3] }), 66 | ).toMatchSnapshot(); 67 | }); 68 | 69 | test("options as array", () => { 70 | expect( 71 | mergeOptions({ default: 1 }, [{ options1: 1 }, { options2: 1 }], { 72 | target: 1, 73 | }), 74 | ).toMatchSnapshot(); 75 | }); 76 | 77 | test("target as array", () => { 78 | expect( 79 | mergeOptions( 80 | { default: 1 }, 81 | { 82 | options: 1, 83 | }, 84 | [{ target1: 1 }, { target2: 1 }], 85 | ), 86 | ).toMatchSnapshot(); 87 | }); 88 | 89 | test("options and target as array", () => { 90 | expect( 91 | mergeOptions( 92 | { default: 1 }, 93 | [{ options1: 1 }, { options2: 1 }], 94 | [{ target1: 1 }, { target2: 1 }], 95 | ), 96 | ).toMatchSnapshot(); 97 | }); 98 | 99 | test("fails on options and target as unequal array", () => { 100 | expect(() => 101 | mergeOptions( 102 | { default: 1 }, 103 | [{ options1: 1 }, { options2: 1 }, { options3: 1 }], 104 | [{ target1: 1 }, { target2: 1 }], 105 | ), 106 | ).toThrow(); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/integration/fixtures.test.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import crypto from "crypto"; 3 | import { transform } from "@babel/core"; 4 | import envPreset from "@babel/preset-env"; 5 | import fs from "fs-extra"; 6 | import glob from "fast-glob"; 7 | import { spawn } from "child_process"; 8 | import assertGruntFactory from "../utils/assertGrunt"; 9 | 10 | const TMP_DIRECTORY = path.join(__dirname, "tmp"); 11 | const GRUNT_BIN = path.join(__dirname, "../../node_modules/.bin/grunt"); 12 | 13 | const files = glob.sync(path.join(__dirname, "fixtures/**/Gruntfile.js"), { 14 | dot: true, 15 | }); 16 | const tests = new Map(); 17 | 18 | async function fileExists(filename) { 19 | try { 20 | await fs.promises.access(filename); 21 | return true; 22 | } catch (err) { 23 | if (err.code === 'ENOENT') { 24 | return false; 25 | } else { 26 | throw err; 27 | } 28 | } 29 | } 30 | 31 | function runExec(code, opts) { 32 | const sandbox = Object.assign( 33 | { 34 | fs, 35 | path, 36 | assertGrunt: assertGruntFactory( 37 | opts.returnCode, 38 | opts.stdout, 39 | opts.timeout, 40 | ), 41 | }, 42 | opts, 43 | ); 44 | 45 | const execCode = transform(code, { 46 | ast: false, 47 | sourceMaps: false, 48 | compact: true, 49 | comments: false, 50 | presets: [[envPreset, { targets: { node: "current" } }]], 51 | }).code; 52 | 53 | let fn = new Function(...Object.keys(sandbox), execCode); 54 | return fn.apply(null, Object.values(sandbox)); 55 | } 56 | 57 | files.forEach((file) => { 58 | const directory = path.dirname(file); 59 | const relativeDirectory = path.relative( 60 | path.join(__dirname, "fixtures"), 61 | directory, 62 | ); 63 | const name = relativeDirectory.replace(/\//g, " "); 64 | 65 | tests.set(name, { directory, relativeDirectory }); 66 | }); 67 | 68 | describe("Fixture Tests", () => { 69 | let cwd; 70 | beforeEach(async () => { 71 | cwd = path.join( 72 | TMP_DIRECTORY, 73 | "integration", 74 | crypto.randomBytes(20).toString("hex"), 75 | ); 76 | await fs.ensureDir(cwd); 77 | }); 78 | afterEach(async () => { 79 | await fs.remove(cwd); 80 | cwd = null; 81 | }); 82 | afterAll(async () => { 83 | await fs.remove(TMP_DIRECTORY); 84 | }); 85 | 86 | tests.forEach(({ directory, relativeDirectory }, name) => { 87 | const directoryParts = relativeDirectory.split("/"); 88 | const testFunc = directoryParts.pop().startsWith(".") ? test.skip : test; 89 | 90 | testFunc( 91 | name, 92 | async () => { 93 | await fs.copy(directory, cwd); 94 | let optionsLoc = path.join(cwd, "options.json"); 95 | let options; 96 | if (await fileExists(optionsLoc)) { 97 | options = require(optionsLoc); 98 | } else { 99 | options = { 100 | args: [directoryParts.shift()], 101 | }; 102 | } 103 | 104 | options.args.unshift("--stack"); 105 | 106 | const execLoc = path.join(cwd, "exec.js"); 107 | let execCode; 108 | if (await fileExists(execLoc)) { 109 | execCode = await fs.readFile(execLoc, "utf-8"); 110 | } 111 | const grunt = spawn(GRUNT_BIN, options.args, { cwd }); 112 | 113 | let stdout = ""; 114 | let stderr = ""; 115 | grunt.stdout.on("data", (data) => { 116 | stdout += data.toString(); 117 | }); 118 | grunt.stderr.on("data", (data) => { 119 | stderr += data.toString(); 120 | }); 121 | 122 | return new Promise((resolve) => { 123 | const timeout = setTimeout(() => { 124 | grunt.removeAllListeners(); 125 | const killed = grunt.kill(); 126 | if (!killed) console.error("cannot kill grunt"); 127 | finish({ timeout: true }); 128 | }, 3000); 129 | 130 | const finish = (result) => { 131 | clearTimeout(timeout); 132 | if (execCode) runExec(execCode, { cwd, stderr, stdout, ...result }); 133 | resolve(); 134 | }; 135 | 136 | grunt.on("close", (returnCode) => { 137 | finish({ returnCode, timeout: false }); 138 | }); 139 | }); 140 | }, 141 | 10000, 142 | ); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /tasks/webpack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const webpack = require("webpack"); 4 | const pkg = require("../package.json"); 5 | const OptionHelper = require("../src/options/WebpackOptionHelper"); 6 | const ProgressPluginFactory = require("../src/plugins/ProgressPluginFactory"); 7 | 8 | module.exports = (grunt) => { 9 | const processPluginFactory = new ProgressPluginFactory(grunt); 10 | 11 | grunt.registerTask( 12 | "webpack", 13 | "Run webpack.", 14 | function webpackTask(cliTarget) { 15 | const done = this.async(); 16 | 17 | let targets; 18 | if (cliTarget) { 19 | targets = [cliTarget]; 20 | } else { 21 | const config = grunt.config.getRaw([this.name]); 22 | targets = config ? Object.keys(config) : []; 23 | } 24 | let runningTargetCount = targets.length; 25 | let keepalive = false; 26 | 27 | if (runningTargetCount === 0) { 28 | done( 29 | new Error( 30 | "No configuration was found for webpack. For further assistance on how to create the config refer to https://github.com/webpack/grunt-webpack/blob/main/README.md#grunt-webpack", 31 | ), 32 | ); 33 | return; 34 | } 35 | 36 | const compilerClosePromises = []; 37 | 38 | targets.forEach((target) => { 39 | if (target === "options") { 40 | runningTargetCount--; 41 | return; 42 | } 43 | 44 | const optionHelper = new OptionHelper(grunt, this.name, target); 45 | 46 | optionHelper.preloadOptions(() => { 47 | const watch = optionHelper.get("watch"); 48 | const opts = { 49 | cache: watch ? false : optionHelper.get("cache"), 50 | failOnError: optionHelper.get("failOnError"), 51 | keepalive: optionHelper.get("keepalive"), 52 | progress: optionHelper.get("progress"), 53 | stats: optionHelper.get("stats"), 54 | storeStatsTo: optionHelper.get("storeStatsTo"), 55 | watch, 56 | }; 57 | 58 | const webpackOptions = optionHelper.getWebpackOptions(); 59 | 60 | const compiler = webpack(webpackOptions); 61 | 62 | if (opts.progress) { 63 | processPluginFactory.addPlugin(compiler, webpackOptions); 64 | } 65 | 66 | const handler = (webpackErr, stats) => { 67 | if (webpackErr) { 68 | done(webpackErr); 69 | return; 70 | } 71 | 72 | if (opts.stats && !stats.hasErrors()) { 73 | grunt.log.writeln( 74 | stats 75 | .toString(opts.stats) 76 | // add plugin version with and without colors 77 | .replace( 78 | /(\n(.*)Version: webpack (.*)\d+\.\d+\.\d+(.*))\n/, 79 | `$1$2 / grunt-webpack $3${pkg.version}$4\n`, 80 | ), 81 | ); 82 | } 83 | 84 | if (typeof opts.storeStatsTo === "string") { 85 | grunt.config.set(opts.storeStatsTo, stats.toJson(opts.stats)); 86 | } 87 | 88 | if (stats.hasErrors()) { 89 | // in case opts.stats === false we still want to display errors. 90 | grunt.log.writeln(stats.toString(opts.stats || "errors-only")); 91 | if (opts.failOnError) { 92 | // construct error without stacktrace, as the stack is not relevant here 93 | const error = new Error(); 94 | error.stack = null; 95 | done(error); 96 | return; 97 | } 98 | } 99 | 100 | keepalive = keepalive || opts.keepalive; 101 | 102 | if (!keepalive) { 103 | compilerClosePromises.push( 104 | new Promise((resolve, reject) => 105 | compiler.close((err) => (err ? reject(err) : resolve())), 106 | ), 107 | ); 108 | if (--runningTargetCount === 0) { 109 | Promise.all(compilerClosePromises) 110 | .then(() => done()) 111 | .catch((err) => done(err)); 112 | } 113 | } 114 | }; 115 | 116 | if (opts.watch) { 117 | compiler.watch(webpackOptions.watchOptions || {}, handler); 118 | } else { 119 | compiler.run(handler); 120 | } 121 | }); 122 | }); 123 | }, 124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm][npm]][npm-url] 2 | 3 |
13 | 14 |