├── .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 |
4 | 6 | 7 | 9 | 10 |

Grunt Webpack

11 |

Use Webpack with Grunt.

12 |

13 | 14 |

Requirements

15 | 16 | - Version 6 of `grunt-webpack` supports webpack 5 and (optional) `webpack-dev-server` version 5. 17 | - Node.js 18.19.0 or newer 18 | 19 |

Install

20 | 21 | Install this grunt plugin next to your project's [Gruntfile.js](http://gruntjs.com/getting-started). You also need to install webpack yourself, this grunt plugin does not install webpack itself. 22 | 23 | ```bash 24 | npm install webpack grunt-webpack --save-dev 25 | ``` 26 | 27 | If you also want to use the webpack-dev-server task you also need to install `webpack-dev-server` 28 | 29 | ```bash 30 | npm install webpack-dev-server --save-dev 31 | ``` 32 | 33 | Then add this line to your project's `Gruntfile.js` gruntfile: 34 | 35 | ```javascript 36 | const webpackConfig = require('./webpack.config.js'); 37 | 38 | module.exports = function(grunt) { 39 | 40 | // Project configuration. 41 | grunt.initConfig({ 42 | ..., 43 | webpack: { 44 | myConfig: webpackConfig, 45 | }, 46 | ... 47 | }); 48 | 49 | grunt.loadNpmTasks('grunt-webpack'); 50 | }; 51 | ``` 52 | 53 |

Configuration

54 | 55 | `webpack-grunt` offers two different tasks `webpack` and `webpack-dev-server`. Both support all webpack options as 56 | can be seen in the [webpack documentation][webpack-config]. For exceptions and additions see this list. 57 | 58 | ### Both Tasks 59 | 60 | #### progress 61 | 62 | Type: `bool` 63 | Default: `true` (`false` if no TTY present) 64 | 65 | Activates or deactivates the progress output of webpack. 66 | 67 | ### Webpack Task 68 | 69 | #### failOnError 70 | 71 | Type: `bool` 72 | Default: `true` (`false` if watch mode is used) 73 | 74 | Will terminate the grunt process when an error happens if set to `true`. If set to `false` the grunt process will not be immediately terminated on error and instead continue to run. 75 | 76 | #### keepalive 77 | 78 | Type: `bool` 79 | Default: `false` (`true` if watch mode is used) 80 | 81 | When set to true the grunt process/task will be kept alive after webpack task is finished. This is especially useful for `watch` as this usually needs to run as long as not manually killed. 82 | 83 | #### storeStatsTo 84 | 85 | Type: `string` 86 | Default: `null` 87 | 88 | When set the stats from webpack will be written to a variable with the name provided in this option. The variable can later be used inside of other grunt tasks with template tags `<%= %>`. 89 | 90 | ```js 91 | ... 92 | storeStatsTo: "webpackStats" 93 | 94 | ... 95 | 96 | <%= webpackStats.hash %> 97 | ... 98 | ``` 99 | 100 | > For more information about grunt template tags have a look at the [grunt docs][grunt-template]. 101 | 102 | #### watch 103 | 104 | Type: `bool` 105 | Default: `undefined` 106 | 107 | > Turn on watch mode. This means that after the initial build, webpack will continue to watch for changes in any of the resolved files. 108 | 109 | Turning on watch mode also sets the following defaults: 110 | 111 | - Default `cache` to `true` 112 | - Default `keepalive` to `true` 113 | - Default `failOnError` to `false` 114 | 115 | ### Webpack-dev-server Task 116 | 117 | There are no special options for this task. Some changed defaults for WebpackDevServer options are: 118 | 119 | | Name | default value | 120 | | -------------- | ------------- | 121 | | devServer.host | localhost | 122 | 123 |

Examples

