Giorgio Tempesta
Posted on April 16, 2024
The problem
If you want to integrate an existing front-end project with Laravel, the suggested compilers are Laravel Mix or Vite. But if you’re already using Webpack, it’s not easy to integrate your bundled files with your application.
The solution
While looking at an application built with Create React App, I’ve found a comment in the configuration of the Webpack Manifest Plugin that gives a hint to a possible solution:
Generate an asset manifest file with the following content:
- "files" key: Mapping of all asset filenames to their corresponding output file so that tools can pick it up without having to parse
index.html
- "entrypoints" key: Array of files which are included in
index.html
, can be used to reconstruct the HTML if necessary
So basically they're saying that the Webpack Manifest Plugin is able to generate a manifest.json
file which can be read and used to link the correct entry points. I've done a couple of experiments and this is true: we can load the full application only by linking the files under the entrypoints
key.
How to generate the manifest
In your application, install webpack-manifest-plugin
.
npm install -D webpack-manifest-plugin
Then in your Webpack configuration you can add:
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
Then, add the plugin to your plugins array like so:
// ...
plugins: [
new WebpackManifestPlugin({
fileName: 'manifest.json',
publicPath: '/',
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.app.filter(fileName => !fileName.endsWith('.map'));
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
]
This will generate a manifest.json
file in your output directory: this only works if your entry point is called “app”: if that’s not the case, update the value after entrypoints on line 10 of the previous snippet. You can even pass the names of the manifest and of the entrypoint as parameters of a function, like this:
const generateManifest = ({ manifestName, entryPointName}) => ({
plugins: [
new WebpackManifestPlugin({
fileName: manifestName,
publicPath: '/',
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints[entryPointName].filter(fileName => !fileName.endsWith('.map'));
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
],
});
The output
Now your folder will have a manifest.json
file that looks something like this.
{
"files": {
"app.js": "/js/app.7f590a830debb2472a38.js",
// ...
"js/8624.js": "/js/8624.5bd57e8713fca6a73d59.js",
"js/6267.js": "/js/6267.0719662d3c1e2750b716.js",
"css/5588.chunk.css": "/css/5588.0593ac1b5d5e26a99fea.chunk.css",
"css/9140.chunk.css": "/css/9140.f16549164f98ea2e3c90.chunk.css",
"build-assets/fonts/fa-light-300.svg": "/build-assets/fonts/fa-light-300.76f70e6c0db0270aebcc..svg",
"build-assets/fonts/fa-solid-900.svg": "/build-assets/fonts/fa-solid-900.ceb187c9cc886c93094c..svg"
},
"entrypoints": [
"css/1916.8bdc4cbec28b4d86e7d4.css",
"js/app.60181d537bc41e03359f.js"
]
}
Under files
you have all the files generated by your application, while inside entrypoints
you can find the only files needed by your index.html
(an actual file or a generated one, as in our case) to make the application work, since all the other files are created on compile time, and imported in the page by the runtime code injected by Webpack.
Optimize the output
In order to reduce code duplication and speed up the runtime execution, Webpack gives us a couple of configurations, namely runtimeChunk
and splitChunks
.
runTimeChunk
creates a dedicated chunk with a static list of the dependencies needed by Webpack to build your application. This file tends to change less than others, so from a cache perspective it’s an improvement on the runtime speed (see here).
splitChunks
on the other hand reduces the total code size, by creating additional chunks with the common code needed by different parts of the application. After lots of tweaking, I’ve discovered that the best configuration is also the simplest (see here).
// ...
splitChunks: {
chunks: 'all',
},
So the full optimization configuration would be
// ...
optimization: {
splitChunks: {
chunks: 'all',
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
Now the manifest.json
file will have some more files:
{
"files": {
"app.js": "/js/app.7f590a830debb2472a38.js",
"runtime-app.js": "/js/runtime-app.ccc2d46925ce6635af17.js",
// ...
"js/8624.js": "/js/8624.5bd57e8713fca6a73d59.js",
"js/6267.js": "/js/6267.0719662d3c1e2750b716.js",
"css/5588.chunk.css": "/css/5588.0593ac1b5d5e26a99fea.chunk.css",
"css/9140.chunk.css": "/css/9140.f16549164f98ea2e3c90.chunk.css",
"build-assets/fonts/fa-light-300.svg": "/build-assets/fonts/fa-light-300.76f70e6c0db0270aebcc..svg",
"build-assets/fonts/fa-solid-900.svg": "/build-assets/fonts/fa-solid-900.ceb187c9cc886c93094c..svg"
},
"entrypoints": [
"js/runtime-app.ccc2d46925ce6635af17.js",
"css/1916.8bdc4cbec28b4d86e7d4.css",
"js/827.71f8c3e5e4f4b3b2861f.js",
"js/app.7f590a830debb2472a38.js"
]
}
Use the assets
At this point you should link the assets inside your webpage.
You should create a php function that lets you link css files inside a link tag and js files inside a script tag.
I’m going to start with just one function but in the end you can create your own class to manage your assets.
Inside helpers.php
you can write.
function app_assets($fileExtension) {
// find the path
$manifestPath = public_path('manifest.json');
if (!file_exists($manifestPath)) {
throw new \Exception("The manifest at $manifestPath does not exist.");
}
// create the associative array
$manifestValues = json_decode(file_get_contents($manifestPath), true);
$entryPoints = $manifestValues['entrypoints'];
// keep only files with the desired extension
$files = array_filter($entryPoints, function ($fileName) use ($fileExtension): bool {
return strpos($fileName, $value) !== false;
});
return $files;
}
Using this helper you can link your assets in the index page as you prefer.
In app.blade.php
link CSS files where you need them.
@foreach(app_assets('css') as $link)
<link rel="stylesheet" href="{{secure_asset($link)}}" type="text/css"/>
@endforeach
Then link the JavaScript files:
@foreach(app_assets('js') as $link)
<script src="{{secure_asset($link)}}" type="module" fetchpriority="high"></script>
@endforeach
Create the class
You can create a class to allow for a more flexible implementation.
<?php
namespace App\Helpers;
class ManifestAssets
{
/**
* @param string $extension
* @param string $manifestDirectory
* @param string $manifestFilename
* @return array
* @throws \Exception
*/
public function __invoke(string $extension, string $manifestDirectory, string $manifestFilename)
{
// you can specify a different directory
$manifestDir = !empty($manifestDirectory) ? "$manifestDirectory/" : "";
$manifestPath = public_path($manifestDir . $manifestFilename);
if (!file_exists($manifestPath)) {
throw new \Exception("The manifest at $manifestPath does not exist.");
}
// create the associative array
$manifest_values = json_decode(file_get_contents($manifestPath), true);
// you can add a prefix to files if needed
$entrypoints = $this->updatePaths($manifest_values['entrypoints'], "/$manifestDir");
// filter file extension
$files = $this->filterPaths($entrypoints, "/$extension/");
return $files;
}
protected function filterPaths($paths, $value): array {
// filter only the paths with the correct extension
return array_filter($paths, function ($filename) use ($value): bool {
return strpos($filename, $value) !== false;
});
}
protected function updatePaths($paths, $prefix): array {
// add a specific prefix to all paths
$updated_paths = [];
foreach ($paths as $path) {
$new_path = $prefix . $path;
$updated_paths[] = $new_path;
}
return $updated_paths;
}
}
Now you can change the code of your helper:
function app_assets($extension, $filename = 'asset-manifest.json') {
return app(\App\Helpers\ManifestAssets::class)($extension, '', $filename);
}
Bonus: Bundle Analyzr
If you want to make sure that your bundle is optimized you should check its structure from time to time using some kind of tool.
Install the library
npm install -D webpack-bundle-analyzer
Then add it to the plugins array in the Webpack configuration.
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
// ...
plugins: [
new BundleAnalyzerPlugin({
analyzerPort: 8000,
}),
],
This will help you figure out if everything works as expected. See here for more information: https://blog.jakoblind.no/webpack-bundle-analyzer/.
Second bonus: Turn off side effects
One of the things I discovered using the Webpack Analyzr plugin is that some libraries weren’t splitted from the main chunk as I was expecting.
This was due to the libraries imported in some modules that were imported using barrel files. The solution was to turn off side effects for the folders with the barrel files.
In the Webpack configuration, add this to your module setting:
module: {
rules: [
{
test: [/\/common\/index\.js$/, /\/common\/[^/]+\/index\.js$/],
sideEffects: false,
},
],
},
Here is a better explanation:
Posted on April 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.