Introduction

One of the the more frequently asked questions relating to the Oracle JET command line tool (ojet cli) concerns how to control the application bundle that is created by the ojet build --release or ojet serve --release command.  To explain, when you build an application with the –release flag, the cli will consolidate the files referenced from main.js into a single bundle file – generally called bundle.js. The way this works out of the box is that the optimizer (r.js by default) is passed the entry point of the application (main.js) and it will examine all the files that that file loads recursivley and create a single consolidated file which can be downloaded with a single GET request to the server, rather than one GET request per file. So that’s great and it all happens automatically – but what if you need more? In this article, I’ll illustrate the most common scenario where you may need more than this deafult bundling and how to address it when using the default optimizer with the ojet cli.  If you are using Webpack as your optimizer / bundler this article does not apply. 

What’s the Problem?

To illustrate the bundling scenario that most often gets asked about, let’s step though it. 

First of all I create a new application that will use JET modules, I can just use the navbar application template to scaffold one up quickly:

ojet create modExample --template=navbar 

This will create a runnable starter project, which uses four modules defined as pairs under the views and viewModels folders respectively in the source tree (about, customers, dashboard, incidents).  Now if I wanted to build this for final depoyment, I’d actually then carry out two steps:

  1. Edit the js/path_mapping.json file to set the use attribute to the value “cdn” – this is to ensure that all of the common JET and third-party libraries are just loaded from the CDN rather than being included in my generated bundle
  2. Run ojet build with the –release flag to automate the bundle creation 

Having done this, if you look into the /web output folder you’ll see a bundle.js file generated. However, when you run the application and click between the tabs that load the modules inspect the network traffic you’ll see that yes the bundle.js is loaded, but you are still seeing the individual loads for each of the module .js and .html files. Huh? It seems like the bundling has not worked, after all, most of the content of the application might well be in those modules.

The issue here is the way that the default bundling is done. The r.js optimizer is supplied with a configuration which specifies the main.js as the optimization root, and as such, only the files that are directly loaded by the define() block of the main.js will be recursed into and included into the bundle. Modules that will be handled by <oj-module>, on the other hand,  are generally loaded dynamically and are not included in that initial tree walk of the define() hierachy. 

Fattening the Bundle

So now that we know why the module .js and .html files are not included into the bundle, how do we change that?  Technically this is pretty simple, all we need to do is to amend the configuration that is being passed to r.js and set an include attribute. This attribute should be set to an array of file names with their paths relative to the optimzation root (/web/js in most cases).  These files will then be included into the bundling process for you along with the main entry point. The gotchya here is that the file names and paths that we include in this list need to be specified in the form that a define() or require() command would use them, so an HTML file for example would be included along with the text plugin prefix e.g. ‘text!views/customer.html’, whereas a javascript file would be included without the .js extension (because this is implicit) e.g. ‘viewModels/customer’

You might be about to ask yes great but where do I set this include list up? Well the answer is use a build hook, and specifically the before_optimize.js hook file that you will find in the scripts/hooks folder of your JET project (it will have been created by default if your project was generated in a recent version of JET, but if not you can always add it). 

This hook is passed a configuration object called configObj  which includes a requireJs attribute which is the actual configuration that r.js will be given.  In the hook you can make changes to the configuration and pass it back to the JET cli via the final resolve(configObj) call in the code. So trivially you could do something like this to include the customer module into the bundle:

configObj.requireJs.include = ['viewModels/customer','text!views/customer.html'];

Of course, the main issue with this is that you don’t generally want to have to keep going back to the hook to update this array every time you add a new module, so let’s finish off with a more generic solution.

Sample Hook Implementation

For the common case of modules stored within the paired /views and /viewModels folders we can easily automate the inclusion of all the modules with a small amount of scripting to discover the file names in question and then reformat them correctly for the include array.  You can of course adapt and extend this code to include other folders that might contain code that you want to be added to the bundle as well as the modules, but that’s up to you. 

One thing to note here, from a tecnical perspective, is that all of the file-io operations used are synchronous versions because we want to block the CLI tool from proceeding with the optimization until we have finished tweaking the configuration. 

Here’s the basic implementation of the whole hook:

 

/**
  Copyright (c) 2015, 2022, Oracle and/or its affiliates.
  Licensed under The Universal Permissive License (UPL), Version 1.0
  as shown at https://oss.oracle.com/licenses/upl/
 */
‘use strict’;
const fs = require(‘fs-extra’);
const path = require(‘path’);

/*
 * before_build hook to automatically include JET module implementations into
 * the main program bundle
 */
module.exports = function (configObj) {
  return new Promise((resolve, reject) => {
    console.log(“Before Optimize – retreiving module list”);
    //Read the oraclejetconfig.json to locate the correct output folder to read
    const jetConfigName = ‘oraclejetconfig.json’; //will be in the root
    if (fs.pathExistsSync(jetConfigName)) {
      const jetConfig = fs.readJSONSync(jetConfigName);
      const webFolder = jetConfig.paths.staging.web;
      const viewImplFolder = path.join(webFolder, ‘js’, ‘viewModels’);
      const viewModels = getIncludeFiles(viewImplFolder);
      const viewFolder = path.join(webFolder, ‘js’, ‘views’);
      const views = getIncludeFiles(viewFolder);
      const includeFiles = […viewModels, …views];

      //Add the retrieved list of extra files to the optimizer configuration
      console.log(`Before Optimize – Adding ${includeFiles.length} files to optimizer list`);
      configObj.requireJs.include = includeFiles;
    }
    else {
     console.error(‘\tERROR: Unable to read oraclejetconfig.json’);
    }
    resolve(configObj);
    });
  };
 
  /*
   * Function to gather the names of all the files in the target folder
   * This could be enhanced to recurse through sub-folders if required
   * The only trickery here is to return the paths to the files relative to
   * the root that the optimization process will be based on with the correct
   * path style for requireJs – hence the use of reducedRoot below
   */
  function getIncludeFiles(inFolder) {
    const foundFiles = [];
    const reducedRoot = inFolder.split(path.sep).slice(2).join(‘/’);
    fs.readdirSync(inFolder).forEach((fileOrDirectory) => {
    const fullPath = path.join(inFolder, fileOrDirectory);
    //You can see how you could make this routine recursive if you need to
    if (!fs.statSync(fullPath).isDirectory()) {
      if (path.extname(fileOrDirectory) === ‘.js’) {
        //strip the extension as that is not needed
        const withoutExt = path.basename(fileOrDirectory,’.js’);
        foundFiles.push(path.join(reducedRoot, withoutExt));
      }
      else {
        //html and json files will be loaded via the text! plugin and
        //need to retain the extension.
      foundFiles.push(‘text!’ + path.join(reducedRoot, fileOrDirectory));
      }
    }
});
return foundFiles;
}