124 | 125 | ### Webpack 126 | 127 | #### imported config 128 | 129 | This is a simple example that requires the webpack config from the config file. 130 | It also disables stats in non 'development' environments and enables watch in development. 131 | 132 | ```javascript 133 | const webpackConfig = require("./webpack.config.js"); 134 | 135 | module.exports = function (grunt) { 136 | grunt.initConfig({ 137 | webpack: { 138 | options: { 139 | stats: !process.env.NODE_ENV || process.env.NODE_ENV === "development", 140 | }, 141 | prod: webpackConfig, 142 | dev: Object.assign({ watch: true }, webpackConfig), 143 | }, 144 | }); 145 | 146 | grunt.loadNpmTasks("grunt-webpack"); 147 | }; 148 | ``` 149 | 150 | > The webpack task in this example has two targets called `prod` and `dev`. They can be renamed to everything besides `options`. See the [grunt docs][grunt-targets] for more information about targets. 151 | 152 | On the command line you can then do the following. 153 | 154 | ```bash 155 | # Run webpack with the `prod` target 156 | > NODE_ENV='production' grunt webpack:prod 157 | 158 | # Run webpack with the `dev` target 159 | > grunt webpack:dev 160 | 161 | # Run webpack for all targets 162 | > grunt webpack 163 | ``` 164 | 165 | > For more examples and information have a look at the [webpack documentation][webpack-start] which mostly also applies here besides the noted differences above. 166 | 167 | #### Lazy config loading 168 | 169 | In some cases you might want to load the webpack config lazy. This can be done by specifying a function as the config value. The first paramter to this function will be the complete grunt config, which can be used in cases where grunt templates do not work (see below). 170 | 171 | ```js 172 | module.exports = function (grunt) { 173 | grunt.initConfig({ 174 | webpack: { 175 | myconfig: () => ({ 176 | entry: path.join(__dirname, "entry"), 177 | output: { 178 | path: __dirname, 179 | filename: "output.js", 180 | }, 181 | }), 182 | }, 183 | }); 184 | 185 | grunt.loadNpmTasks("grunt-webpack"); 186 | }; 187 | ``` 188 | 189 | You could also use a promise 190 | 191 | ```js 192 | const webpackConfig = require("./webpack.config.js"); 193 | 194 | module.exports = function (grunt) { 195 | grunt.initConfig({ 196 | webpack: { 197 | myconfig: Promise.resolve(webpackConfig), 198 | }, 199 | }); 200 | 201 | grunt.loadNpmTasks("grunt-webpack"); 202 | }; 203 | ``` 204 | 205 | #### Grunt templates 206 | 207 | grunt-webpack supports grunt templates in all string values in it's configuration. 208 | 209 | In the next example we use a template for `output.filename`. 210 | 211 | ```js 212 | module.exports = function (grunt) { 213 | grunt.initConfig({ 214 | outputFileName: "output.js", 215 | webpack: { 216 | myconfig: { 217 | entry: path.join(__dirname, "entry"), 218 | output: { 219 | path: __dirname, 220 | filename: "<%= outputFileName %>", 221 | }, 222 | }, 223 | }, 224 | }); 225 | 226 | grunt.loadNpmTasks("grunt-webpack"); 227 | }; 228 | ``` 229 | 230 | For plugins we cannot support grunt template interpolation, as plugins are class instances which we cannot modify during runtime without breaking them. If you need to use template in a string option to a plugin, you can use lazy config loading and use the supplied config. You can also use grunt inside directly to access utility methods: 231 | 232 | ```js 233 | module.exports = function (grunt) { 234 | grunt.initConfig({ 235 | name: "Webpack", 236 | pkg: { 237 | copyright: "<%= name %>", 238 | version: "6.55.345", 239 | }, 240 | webpack: { 241 | myconfig: (config) => ({ 242 | entry: path.join(__dirname, "entry"), 243 | output: { 244 | path: __dirname, 245 | filename: "output.js", 246 | }, 247 | plugins: [ 248 | new webpack.BannerPlugin({ 249 | banner: `/*! ${config.pkg.copyright} - Version ${ 250 | config.pkg.version 251 | } dated ${grunt.template.today()} */`, 252 | raw: true, 253 | }), 254 | ], 255 | }), 256 | }, 257 | }); 258 | 259 | grunt.loadNpmTasks("grunt-webpack"); 260 | }; 261 | ``` 262 | 263 | ### Webpack-dev-server 264 | 265 | #### imported config 266 | 267 | This is a simple example that requires the webpack config from the config file. Read how to configure webpack-dev-server in the [webpack-dev-server documentation][webpack-dev-server-config]. 268 | 269 | ```javascript 270 | const webpackConfig = require("./webpack.config.js"); 271 | 272 | module.exports = function (grunt) { 273 | grunt.initConfig({ 274 | "webpack-dev-server": { 275 | dev: webpackConfig, 276 | }, 277 | }); 278 | 279 | grunt.loadNpmTasks("grunt-webpack"); 280 | }; 281 | ``` 282 | 283 | > The webpack-dev-server task in this example has one target called `dev`. They can be renamed to everything besides `options`. See the [grunt docs][grunt-targets] for more information about targets. 284 | 285 | On the command line you can then do the following. 286 | 287 | ```bash 288 | 289 | # Run webpack-dev-server with the `dev` target 290 | > grunt webpack-dev-server:dev 291 | 292 | # Run webpack for all possible targets 293 | > grunt webpack-dev-server 294 | ``` 295 | 296 | > For more examples and information have a look at the [webpack documentation]5] which mostly also applies here besides the noted differences above. 297 | 298 |

Troubleshooting

299 | 300 | ### Circular reference detected (.plugins) 301 | 302 | If you encounter this message it means that one of the plugins which are present in your config have circular references. 303 | This is not a bug in the plugin and is totally fine for webpack, but sadly grunt cannot handle these. 304 | 305 | To workaround this problem use lazy config loading, by wrapping your config inside a function. 306 | 307 | ```js 308 | const webpackConfig = require("./webpack.config.js"); 309 | 310 | module.exports = function (grunt) { 311 | grunt.initConfig({ 312 | webpack: { 313 | myconfig: () => webpackConfig, 314 | }, 315 | }); 316 | 317 | grunt.loadNpmTasks("grunt-webpack"); 318 | }; 319 | ``` 320 | 321 | [grunt-template]: http://gruntjs.com/api/grunt.template 322 | [webpack-config]: https://webpack.js.org/configuration/ 323 | [webpack-dev-server-config]: https://webpack.js.org/configuration/dev-server/ 324 | [grunt-targets]: https://gruntjs.com/configuring-tasks#task-configuration-and-targets 325 | [webpack-start]: https://webpack.js.org/guides/get-started/ 326 | [npm]: https://img.shields.io/npm/v/grunt-webpack.svg 327 | [npm-url]: https://npmjs.com/package/grunt-webpack 328 | --------------------------------------------------------------------------------