How to use Webpack in a Laravel Project
Tags: laravel, webpack, frontend, es6
Published: 2024-04-16 10:01:03

## 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](https://create-react-app.dev/), I’ve found a comment in the configuration of the [Webpack Manifest Plugin](https://www.npmjs.com/package/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`. 

```bash
npm install -D webpack-manifest-plugin
```

Then in your Webpack configuration you can add: 

```jsx
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
```

Then, add the plugin to your plugins array like so:

```jsx
// ...
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: 

```jsx
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. 

```json
{
  "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](https://webpack.js.org/configuration/optimization/#optimizationruntimechunk)).

`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](https://webpack.js.org/configuration/optimization/#optimizationsplitchunks)).

```jsx
// ...
splitChunks: {
  chunks: 'all',
},
```

So the full optimization configuration would be

```jsx
// ...
optimization: {
  splitChunks: {
    chunks: 'all',
  },
  runtimeChunk: {
    name: entrypoint => `runtime-${entrypoint.name}`,
  },
},
```

Now the `manifest.json` file will have some more files:

```json
{
  "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.

```php
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.

```php
@foreach(app_assets('css') as $link)
  <link rel="stylesheet" href="{{secure_asset($link)}}" type="text/css"/>
@endforeach
```

Then link the JavaScript files:

```php
@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
<?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: 

```php
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

```bash
npm install -D webpack-bundle-analyzer
```

Then add it to the plugins array in the Webpack configuration. 

```jsx
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:

```jsx
 module: {
  rules: [
    {
      test: [/\/common\/index\.js$/, /\/common\/[^/]+\/index\.js$/],
      sideEffects: false,
    },
  ],
},
```

Here is a better explanation: 

[Joe Bell on Twitter / X](https://twitter.com/joebell_/status/1554108255269699584)