Differential serving with webpack encore
Recently we’ve been looking into differential serving. With our set up there were a few complications, so in this post i’ll share how we overcame those, and how to set up differential serving with webpack encore.
If you want an explanation of what differential serving is, or how to do with with a normal webpack setup, i suggest reading this post.
In our project we use webpack encore. We use a babel.config.json
to manage our babel settings, as jest can’t read that info from the webpack encore file.
We also have a .browserslistrc
file, in which we configure what browsers we support.
In order to make this work with webpack encore we need to do a few things. First we need to split up our webpack into 3 configs.
We need our ‘base’ config. I’ll call this webpack.base.config.js
. This contains all our webpack configuration, except for setOutputPath
and setPublicPath
. We’ll export the Encore object, instead of the getWebpackConfig
. So rename your webpack.config.js
and remove those lines.
I’ve taken the symfony demo project as an example, so the webpack.base.config.js
will look like this:
const Encore = require('@symfony/webpack-encore');
Encore
.addEntry('app', './assets/app.js')
.addEntry('login', './assets/login.js')
.addEntry('admin', './assets/admin.js')
.addEntry('search', './assets/search.js')
.enableStimulusBridge('./assets/controllers.json')
.splitEntryChunks()
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.enableSassLoader()
.enableIntegrityHashes(Encore.isProduction())
.autoProvidejQuery()
.autoProvideVariables({
"window.Bloodhound": require.resolve('bloodhound-js'),
"jQuery.tagsinput": "bootstrap-tagsinput"
})
;
module.exports = Encore;
Now, we need the ’legacy’ and the ‘modern’ builds which will support all the older browsers. For that we create a webpack.legacy.config.js
and a webpack.modern.config.js
with the following content.
The name is set so we can refer to that later in our config file.
// webpack.legacy.config.js
const Encore = require('./webpack.base.config');
process.env.BROWSERSLIST_ENV = 'legacy';
Encore
.setOutputPath('public/legacy-build/')
.setPublicPath('/legacy-build')
;
const config = Encore.getWebpackConfig();
config.name = 'legacy';
module.exports = config;
// webpack.modern.config.js
const Encore = require('./webpack.base.config');
process.env.BROWSERSLIST_ENV = 'modern';
Encore
.setOutputPath('public/modern-build/')
.setPublicPath('/modern-build')
;
const config = Encore.getWebpackConfig();
config.name = 'modern';
module.exports = config;
The key here is the BROWSERSLIST_ENV
that is being set. As now we’ll edit our .browserslistrc
file. Instead of having the config in a top level in this file, we have 2 versions, the legacy and the modern. By setting the BROWSERSLIST_ENV
environment variable to either, we tell browserlist which one to use.
This is also the reason we need to have these 2 in separate files. Otherwise the env variables will override each other.
# .browserslistrc
[legacy]
Last 2 versions
IE >= 11
not IE 10
not ie_mob 10
[modern]
chrome >= 80
firefox >= 80
safari >= 12
Now we’ll need to edit our package.json file to build both at the same time. So we change our scripts
part to the following:
"scripts": {
"dev-server": "encore dev-server -c webpack.modern.config.js & encore dev-server -c webpack.legacy.config.js",
"dev": "encore dev -c webpack.modern.config.js & encore dev -c webpack.legacy.config.js & wait",
"watch": "encore dev --watch -c webpack.modern.config.js & encore dev --watch -c webpack.legacy.config.js",
"build": "encore production --progress -c webpack.modern.config.js & encore production --progress -c webpack.legacy.config.js & wait"
}
By using &
and wait
we run both processes at the same time, instead of one by one. You can also use npm-run-all
or something similar.
Next up we’ll change our config/packages/webpack_encore.yaml
to tell it about these two builds. Now we can refer to those builds when rendering our tags.
webpack_encore:
output_path: false
builds:
legacy: '%kernel.project_dir%/public/legacy-build'
modern: '%kernel.project_dir%/public/modern-build'
However, it may be cumbersome to remember to call encore_entry_script_tags
twice. Once for legacy, and once for modern. And then also remember to give it the correct attributes. (nomodule
and type=module
). So instead lets create some twig functions to always render both.
Now, instead of calling encore_entry_script_tags
we call script_tags
, which will render both script tags.
And after we’ve replaced the encore_entry_script_tags
, we have differential serving.
<?php
namespace App\Twig;
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class EncoreExtension extends AbstractExtension
{
public function __construct(
private TagRenderer $tagRenderer
) {}
public function getFunctions(): array
{
return [
new TwigFunction('script_tags', [$this, 'legacyEntry'], ['is_safe' => ['html']]),
new TwigFunction('link_tags', [$this, 'linkEntry'], ['is_safe' => ['html']]),
];
}
public function legacyEntry($name): string
{
return $this->tagRenderer->renderWebpackScriptTags($name, null, 'legacy', ['nomodule' => null]) .
$this->tagRenderer->renderWebpackScriptTags($name, null, 'modern', ['type' => 'module']);
}
public function linkEntry(string $name): string
{
return $this->tagRenderer->renderWebpackLinkTags($name, null, 'legacy');
}
}
If you want to see all the changes, you can check this commit