Laravel Mix build process scalable up to lots of (50+) themes

According to the documentation, you can use environment variables to inject parameters into Laravel Mix. Combine this with a custom script in your package.json and you get what you want.

The default package.json comes with the following two scripts (and more):

"scripts": {
    "dev": "npm run development",
    "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
}

Simply add your own script as a wrapper:

"scripts": {
    "dev": "npm run development",
    "dev:customer1": "cross-env MIX_CUSTOMER=customer1 npm run dev"
}

Surely this works also with yarn...

In your webpack.mix.js, you can then simply query the environment variable to only build a single customer:

const mix = require('laravel-mix');

// Parent Theme
mix.js('resources/[parent-theme-folder]/assets/js/app.js', 'public/[parent-theme-folder]/js/')
  .sass('resources/[parent-theme-folder]/assets/scss/app.scss', 'public/[parent-theme-folder]/css/');

if (process.env.MIX_CUSTOMER === 'customer1') {
    mix.js('resources/[child-theme-1-folder]/assets/js/app.js', 'public/[child-theme-1-folder]/js/')
      .sass('resources/[child-theme-1-folder]/assets/scss/app.scss', 'public/[child-theme-1-folder]/css/');
}

if (process.env.MIX_CUSTOMER === 'customer2') {
    mix.js('resources/[child-theme-2-folder]/assets/js/app.js', 'public/[child-theme-2-folder]/js/')
      .sass('resources/[child-theme-2-folder]/assets/scss/app.scss', 'public/[child-theme-2-folder]/css/');
}

To simplify your webpack.mix.js, you can also do the following when using an environment variable:

const mix = require('laravel-mix');

// Parent Theme
mix.js('resources/[parent-theme-folder]/assets/js/app.js', 'public/[parent-theme-folder]/js/')
  .sass('resources/[parent-theme-folder]/assets/scss/app.scss', 'public/[parent-theme-folder]/css/');

function buildCustomerAssets(customerFolder)
{
    mix.js(`resources/${customerFolder}/assets/js/app.js', 'public/${customerFolder}/js/')
      .sass('resources/${customerFolder}/assets/scss/app.scss', 'public/${customerFolder}/css/');
}

var customers = {
    'customer1': 'child-theme-1-folder',
    'customer2': 'child-theme-2-folder',
};

if (process.env.MIX_CUSTOMER) {
    var customerFolder = customers[process.env.MIX_CUSTOMER];
    buildCustomerAssets(customerFolder);
} else {
    for (const customerFolder of Object.values(customers)) {
        buildCustomerAssets(folder);
    }
}

Because your parent theme uses the same folder structure, you could even compile it using the function. But all of this makes only sense if your build config is exactly the same for all customers.


I ended up going with the technique outlined in lots of detail in the compulsivecoders article, but with a few personal tweaks. I won't rehash their entire article and explain each line, but I'll include every line of code you'll need to duplicate my setup below.

--

First, run npm install laravel-mix-merge-manifest. This part is required if you want to use mix() in your views to find your css/js assets and enable cache-busting. Otherwise, every time you run npm run dev/watch/etc --theme=theme-name it's gonna overwrite the previous theme in your mix-manifest.json file and you'll get Laravel errors that it can't find your assets. You'll actually use this package in your theme's mix files at the end.

Then, delete everything in your webpack.mix.js file in your root and paste this:

// webpack.mix.js
try {
    require(`${__dirname}/webpack/webpack.${process.env.npm_config_theme}.mix.js`)
} catch (ex) {
    console.log(
        '\x1b[41m%s\x1b[0m',
        'Provide correct --theme argument to build, e.g.: `npm run watch --theme=theme1` or `npm run dev --theme=theme2`'
    )
    throw new Error('Provide correct --theme argument to build: `npm run watch --theme=theme1` or `npm run dev --theme=theme2`')
}
// ...that's it. Nothing is ever managed in this file.

I tweaked this from the example in the article. I didn't like using an if() to search an array of all of your theme names because that was one more thing to have to update when you add/remove themes, so I'm just using try {} which will output an error if it fails to find that theme's webpack.mix file. You could probably write something that would find and loop all your mix files and build everything at once, but that's not necessary in my use case, or performant when I have as many themes as I do.

Then create a /webpack/webpack.[theme].mix.js file for every theme. I put mine in a /webpack/ folder just because I didn't want to have 50+ of these files in my root. Note the /webpack/ folder is referenced in the 3rd line of the webpack.mix.js file above, so change that reference if you want it somewhere else. Here's an example of one of mine, but yours will obviously vary.

// /webpack/webpack.my-theme-1.mix.js
const mix = require('laravel-mix');
require('laravel-mix-merge-manifest');

mix.js('resources/my-theme-1/assets/js/app.js', 'public/my-theme-1/js/').sourceMaps().version()
   .sass('resources/my-theme-1/assets/scss/app.scss', 'public/my-theme-1/css/').sourceMaps().version()
   .mergeManifest();

Note the two references to the merge manifest. Those two things are required in each theme.

Finally, you can run all of your usual build commands by passing in a --theme=[theme-name] argument, e.g.:

npm run dev --theme=my-theme-1
npm run watch --theme=my-theme-2
npm run prod --theme=my-parent-theme-1

I personally split my terminal and run watch on both my child and parent themes as I'm starting to build these out, but once my parent theme is stable I'll just have to watch my child themes.

Everything's working great so far, but I'm open to critique. Thanks to everyone for their help getting me on the right path.