Loading newest dependency javascript module file in functions.php

The problem

Import statement like:

import { export1 } from "./module-name.js";

is called static import declaration.

Dynamic query string (to force load the latest file) cannot be used in static import declarations. So, to solve this problem, you’ll have to choose one of the following four options:

  1. Remove export and import statements from all the JavaScript files and enqueue them as non-module JavaScript.

  2. Don’t enqueue ads.js in WordPress, only pass version number of ads.js file as a JavaScript global variable with wp_add_inline_script() function to the page, and dynamically import ads.js as a module from banners.js and ads-search.js.

  3. Enqueue ads.js as a module, pass version number of ads.js file as a JavaScript global variable with wp_add_inline_script() function to the page, enqueue banners.js and ads-search.js as non-module JavaScript (with ads.js in the dependency array), and dynamically import ads.js as a module from banners.js and ads-search.js. This is slightly different from option-2 and may have some benefits depending on implementation.

  4. Use a bundler like Rollup or Webpack and build a single minified script using a file watcher and enqueue the build minified JavaScript file in WordPress as a non-module JavaScript file. I’d recommend this method, but would not explain it here as it out of the scope of this question.

Option-1: Make all JS files non-module

Just remove export and import statements from all the JavaScript files, like below:

// in ads.js
// removed the export declaration
async function selectADS(adNumber, element, adsArray) {
  let usedNum = [];
  let i = 0;
  while (1) {
    var randomNum = Math.floor(Math.random() * adsArray.length);

    if (!usedNum.includes(randomNum)) {
      let link = document.createElement("a");
      link.setAttribute("href", adsArray[randomNum].link);
      link.setAttribute("target", "_blank");

      let ad = document.createElement("img");
      ad.setAttribute("alt", adsArray[randomNum].alt);
      ad.setAttribute("src", adsArray[randomNum].src);

      element.appendChild(link);
      link.appendChild(ad);

      usedNum.push(randomNum);
      i++;
      if (i == adNumber) break;
    }
  }
}

// in banners.js
// call selectADS function normally, no import declaration
selectADS();

Then in the WordPress end, enqueue them in my_custom_theme_scripts() as you’ve written in the question.

Option-2: pass ads.js version to JS & dynamically import ads.js

If for some reason you have to keep the export declaration in ads.js, and still want to import the latest file in banners.js and ads-search.js, you’ll need to make some changes in WordPress enqueue code and in the JavaScript code of banners.js and ads-search.js.

For example, your my_custom_theme_scripts() function can be like this:

function my_custom_theme_scripts() {
    $ads_version = filemtime( get_stylesheet_directory() . '/js/ads.js' );
    $banners_version = filemtime( get_stylesheet_directory() . '/js/banners.js' );

    wp_enqueue_script(
        'banners',
        get_stylesheet_directory_uri() . '/js/banners.js',
        array(),
        $banners_version,
        true
    );
    wp_add_inline_script(
        'banners',
        'const My_Custom_Ads_Version = "' . $ads_version . '";',
        'before'
    );
}

In this case, ads.js file can be like before with export declaration, and banners.js file can dynamically import it like below:

(async () => {  
    if( typeof My_Custom_Ads_Version !== 'undefined' ) {
        const adsModule = await import( `./ads.js?ver=${My_Custom_Ads_Version}` );
        const selectADS = adsModule.selectADS;
        selectADS();
    }
})();

You’ll have to make similar changes to ads-search.js. After that your code will work.

Remember, in this case export declarations can be like before (in ads.js), but the import statements in banners.js and ads-search.js should be changed from static import declaration to dynamic import statements as shown in the code above.

Option-3: Enqueue ads.js as a module, pass ads.js version to JS & dynamically import ads.js

This option is very similar to option-2 above. However, option-2 imports ads.js in runtime. So if an implementation wants to delay the execution of ads.js module objects, but still wants to load the ads.js module with page load, then this implementation will be preferable.

To do this, you’ll have to first hook into script_loader_tag filter, so that WordPress adds type="module" in <script> tag.

Following is an example WordPress code that implements it:

add_filter( 'script_loader_tag', 'my_custom_module_support', PHP_INT_MAX, 2 );
function my_custom_module_support( $tag, $handle ) {
    if( strpos( $handle, 'my_custom_module_' ) === 0 ) {
        if( current_theme_supports( 'html5', 'script' ) ) { 
            return substr_replace( $tag, '<script type="module"', strpos( $tag, '<script' ), 7 );
        }
        else {
            return substr_replace( $tag, 'module', strpos( $tag, 'text/javascript' ), 15 );
        }
    }

    return $tag;
}
function my_custom_theme_scripts() {
    $ads_version = filemtime( get_stylesheet_directory() . '/js/ads.js' );
    $banners_version = filemtime( get_stylesheet_directory() . '/js/banners.js' );

    // we are loading ads.js as a module.
    // my_custom_module_support() function checks any handle
    // that begins with "my_custom_module_" and adds type="modle"
    // to print it as a ES6 module script 
    wp_enqueue_script(
        'my_custom_module_ads',
        get_stylesheet_directory_uri() . '/js/ads.js',
        array(),
        $ads_version,
        true
    );
    wp_add_inline_script(
        'my_custom_module_ads',
        'const My_Custom_Ads_Version = "' . $ads_version . '";'
    );
    wp_enqueue_script(
        'banners',
        get_stylesheet_directory_uri() . '/js/banners.js',
        array( 'my_custom_module_ads' ),
        $banners_version,
        true
    );
}
add_action( 'wp_enqueue_scripts', 'my_custom_theme_scripts' );

With this option as well, ads.js file can be like before with export declaration, and banners.js and ads-search.js can dynamically import from ads.js as shown in option-2 above.

However, with this option, the browser will load ads.js module file before it is imported dynamically in banners.js. So when the dynamic import is performed, the browser will just use the already loaded file.

In most cases this is not significant. However, it can be significant when you perform the dynamic import on an event like below code (especially if ads.js file takes significant time to load):

// sample ads.js code
console.log( 'ads.js loaded:START' );
export async function selectADS( adNumber, element, adsArray ) {
    console.log( 'ads.js as a module loaded' );
};
console.log( 'ads.js loaded:END' );

// banners.js code
console.log( 'banners.js load:START' );

let clicked = false;
document.addEventListener( 'click', async (event) => {   
    if( ! clicked && typeof My_Custom_Ads_Version !== 'undefined' ) {
        const adsModule = await import( `./ads.js?ver=${My_Custom_Ads_Version}` );
        const selectADS = adsModule.selectADS;
        selectADS();
    }
    clicked = true;
});

console.log( 'banners.js load:END' );

With this implementation, if the ads.js file is significantly large and it’s only dynamically imported on an event, code becomes simpler without having to wait for the imported file.

However, this is not the most significant advantage, because you can rewrite that JS code to still import early. More significant advantage of this implementation is if you use a caching system or a bundler that watches changes in HTML code. With this option it’s easier to handle such scenario as opposed to option-2 (because with option-2 adj.js is not visible from HTML).