Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Mix / Webpack environment dependent variable for client code

I'm looking for a way to build my assets using Laravel Mix on my dev machine which will then match the parameters on the production.

For instance, I have a base url for my API which is http://foo.test/api/v1 for local development and https://foo.com/api/v1 on my production server.

So in my client code (e.g. http.js) I would like to do something like this:

Vue.axios.defaults.baseURL = API_BASE_URL;

Now API_BASE_URLshould get replaced with http://foo.test/api/v1 when I run npm run dev or with https://foo.com/api/v1 when I run npm run prod.

I already tried the following approach which does not work.

webpack.mix.js

mix.webpackConfig(webpack => {
   return {
      plugins: [
        new webpack.DefinePlugin({
            API_BASE_URL: JSON.stringify('https://foo.com/api/v1'),
        })
      ]
   }
});

Then in http.js

Vue.axios.defaults.baseURL = API_BASE_URL;

This compiles to:

Object({"NODE_ENV":"production"}).API_BASE_URL;

I tried this approach with process.env.API_BASE_URL as well.

like image 786
Rico Leuthold Avatar asked Dec 01 '22 10:12

Rico Leuthold


1 Answers

It's a bit late, and I don't know if it still applies, but we're probably not the only ones looking for this. I've spend a lot of time on this, trying to figuring out that exact question. I'll share my story here, hoping that other people can avoid the loss of time.

What is passed to your client side application is basically the contents of process.env. Laravel Mix tries to protect you from exposing the entire .env (that gets loaded via dotenv) by filtering the variables and allowing only those that start with MIX_. This behavior comes from the MixDefinitionsPlugin.

When the webpack configuration is assembled it places your custom plugins first, followed by the mix plugins. That means the MixDefinitionsPlugin will always have the last word.

If you can work with that limitation, then a simple solution would be:

Add the following in your .env:

MIX_API_URL=http://foo.test/api/v1

When you recompile your assets you'll have a process.env.MIX_API_URL on the client side. It's as simple as that.

Btw, since this loads using dotenv-expand you can even use other variables (ie if your api was already set for the backend):

MIX_API_URL=${API_URL}

If you want to do this for your production variant, the least amount of hassle would be (temporarily) replace your .env file with a production one. Just create a shell script to build your production variant:

  • Replace the development .env with your production .env.
  • Run npm run production.
  • Restore your dev .env

Done. There is a 2nd solution, it's a lot uglier, but I like it more, read on :)


Still here?

I wasn't really happy with that as I just wanted to be able to run npm run production and it would work, so I looked further.

Since only the last defined process.env is used when the javascript transpiles, the plugin closest to that wins. As mentioned above, this is the MixDefinitionsPlugin and there is nothing you can do about it.

If looked into several other ways of accomplishing what we want

Adding your own after the MixDefinitionsPlugin?

(Besides the script replacing the .env, this is the only viable solution I could find)

Mix emits a "loading-plugins" event with the plugins as parameter right after it finalised their loading. This means that you'll get a set of very prepared and very pretty plugins that are about to go to webpack.

Initially I tried to push a plugin at the end of the array, but that one is only executed after the transpilation so that gains you nothing. Alternative would be to figure out where the plugin is situated and work with that. If you can modify its contents there you could in theory add values "on the fly" and those will end up in your browser as well. Which is what I did.

Basically this boils down to the following:

if (mix.inProduction()) {
  Mix.listen('loading-plugins', plugins => {
    const _ = require('lodash');
    let define = _.find(plugins, plugin => {return plugin.constructor.name === 'DefinePlugin'});
    define.definitions['process.env'].MIX_API_URL = 'http://foo.com/api/v1';
  });
}

(Note that we're looking for a DefinePlugin here instead of the MixDefinitionsPlugin, as that already did its work)

Since you can add what you need here without the need for the MIX_-prefix, you could alternatively set your API_URL here for both:

Mix.listen('loading-plugins', plugins => {
  const _ = require('lodash');
  let define = _.find(plugins, plugin => {return plugin.constructor.name === 'DefinePlugin'});
  define.definitions['process.env'].API_URL = mix.inProduction() ?
    'http://foo.com/api/v1' : 'http://foo.test/api/v1';
});    

You could also load another .env file here (via dotenv.config()) and merge it into the definitions, the sky is the limit.

A different name for the .env?

Something else I thought about.

The MixDefinitionsPlugin constructor allows for this possibility (function MixDefinitionsPlugin(envPath)), unfortunately the actual implementation does not:

return new webpack.DefinePlugin(
  new MixDefinitionsPlugin().getDefinitions(merge)
);

So no such luck, this is unusable.

Different folder?

Next up was checking if I could perhaps create a different folder with a production .env file.

Mix has a Paths object that stores the root. So the idea here was to store my alternative .env in a subdirectory, update the root and presto.

Mix.paths.setRootPath(Mix.paths.root('./resources/assets/production'));

This works for the .env, but it breaks a lot of other things. The root is used as the context for webpack, as configuration for the purify-css plugin (css cleaning of vue and blade files) and most importantly it also breaks the url loader. No go here as well (unless you want to intercept the webpack configuration and restore the root everywhere, not worth it)

So here ends the story of about 5 hours of my life. I'm nowhere near a javascript expert, so perhaps I've made som errors or missed things, but it's my story :)

like image 80
Blizz Avatar answered Dec 05 '22 19:12

Blizz