Skip to Content
Technical Articles

UI5 Tooling – Custom Transpiler (Babel) Builder Extension Task

With the new UI5 Tooling, SAP provides a solution to build a productive version of your UI5 app without using grunt.. Which is a good thing 😊. In my current setup with Grunt, I was also using BabelJs as a transpiler to enable the latest JavaScript syntax. This is not available by default in this new UI5 Tooling. Luckily, the new UI5 tooling provides a lot of extension possibilities for adding custom logic to the build process. I’ve created a custom extension task to implement BabelJs into the UI5 Build process. I will show and explain all the required steps. This extension also supports Async and Await syntax!

Before we start changing the build process, this is how a default UI5 build looks like:

First, we need to define the required devDependencies in the package.json:

Don’t forget to run “npm install” after adding the devDependencies.

Next, we need to define a custom builder task with a reference to the implementation file “lib/tasks/babel.js”

Add this “babel.js” file to the project, this is the place where we’ll implement babel and transform all the files.

We will just start by implementing the transformation of the code without supporting Async Await. The extension will do the following:

  • Read all the files from the virtual workspace (the virtual workspace contains all the files from the webapp folder)
  • Filter out the files in the libs folder, assuming that these files are already transpiled
  • Convert each file by using babel
  • Update the virtual workspace

(Full code is added at the end of the blog.)

Running “npm run build” will now also run the extension and show each file that will be transformed:

This is only doing the basic transformation, but I also want to use Async and Await syntax. This requires an additional library, named “regenerator-runtime”.

As I do not want to add this manually to each project, I’m going to do this automatically in the extension. Therefore, we need to define this regenerator in the package.json:

I’ve added logic in the extension that will add the “regenerator-runtime”:

  • The code will look for the “regenerator-runtime” in the node_modules
  • Add it as a resource to the virtual workspace
  • Get the virtual path based on the “Component.js” to keep it generic (assuming that every project has a Component.js)
  • Create a new resource
  • Add the resource to the Workspace

This code is added just before the transformation of the resources but could also be done after it. The regenerator will not be transpiled because the virtual workspace is not reloaded.

Let’s now add an Async function in the PersonState:

After building the app again, we’ll notice that the dist folder has the regenerator-runtime:

And the code of getPerson has changed:

Babel also added some functions at the top for the Async Await syntax

This will not yet work! The regenerator-runtime needs to be loaded into our app. Defining this in the manifest.json will not work, at least not always. Loading the regenerator in the manifest will only work for controllers. But in my case, I use it in the PersonState which is used in the Component.js and loaded before the regenerator.

It will give you the following error:

It took me a while to figure out the root cause but even harder to find a solution. After searching online, I found this GitHub repository of Peter Muessig  : https://github.com/petermuessig/my-es6-ui5-app . So I asked his help with the regenerator and he came up with the following solution in development mode. Adding the following code at the top of the Component.js will load the regenerator first!

As I wanted to reduce all the manual steps, I implemented this in the extension. The extension will read the “Component.js” and add the code from Peter to it:

For production, Peter provided my the following configuration for the ui5.yml file:

This configuration will add the regenerator at the top of the component-preload:

The app runs now with Async Await in all browser after building:

This will give you the possibility to use the latest JavaScript syntax! Big thanks to Peter for his support!

You can find the full project at: https://github.com/lemaiwo/UI5ToolsExampleApp

Example of Peter: https://github.com/petermuessig/my-es6-ui5-app

The complete ui5 config:

specVersion: '1.0'
metadata:
  name: PersonSkills
type: application
server:
  customMiddleware:
    - name: odataProxy
      beforeMiddleware: serveResources
