How to use Webpack in a Laravel Project

jurjin

Giorgio Tempesta

Posted on April 16, 2024

How to use Webpack in a Laravel Project

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
Enter fullscreen mode Exit fullscreen mode

Then in your Webpack configuration you can add:

const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
Enter fullscreen mode Exit fullscreen mode

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,
      };
    },
  }),
]
Enter fullscreen mode Exit fullscreen mode

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,
        };
      },
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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',
},
Enter fullscreen mode Exit fullscreen mode

So the full optimization configuration would be

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

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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then link the JavaScript files:

@foreach(app_assets('js') as $link)
  <script src="{{secure_asset($link)}}" type="module" fetchpriority="high"></script>
@endforeach
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

// ...

plugins: [
  new BundleAnalyzerPlugin({
    analyzerPort: 8000,
  }),
],
Enter fullscreen mode Exit fullscreen mode

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,
    },
  ],
},
Enter fullscreen mode Exit fullscreen mode

Here is a better explanation:

Joe Bell on Twitter / X

💖 💪 🙅 🚩
jurjin
Giorgio Tempesta

Posted on April 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related