javascriptviteesmodules

Laravel 10 and Vite - complicated JS importing (es module) request


This is an odd solution but for reasons too long to explain, it sort of needs to work this way. I've had it working with Webpack Mix, but since upgrading to Laravel 10 I really want to use Vite instead. I've not had success in getting it to work correctly though.

My APP.JS file imports (using ES module) another JS file (general-module) which holds a number of useful functions (I've simplified down to 2 functions for this example).

I store, for each of these imported functions, a pointer to the functions in app.js like so:

const helperLookup = {
    "dateFormat" : {},
    "validateFile" : {},
};    

 //Import general helper file
 import('../js/components/general-module.js')
 .then(module => {
    
            //create a "helperLookup" link to the each function
            helperLookup["dateFormat"] = module.dateFormat;
            helperLookup["validateFile"] = module.validateFile;

    });
}

This part works. And, through Vite's asset bundling the "../js/components/general.module.js" is replaced with "./build/general-module.js" because my Vite config file's "rollupOptions" is set up so that the chunkFileNames = the original name of the file (sadly without directory, but it's not a big issue).

Next I have another JS file (first-page.js) is loaded in to app.js (using esmodule importing again) which will need access to the "dateFormat" function from the helperLookup. It also imports it's own components, not part of the helperLookup.

My first-page.js file looks like this...

import {isAjaxWaiting,AjaxRequest} from '../components/ajax-module';

let dateFormat;
export function shareGlobalFunctions(_dateFormat){
    dateFormat = _dateFormat;
}
export default() => {
     console.log("I'm loaded");
}

...and my APP.JS file imports this file and passes in the "dateFormat" function like so:

import('../js/first-page.js')
.then( module => {    
       module.shareGlobalFunctions( helperLookup["dateFormat"] );
       module.default();
})

This also works. And, through Vite's asset bundling the "../js/first-page.js" is replaced with "./build/first-page.js".

All is working so far.

Now it gets more difficult, and I can't figure it out. I have many additional JS files, which are only required if a certain HTML page asks for it (just like first-page.js). I want app.js to import them dynamically after importing the other first 2 files. The way I do this is by including the filename as an inline JS object in the HTML/Blade page. Like so:

<script>
    document.js_arr = [
        {
            "file" : "example-module.js",
        }
    ];
</script>

My app.Js (after importing the previous 2 files) will check document.js_arr and if not empty will attempt to import whatever files are requested - just like it does for first-page.js.

(pseudo)If (document.js_arr != empty){

    (pseudo)foreach item in array{
       let filename = (pseudo) the "file" variable

       //Import general helper file
       import('../js/'+ filename )
       .then( module => {
            module.default();    
       });

    }  
}

The problem here is that, as the import takes a dynamic variable rather that a fixed string, Vite doesn't know the value of that filename at build-time so it can't bundle that file or swap out the reference for it's build location.

As I know the filename would be the same after bundling, I tried to fix the problem by swapping "../js/" + filename for "../build/" + filename, and added the filename directly in to my vite.config.js list of files. This works to a degree (eg. it finds the js file and loads it in) but the "module.default()" request fails with the following error:

TypeError: t.default is not a function

I understand this issue is because "module.default()" has been converted to the letter "t", and the app.js doesn't know about this change (because the additional JS file was compiled/bundled in the vite.config.js, and not progressively while Vite sniffed through all the connected JavaScript).

So the solutions would be to have these other JS files processed in the same way as first-page.js and general-module.js. But how can I do this if Vite doesn't know the filename at build-time? Or is there a way to hack this or do it another way?

Update

The answer provided by Nico was to use import.meta.glob like so:

const modules = import.meta.glob('../js/components/*.js')

...and then, when the required JS file is imported I reference it like so...

modules[js_object["file"]]()
.then( module => {
     module.default();    
});

We reference the resources/original version, and the importer looks up the bundled asset version of that file. This even works for importing stylesheets too, and it automatically knows to process them as stylesheets without any additional code in the import! Nice.


Solution

  • I think what you're looking for is the dynamic or Glob import using vite. Personally I've never used it as I like things to load statically, but it looks to do what you want.

    So those are your options, of course there's frameworks and libraries that also have ways to load async components which you could look in to. The key to remember is that the const a = import('a') syntax returns a promise while the import {a} from 'a' does not

    dynamic import glob import