builder:
  bundles:
  - bundleDefinition:
      name: be/wl/PersonSkills/Component-preload.js
      defaultFileTypes:
      - ".js"
      - ".json"
      - ".xml"
      - ".html"
      - ".library"
      sections:
      - mode: raw
        filters:
        - be/wl/PersonSkills/regenerator-runtime/runtime.js
      - mode: preload
        filters:
        - be/wl/PersonSkills/manifest.json
        - be/wl/PersonSkills/controller/**
        - be/wl/PersonSkills/Component.js
        - be/wl/PersonSkills/i18n/**
        - be/wl/PersonSkills/model/**
        - be/wl/PersonSkills/ui5fixes/**
        - be/wl/PersonSkills/util/**
        - be/wl/PersonSkills/view/**
        - be/wl/PersonSkills/libs/**
        - be/wl/PersonSkills/test/**
        - be/wl/PersonSkills/service/**
        - be/wl/PersonSkills/state/**
        - be/wl/PersonSkills/localService/**
        resolve: false
        sort: true
        declareModules: false
    bundleOptions:
      optimize: true
      usePredefineCalls: true
  customTasks:
    - name: babel
      afterTask: replaceVersion
---
specVersion: "1.0"
kind: extension
type: server-middleware
metadata:
  name: odataProxy
middleware:
  path: lib/middleware/odataProxy.js
---
specVersion: "1.0"
kind: extension
type: task
metadata:
  name: babel
task:
  path: lib/tasks/babel.js

The complete extension code:

const colors = require('colors');
const emoji = require('node-emoji');
const babel = require("@babel/core");
const pathregen = require("regenerator-runtime/path").path;
const resourceFactory = require("@ui5/fs").resourceFactory;
const fs = require('fs');
const path = require("path");
const icons = ["hatching_chick", "baby_chick", "hatched_chick", "bird"];
const baseLogTask = "info".green + " babel:".magenta;
/**
 * Custom task example
 *
 * @param {Object} parameters Parameters
 * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files
 * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files
 * @param {Object} parameters.options Options
 * @param {string} parameters.options.projectName Project name
 * @param {string} [parameters.options.configuration] Task configuration if given in ui5.yaml
 * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
 */
module.exports = async function ({
    workspace,
    dependencies,
    options
}) {
    const jsResources = await workspace.byGlob("**/*.js");
    
    console.info("\n" + baseLogTask + "Add regenerator-runtime".green + emoji.get('palm_tree'));
    //get path of Component
    const componentResource = jsResources.find((jsResource) => jsResource.getPath().includes("Component.js"));
    const toPath = componentResource.getPath();
    const pathPrefix = toPath.replace("Component.js", "");
    //get path of regenerator in node_modules
    const pathRegenerator = pathregen.substr(pathregen.indexOf("node_modules") + 13).replace("\\", "/");
    //build full path for regenerator in current project
    const virtualPathRegenerator = pathPrefix + pathRegenerator;
    //get code of regenereator
    const runtimeCode = fs.readFileSync(pathregen, 'utf8');
    //create resource
    const runtimeResource = resourceFactory.createResource({
        path: virtualPathRegenerator,
        string: runtimeCode
    });
    //save regenerator to workspace
    await workspace.write(runtimeResource);

    console.info(baseLogTask + "Include regenerator-runtime".green + emoji.get('palm_tree'));
    //add require regenerator for development prupose
    let componentSource = await componentResource.getString();
    const requirePath = virtualPathRegenerator.replace("/resources/", "").replace(".js", "");
    componentSource = "// development mode: load the regenerator runtime synchronously\nif(!window.regeneratorRuntime){sap.ui.requireSync(\"" + requirePath + "\")}" + componentSource;
    componentResource.setString(componentSource);
    await workspace.write(componentResource);
    
    console.info(baseLogTask + "Start tranformation".green + emoji.get('palm_tree'));
    const filteredResources = jsResources.filter(resource => {
        return (!resource.getPath().includes("/libs/"));
    });
    let iconIdx = 0;
    const transformCode = async resource => {
        var source = await resource.getString();
        console.info(baseLogTask + "Transforming:".blue + emoji.get(icons[iconIdx]) + resource.getPath());
        iconIdx = iconIdx >= (icons.length - 1) ? 0 : ++iconIdx;
        var {code,map,ast} = babel.transformSync(source, {
            presets: [["@babel/preset-env"]],
            plugins: [["@babel/plugin-transform-modules-commonjs", {"strictMode": false}]]
        });
        resource.setString(code);
        return resource;
    };
    const transformedResources = await Promise.all(filteredResources.map(resource => transformCode(resource)));
    console.info(baseLogTask + "Tranformation finished".green + emoji.get('palm_tree'));

    console.info(baseLogTask + "Start updating files".green);
    await Promise.all(transformedResources.map((resource) => {
        return workspace.write(resource);
    }));
    console.info(baseLogTask + "Updating files finished".green);

    console.info(baseLogTask + "Babel task finished" + emoji.get('white_check_mark'))
};
2 Comments
You must be Logged on to comment or reply to a post.