Skip to content

Commit 7436a0d

Browse files
committed
feature #1336 Make webpack-dev-server optional (Kocal)
This PR was merged into the main branch. Discussion ---------- Make webpack-dev-server optional As discussed with `@stof`, we want to make the webpack-dev-server an optional peer dependency: JavaScript dependencies are problematic, depending on many sub-dependencies, which in turn depend on more sub-dependencies, and so on... Welcome to the dependency hell! Even if the dev-server functionality isn't used, the dependency tree is immensely more complex (over 300 additional dependencies), but this is an open door to security holes present in “discrete” (say “little-known”), but over-used dependencies. In recent months, a sort of “witch-hunt” has been set up by some people in the JavaScript ecosystem, to replace sub-dependencies with lighter alternatives (either another dependency, or a native version) in _popular project_. I've started doing this on Encore for a few dependencies, and making the webpack-dev-server optional is a big win. ``` Package size report =================== Package info for "`@symfony`/webpack-encore@4.7.0": 61 MB Released: 2024-08-29 16:26:01.762 +0000 UTC (1w3d ago) Downloads last week: 19,232 (11.57%) Estimated traffic last week: 1.2 TB Subdependencies: 626 Removed dependencies: - webpack-dev-server@4.15.2: 29 MB (47.60%) Downloads last week: 2,605,016 (N/A% from 4.15.2) Downloads last week from "`@symfony`/webpack-encore@4.7.0": 19,232 (N/A%) Traffic last week: N/A Traffic from "`@symfony`/webpack-encore@4.7.0": 1.2 TB (N/A%) Subdependencies: 283 (45.20%) Estimated new statistics: Package size: 61 MB → 43 MB (69.83%) Subdependencies: 626 → 295 (-331) Traffic with last week's downloads: For current version: 1.2 TB → 823 GB (355 GB saved) For all versions: 10 TB → 7.1 TB (3.1 TB saved) ``` When upgrading Encore to v5, end-users will have to install the `webpack-dev-server` back to use it again. Commits ------- 600c943 Make webpack-dev-server optional
2 parents 9f31ac1 + 600c943 commit 7436a0d

File tree

8 files changed

+190
-8
lines changed

8 files changed

+190
-8
lines changed

.github/workflows/low-depends.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ jobs:
3030

