diff --git a/.github/workflows/low-depends.yml b/.github/workflows/low-depends.yml index 2f3f0d11..20496879 100644 --- a/.github/workflows/low-depends.yml +++ b/.github/workflows/low-depends.yml @@ -30,6 +30,14 @@ jobs: - name: Force Lowest Dependencies run: node ./scripts/force-lowest-dependencies + + # We have some tests that need to "git checkout" package.json file. + # Commit them prevent the tests from re-using the locked dependencies. + - name: Commit Changes, to preserve a clean working directory + run: | + git config --global user.email "" + git config --global user.name "Symfony" + git commit -am "Force Lowest Dependencies" - name: Install Yarn Dependencies run: yarn install diff --git a/CHANGELOG.md b/CHANGELOG.md index add6ace9..f6af9886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,23 @@ Encore.configureDevServerOptions((options) => { }); ``` +* #1336 Make webpack-dev-server optional (@Kocal) + +The `webpack-dev-server` package is now an optional peer dependency. +It has been removed because some projects may not use it, and it was installing a bunch of unnecessary dependencies. + +Removing the `webpack-dev-server` dependency from Encore reduces the number of dependencies from **626** to **295** (**-331**!), +it helps to reduce the size of the `node_modules` directory and the number of possible vulnerabilities. + +To use the `webpack-dev-server` again, you need to install it manually: +```shell +npm install webpack-dev-server --save-dev +# or +yarn add webpack-dev-server --dev +# or +pnpm install webpack-dev-server --save-dev +``` + ## 4.7.0 ### Features diff --git a/bin/encore.js b/bin/encore.js index abf3fc25..d3dc3275 100755 --- a/bin/encore.js +++ b/bin/encore.js @@ -14,6 +14,7 @@ const parseRuntime = require('../lib/config/parse-runtime'); const context = require('../lib/context'); const pc = require('picocolors'); const logger = require('../lib/logger'); +const featuresHelper = require("../lib/features"); const runtimeConfig = parseRuntime( require('yargs-parser')(process.argv.slice(2)), @@ -56,6 +57,13 @@ if (runtimeConfig.helpRequested) { } if (runtimeConfig.useDevServer) { + try { + featuresHelper.ensurePackagesExistAndAreCorrectVersion('webpack-dev-server', 'the webpack Development Server'); + } catch (e) { + console.log(e); + process.exit(1); + } + console.log('Running webpack-dev-server ...'); console.log(); diff --git a/index.js b/index.js index 67c85814..a5538140 100644 --- a/index.js +++ b/index.js @@ -726,7 +726,7 @@ class Encore { * }); * ``` * - * @param {OptionsCallback} callback + * @param {OptionsCallback} callback * @returns {Encore} */ configureDevServerOptions(callback) { diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index 1d85e00a..0fb34d6b 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -31,6 +31,7 @@ const crypto = require('crypto'); const logger = require('./logger'); const regexpEscaper = require('./utils/regexp-escaper'); const { calculateDevServerUrl } = require('./config/path-util'); +const featuresHelper = require('./features'); /** * @param {RuntimeConfig|null} runtimeConfig @@ -165,7 +166,7 @@ class WebpackConfig { this.splitChunksConfigurationCallback = () => {}; /** @type {OptionsCallback>} */ this.watchOptionsConfigurationCallback = () => {}; - /** @type {OptionsCallback} */ + /** @type {OptionsCallback} */ this.devServerOptionsConfigurationCallback = () => {}; /** @type {OptionsCallback} */ this.vueLoaderOptionsCallback = () => {}; @@ -591,9 +592,11 @@ class WebpackConfig { } /** - * @param {OptionsCallback} callback + * @param {OptionsCallback} callback */ configureDevServerOptions(callback) { + featuresHelper.ensurePackagesExistAndAreCorrectVersion('webpack-dev-server'); + if (typeof callback !== 'function') { throw new Error('Argument 1 to configureDevServerOptions() must be a callback function.'); } diff --git a/lib/features.js b/lib/features.js index 86781cf7..fc0a1822 100644 --- a/lib/features.js +++ b/lib/features.js @@ -14,6 +14,12 @@ const packageHelper = require('./package-helper'); /** * An object that holds internal configuration about different * "loaders"/"plugins" that can be enabled/used. + * + * @type {{[key: string]: { + * method: string, + * packages: Array<{ name: string, enforce_version?: boolean } | Array<{ name: string }>>, + * description: string, + * }}} */ const features = { sass: { @@ -146,7 +152,14 @@ const features = { { name: 'svelte-loader', enforce_version: true } ], description: 'process Svelte JS files' - } + }, + 'webpack-dev-server': { + method: 'configureDevServerOptions()', + packages: [ + { name: 'webpack-dev-server' } + ], + description: 'run the Webpack development server' + }, }; function getFeatureConfig(featureName) { @@ -158,12 +171,12 @@ function getFeatureConfig(featureName) { } module.exports = { - ensurePackagesExistAndAreCorrectVersion: function(featureName) { + ensurePackagesExistAndAreCorrectVersion: function(featureName, method = null) { const config = getFeatureConfig(featureName); packageHelper.ensurePackagesExist( packageHelper.addPackagesVersionConstraint(config.packages), - config.method + method || config.method ); }, diff --git a/package.json b/package.json index 960bd079..58647bcc 100755 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "tapable": "^2.2.1", "terser-webpack-plugin": "^5.3.0", "tmp": "^0.2.1", - "webpack-dev-server": "^5.0.4", "yargs-parser": "^21.0.0" }, "devDependencies": { @@ -97,6 +96,7 @@ "vue-loader": "^17.0.0", "webpack": "^5.72", "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4", "webpack-notifier": "^1.15.0" }, "peerDependencies": { @@ -211,6 +211,9 @@ "webpack-cli": { "optional": false }, + "webpack-dev-server": { + "optional": true + }, "webpack-notifier": { "optional": true } diff --git a/test/bin/encore.js b/test/bin/encore.js index 8f0e8729..ba57590d 100644 --- a/test/bin/encore.js +++ b/test/bin/encore.js @@ -15,7 +15,9 @@ const expect = chai.expect; const path = require('path'); const testSetup = require('../helpers/setup'); const fs = require('fs-extra'); -const exec = require('child_process').exec; +const { exec, execSync, spawn } = require('child_process'); + +const projectDir = path.resolve(__dirname, '../', '../'); describe('bin/encore.js', function() { // being functional tests, these can take quite long @@ -213,4 +215,132 @@ module.exports = Encore.getWebpackConfig(); done(); }); }); + + it('Run the webpack-dev-server successfully', (done) => { + testSetup.emptyTmpDir(); + const testDir = testSetup.createTestAppDir(); + + fs.writeFileSync( + path.join(testDir, 'package.json'), + `{ + "devDependencies": { + "@symfony/webpack-encore": "*" + } + }` + ); + + fs.writeFileSync( + path.join(testDir, 'webpack.config.js'), + ` +const Encore = require('../../index.js'); +Encore + .enableSingleRuntimeChunk() + .setOutputPath('build/') + .setPublicPath('/build') + .addEntry('main', './js/no_require') +; + +module.exports = Encore.getWebpackConfig(); + ` + ); + + const binPath = path.resolve(__dirname, '../', '../', 'bin', 'encore.js'); + const abortController = new AbortController(); + const node = spawn('node', [binPath, 'dev-server', `--context=${testDir}`], { + cwd: testDir, + env: Object.assign({}, process.env, { NO_COLOR: 'true' }), + signal: abortController.signal + }); + + let stdout = ''; + let stderr = ''; + + node.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + node.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + node.on('error', (error) => { + if (error.name !== 'AbortError') { + throw new Error('Error executing encore', { cause: error }); + } + + expect(stdout).to.contain('Running webpack-dev-server ...'); + expect(stdout).to.contain('Compiled successfully in'); + expect(stdout).to.contain('webpack compiled successfully'); + + expect(stderr).to.contain('[webpack-dev-server] Project is running at:'); + expect(stderr).to.contain('[webpack-dev-server] Loopback: http://localhost:8080/'); + expect(stderr).to.contain('[webpack-dev-server] Content not from webpack is served from'); + + done(); + }); + + setTimeout(() => { + abortController.abort(); + }, 5000); + }); + + describe('Without webpack-dev-server installed', () => { + before(() => { + execSync('yarn remove webpack-dev-server --dev', { cwd: projectDir }); + }); + + after(() => { + // Re-install webpack-dev-server and ensure the project is in a clean state + execSync('git checkout package.json', { cwd: projectDir }); + execSync('yarn install', { cwd: projectDir }); + }); + + it('Throw an error when trying to use the webpack-dev-server if not installed', done => { + testSetup.emptyTmpDir(); + const testDir = testSetup.createTestAppDir(); + + fs.writeFileSync( + path.join(testDir, 'package.json'), + `{ + "devDependencies": { + "@symfony/webpack-encore": "*" + } + }` + ); + + fs.writeFileSync( + path.join(testDir, 'webpack.config.js'), + ` +const Encore = require('../../index.js'); +Encore + .enableSingleRuntimeChunk() + .setOutputPath('build/') + .setPublicPath('/build') + .addEntry('main', './js/no_require') +; + +module.exports = Encore.getWebpackConfig(); + ` + ); + + const binPath = path.resolve(projectDir, 'bin', 'encore.js'); + exec( + `node ${binPath} dev-server --context=${testDir}`, + { + cwd: testDir, + env: Object.assign({}, process.env, { NO_COLOR: 'true' }) + }, + (err, stdout, stderr) => { + expect(stdout).to.contain('Install webpack-dev-server to use the webpack Development Server'); + expect(stdout).to.contain('npm install webpack-dev-server --save-dev'); + expect(stderr).to.equal(''); + + expect(stdout).not.to.contain('Running webpack-dev-server ...'); + expect(stdout).not.to.contain('Compiled successfully in'); + expect(stdout).not.to.contain('webpack compiled successfully'); + + done(); + }); + }); + }); });