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

2

6

Is there a standard way to scale Laravel Mix to support a build process of 50+ different frontend themes?

I'm simply running npm run dev and npm run watch currently with four different themes in my webpack.mix.js file, and that goes through and builds/watches all of those themes at once, but I fear performance is going to break down when we go to scale. Ideally I would like to be able to only build/watch themes one at a time, e.g. npm run dev --theme:some-site or npm run watch --theme:another-site

Here's what my webpack.mix.js is eventually going to look like at this rate if I don't change anything:

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/')
  ;

// Client 1
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/')
  ;
// Client 2
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/')
  ;
...etc...
// Client 47
mix.js('resources/[child-theme-47-folder]/assets/js/app.js', 'public/[child-theme-47-folder]/js/')
  .sass('resources/[child-theme-47-folder]/assets/scss/app.scss', 'public/[child-theme-47-folder]/css/')
  ;

Any suggestions? Thanks!

--

Some more info on our setup, if it helps...

We're using a parent/child theme package - igaster/laravel-theme - to manage multiple frontends in one Laravel project. Essentially, we have 4 types of website products, and 20-30 clients that all have their own instance of up to 4 of those products. They each have their own child theme that extends the parent theme for that product to add any custom layouts, styles, views, etc. We felt one Laravel project was going to be easier to manage than setting up 20-30 different Laravel projects for each client, especially when it came to managing maintenance and updates.

Pelops answered 18/9, 2019 at 20:48 Comment(2)
You might want to check this out compulsivecoders.com/tech/…. Search for "Split your build into multiple mix files" within the documentBrian
Thank you! I admit a search did bring me to that page earlier, and I think that will work for me, but that was the only article I'd seen on the topic. It surprised me that multiple themes in one Laravel project wasn't a more common issue (thinking multi-tenancy), so I mostly wanted to check with the community to make sure I wasn't missing anything more obvious.Pelops
P
2

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.

Pelops answered 20/9, 2019 at 18:52 Comment(0)
O
3

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.

Orobanchaceous answered 19/9, 2019 at 5:10 Comment(4)
Thank you so much for the detailed example! I've got some options to consider between this and the compulsivecoders.com/tech/… article that @Brian provided. Yours seems a lot simpler to set up with a much shorter/simpler npm script, but I do like how the latter breaks it apart into separate webpack.{app}.mix.js files to manage, though I'm not sure why. Maybe easier to manage/archive things, but probably just preference. I'll start with yours and see how it goes. Thanks!Pelops
Glad it helped. To improve on it, you could also add an env variable to directory name map in your webpack config and reduce the repeating config significantly.Orobanchaceous
@Pelops I added an improved example to my answer. It will scale very nicely.Orobanchaceous
Thanks so much. The reusable build function is a neat trick that I'm gonna try to remember for later if I have use for it, but unfortunately my builds can occasionally vary because we'll customize just about anything for the client if they're paying us. Perhaps I could use that for all of my standard builds and then have a way to add additional info for custom builds later. I did get something else working that's similar to the compulsivecoders article, but tweaked a bit for my use case. I think I'll document that in a new answer in case it's helpful.Pelops
P
2

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.

Pelops answered 20/9, 2019 at 18:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.