3131
- name: Force Lowest Dependencies
3232
run: node ./scripts/force-lowest-dependencies
33+
34+
# We have some tests that need to "git checkout" package.json file.
35+
# Commit them prevent the tests from re-using the locked dependencies.
36+
- name: Commit Changes, to preserve a clean working directory
37+
run: |
38+
git config --global user.email ""
39+
git config --global user.name "Symfony"
40+
git commit -am "Force Lowest Dependencies"
3341
3442
- name: Install Yarn Dependencies
3543
run: yarn install

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ Encore.configureDevServerOptions((options) => {
7272
});
7373
```
7474

75+
* #1336 Make webpack-dev-server optional (@Kocal)
76+
77+
The `webpack-dev-server` package is now an optional peer dependency.
78+
It has been removed because some projects may not use it, and it was installing a bunch of unnecessary dependencies.
79+
80+
Removing the `webpack-dev-server` dependency from Encore reduces the number of dependencies from **626** to **295** (**-331**!),
81+
it helps to reduce the size of the `node_modules` directory and the number of possible vulnerabilities.
82+
83+
To use the `webpack-dev-server` again, you need to install it manually:
84+
```shell
85+
npm install webpack-dev-server --save-dev
86+
# or
87+
yarn add webpack-dev-server --dev
88+
# or
89+
pnpm install webpack-dev-server --save-dev
90+
```
91+
7592
## 4.7.0
7693

7794
### Features

bin/encore.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const parseRuntime = require('../lib/config/parse-runtime');
1414
const context = require('../lib/context');
1515
const pc = require('picocolors');
1616
const logger = require('../lib/logger');
17+
const featuresHelper = require("../lib/features");
1718

1819
const runtimeConfig = parseRuntime(
1920
require('yargs-parser')(process.argv.slice(2)),
@@ -56,6 +57,13 @@ if (runtimeConfig.helpRequested) {
5657
}
5758

5859
if (runtimeConfig.useDevServer) {
60+
try {
61+
featuresHelper.ensurePackagesExistAndAreCorrectVersion('webpack-dev-server', 'the webpack Development Server');
62+
} catch (e) {
63+
console.log(e);
64+
process.exit(1);
65+
}
66+
5967
console.log('Running webpack-dev-server ...');
6068
console.log();
6169

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ class Encore {
726726
* });
727727
* ```
728728
*
729-
* @param {OptionsCallback<import('webpack-dev-server').Configuration>} callback
729+
* @param {OptionsCallback<object>} callback
730730
* @returns {Encore}
731731
*/
732732
configureDevServerOptions(callback) {

lib/WebpackConfig.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const crypto = require('crypto');
3131
const logger = require('./logger');
3232
const regexpEscaper = require('./utils/regexp-escaper');
3333
const { calculateDevServerUrl } = require('./config/path-util');
34+
const featuresHelper = require('./features');
3435

3536
/**
3637
* @param {RuntimeConfig|null} runtimeConfig
@@ -165,7 +166,7 @@ class WebpackConfig {
165166
this.splitChunksConfigurationCallback = () => {};
166167
/** @type {OptionsCallback<Exclude<webpack.Configuration['watchOptions'], undefined>>} */
167168
this.watchOptionsConfigurationCallback = () => {};
168-
/** @type {OptionsCallback<import('webpack-dev-server').Configuration>} */
169+
/** @type {OptionsCallback<object>} */
169170
this.devServerOptionsConfigurationCallback = () => {};
170171
/** @type {OptionsCallback<object>} */
171172
this.vueLoaderOptionsCallback = () => {};
@@ -591,9 +592,11 @@ class WebpackConfig {
591592
}
592593

593594
/**
594-
* @param {OptionsCallback<import('webpack-dev-server').Configuration>} callback
595+
* @param {OptionsCallback<object>} callback
595596
*/
596597
configureDevServerOptions(callback) {
598+
featuresHelper.ensurePackagesExistAndAreCorrectVersion('webpack-dev-server');
599+
597600
if (typeof callback !== 'function') {
598601
throw new Error('Argument 1 to configureDevServerOptions() must be a callback function.');
599602
}

lib/features.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ const packageHelper = require('./package-helper');
1414
/**
1515
* An object that holds internal configuration about different
1616
* "loaders"/"plugins" that can be enabled/used.
17+
*
18+
* @type {{[key: string]: {
19+
* method: string,
20+
* packages: Array<{ name: string, enforce_version?: boolean } | Array<{ name: string }>>,
21+
* description: string,
22+
* }}}
1723
*/
1824
const features = {
1925
sass: {
@@ -146,7 +152,14 @@ const features = {
146152
{ name: 'svelte-loader', enforce_version: true }
147153
],
148154
description: 'process Svelte JS files'
149-
}
155+
},
156+
'webpack-dev-server': {
157+
method: 'configureDevServerOptions()',
158+
packages: [
159+
{ name: 'webpack-dev-server' }
160+
],
161+
description: 'run the Webpack development server'
162+
},
150163
};
151164

152165
function getFeatureConfig(featureName) {
@@ -158,12 +171,12 @@ function getFeatureConfig(featureName) {
158171
}
159172

160173
module.exports = {
161-
ensurePackagesExistAndAreCorrectVersion: function(featureName) {
174+
ensurePackagesExistAndAreCorrectVersion: function(featureName, method = null) {
162175
const config = getFeatureConfig(featureName);
163176

164177
packageHelper.ensurePackagesExist(
165178
packageHelper.addPackagesVersionConstraint(config.packages),
166-
config.method
179+
method || config.method
167180
);
168181
},
169182

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
"tapable": "^2.2.1",
4343
"terser-webpack-plugin": "^5.3.0",
4444
"tmp": "^0.2.1",
45-
"webpack-dev-server": "^5.0.4",
4645
"yargs-parser": "^21.0.0"
4746
},
4847
"devDependencies": {
@@ -97,6 +96,7 @@
9796
"vue-loader": "^17.0.0",
9897
"webpack": "^5.72",
9998
"webpack-cli": "^5.1.4",
99+
"webpack-dev-server": "^5.0.4",
100100
"webpack-notifier": "^1.15.0"
101101
},
102102
"peerDependencies": {
@@ -211,6 +211,9 @@
211211
"webpack-cli": {
212212
"optional": false
213213
},
214+
"webpack-dev-server": {
215+
"optional": true
216+
},
214217
"webpack-notifier": {
215218
"optional": true
216219
}

test/bin/encore.js

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ const expect = chai.expect;
1515
const path = require('path');
1616
const testSetup = require('../helpers/setup');
1717
const fs = require('fs-extra');
18-
const exec = require('child_process').exec;
18+
const { exec, execSync, spawn } = require('child_process');
19+
20+
const projectDir = path.resolve(__dirname, '../', '../');
1921

2022
describe('bin/encore.js', function() {
2123
// being functional tests, these can take quite long
@@ -213,4 +215,132 @@ module.exports = Encore.getWebpackConfig();
213215
done();
214216
});
215217
});
218+
219+
it('Run the webpack-dev-server successfully', (done) => {
220+
testSetup.emptyTmpDir();
221+
const testDir = testSetup.createTestAppDir();
222+
223+
fs.writeFileSync(
224+
path.join(testDir, 'package.json'),
225+
`{
226+
"devDependencies": {
227+
"@symfony/webpack-encore": "*"
228+
}
229+
}`
230+
);
231+
232+
fs.writeFileSync(
233+
path.join(testDir, 'webpack.config.js'),
234+
`
235+
const Encore = require('../../index.js');
236+
Encore
237+
.enableSingleRuntimeChunk()
238+
.setOutputPath('build/')
239+
.setPublicPath('/build')
240+
.addEntry('main', './js/no_require')
241+
;
242+
243+
module.exports = Encore.getWebpackConfig();
244+
`
245+
);
246+
247+
const binPath = path.resolve(__dirname, '../', '../', 'bin', 'encore.js');
248+
const abortController = new AbortController();
249+
const node = spawn('node', [binPath, 'dev-server', `--context=${testDir}`], {
250+
cwd: testDir,
251+
env: Object.assign({}, process.env, { NO_COLOR: 'true' }),
252+
signal: abortController.signal
253+
});
254+
255+
let stdout = '';
256+
let stderr = '';
257+
258+
node.stdout.on('data', (data) => {
259+
stdout += data.toString();
260+
});
261+
262+
node.stderr.on('data', (data) => {
263+
stderr += data.toString();
264+
});
265+
266+
node.on('error', (error) => {
267+
if (error.name !== 'AbortError') {
268+
throw new Error('Error executing encore', { cause: error });
269+
}
270+
271+
expect(stdout).to.contain('Running webpack-dev-server ...');
272+
expect(stdout).to.contain('Compiled successfully in');
273+
expect(stdout).to.contain('webpack compiled successfully');
274+
275+
expect(stderr).to.contain('[webpack-dev-server] Project is running at:');
276+
expect(stderr).to.contain('[webpack-dev-server] Loopback: http://localhost:8080/');
277+
expect(stderr).to.contain('[webpack-dev-server] Content not from webpack is served from');
278+
279+
done();
280+
});
281+
282+
setTimeout(() => {
283+
abortController.abort();
284+
}, 5000);
285+
});
286+
287+
describe('Without webpack-dev-server installed', () => {
288+
before(() => {
289+
execSync('yarn remove webpack-dev-server --dev', { cwd: projectDir });
290+
});
291+
292+
after(() => {
293+
// Re-install webpack-dev-server and ensure the project is in a clean state
294+
execSync('git checkout package.json', { cwd: projectDir });
295+
execSync('yarn install', { cwd: projectDir });
296+
});
297+
298+
it('Throw an error when trying to use the webpack-dev-server if not installed', done => {
299+
testSetup.emptyTmpDir();
300+
const testDir = testSetup.createTestAppDir();
301+
302+
fs.writeFileSync(
303+
path.join(testDir, 'package.json'),
304+
`{
305+
"devDependencies": {
306+
"@symfony/webpack-encore": "*"
307+
}
308+
}`
309+
);
310+
311+
fs.writeFileSync(
312+
path.join(testDir, 'webpack.config.js'),
313+
`
314+
const Encore = require('../../index.js');
315+
Encore
316+
.enableSingleRuntimeChunk()
317+
.setOutputPath('build/')
318+
.setPublicPath('/build')
319+
.addEntry('main', './js/no_require')
320+
;
321+
322+
module.exports = Encore.getWebpackConfig();
323+
`
324+
);
325+
326+
const binPath = path.resolve(projectDir, 'bin', 'encore.js');
327+
exec(
328+
`node ${binPath} dev-server --context=${testDir}`,
329+
{
330+
cwd: testDir,
331+
env: Object.assign({}, process.env, { NO_COLOR: 'true' })
332+
},
333+
(err, stdout, stderr) => {
334+
expect(stdout).to.contain('Install webpack-dev-server to use the webpack Development Server');
335+
expect(stdout).to.contain('npm install webpack-dev-server --save-dev');
336+
expect(stderr).to.equal('');
337+
338+
expect(stdout).not.to.contain('Running webpack-dev-server ...');
339+
expect(stdout).not.to.contain('Compiled successfully in');
340+
expect(stdout).not.to.contain('webpack compiled successfully');
341+
342+
done();
343+
});
344+
});
345+
});
216346
});

0 commit comments

Comments
 (0)