Skip to content

About

A Babel transformer plugin for OpenUI5/SAPUI5. It allows you to develop UI5 applications by using the latest ECMAScript or TypeScript, including new syntax and objective oriented programming technology.

Topics

Resources

Contributing

Stars

33 stars

Watchers

4 watching

Forks

Repository files navigation

babel transform-ui5

A community-driven Babel transformer plugin for SAPUI5/OpenUI5.

It allows you to develop SAPUI5 applications using the latest ECMAScript, including classes and modules, or TypeScript.

Used by the UI5 ecosystem

Most UI5 TypeScript projects consume this plugin indirectly through ui5-tooling-transpile — a @ui5/cli middleware and build task that bundles babel-preset-transform-ui5 and provides a zero-configuration Babel setup for standard UI5 projects. If you use @ui5/cli, installing ui5-tooling-transpile is the recommended way to use this plugin without writing a Babel config manually.

The official Easy UI5 TypeScript project generators — ts-app and ts-library — both scaffold projects that use ui5-tooling-transpile, so any project generated from those templates relies on this plugin out of the box.

Install

This repo contains both a preset and a plugin. It is recommended to use the preset since the plugin may get split into two in the future.

npm install babel-preset-transform-ui5 --save-dev

or

yarn add babel-preset-transform-ui5 --dev

Configure

.babelrc

At a minimum, add transform-ui5 to the presets.

{
    "presets": ["transform-ui5"]
}

Or if you want to supply plugin options, use the array syntax.

{
    "presets": [
        ["transform-ui5", {
            ...pluginOpts
        }]
    ]
}

Class property syntax (static metadata = {}) is supported natively by recent Babel versions. If you are on an older Babel 7 setup, add @babel/plugin-proposal-class-properties (Babel < 7.12) or rely on @babel/preset-env (Babel ≥ 7.12, where it is included automatically).

It is also recommended to use @babel/preset-env to control which ES version the final code is transformed to.

The order of presets is important and @babel/preset-env should be higher in the array than this one. Babel applies them in reverse order for legacy reasons, so this preset will be applied first.

{
    "presets": [
        ["@babel/preset-env", { // applied 3rd
            ...presetEnvOpts
        }],
        ["transform-ui5", { // applied 2nd
            ...pluginOpts
        }],
        "@babel/preset-typescript", // applied 1st
    ]
}

Features

There are 2 main features of the plugin, and you can use both or one without the other:

  1. Converting ES modules (import/export) into sap.ui.define(...) or sap.ui.require(...).
  2. Converting ES classes into Control.extend(...) syntax.

This only transforms the UI5 relevant things. It does not transform everything to ES5 (for example it does not transform const/let to var). This makes it easier to use @babel/preset-env to transform things correctly.

A more detailed feature list includes:

  • ES2015 Imports (default, named, and dynamic)
  • ES2015 Exports (default and named)
  • Classes, using inheritance and super keyword
    • Static methods and fields
    • Class properties
    • Class property arrow functions are bound correctly in the constructor.
  • Existing sap.ui.define calls don't get wrapped but classes within can still be converted.
    • Fixes constructor shorthand method, if used.
  • Various options to control the class name string used.
    • JSDoc (alias, namespace, name)
    • Decorators (alias, namespace, name)
    • File path based namespace, including setting a prefix.

Converting ES modules (import/export) into sap.ui.define or sap.ui.require

The plugin will wrap any code having import/export statements in sap.ui.define. If there is no import/export, it won't be wrapped.

Static Import

The plugin supports all of the ES import statements, and will convert them into sap.ui.define arguments.

import Default from "module";
import Default, { Named } from "module";
import { Named1, Named2 } from "module";
import * as Namespace from "module";

The plugin uses a temporary name (as needed) for the initial imported variable, and then extracts the properties from it. This allows importing ES Modules which have a 'default' value, and also non-ES modules which do not. The plugin also merges imports statements from the same source path into a single require and then deconstructs it accordingly.

This:

import Default, { Name1, Name2 } from "app/File";
import * as File from "app/File";

Becomes:

sap.ui.define(['app/File'], function(__File) {
  "use strict";

  function _interopRequireDefault(obj) {
      return (obj && obj.__esModule && (typeof obj.default !== "undefined")) ? obj.default : obj;
  }
  const Default = _interopRequireDefault(__File);
  const Name1 = __File["Name1"];
  const Name2 = __File["Name2"];
  const File = __File;
}

Also refer to the noImportInteropPrefixes and neverUseStrict option below.

Dynamic Import

ECMAScript allows for dynamic imports calls like import(path) that return a Promise which resolves with an ES Module.

This plugin will convert that to an async sap.ui.require wrapped in a Promise. The resolved object will be a ES module or pseudo ES module having a 'default' property on it to reference the module by, to match the format used by import(). If the module is not a real ES module and already has a default property, the promise will be rejected with an error.

For JavaScript projects, this syntax doesn't provide much advantage over a small utility function and has the downside of not working if your module has a 'default' property. The main advantage of this syntax is with TypeScript projects, where the TypeScript compiler will know the type of the imported module, so there is no need to define a separate interface for it.

Also note that while the ES dynamic import specification requires a relative path, sap.ui.require works with absolute paths using a module prefix.

import.meta rewriting

The plugin rewrites two import.meta member accesses to their UI5 equivalents (adding the corresponding built-in module as a hidden sap.ui.define dependency):

  • import.meta.url → module.url (adds the built-in module dependency to the sap.ui.define call)
  • import.meta.resolve(path) → require.toUrl(path) (adds the built-in require dependency)
// Input
const url = import.meta.url;
const path = import.meta.resolve("./assets/icon.png");

// Output (inside the sap.ui.define factory)
const url = module.url;
const path = require.toUrl("./assets/icon.png");

import.meta accesses of any other form are left unchanged. Files that carry /* @ui5-no-wrap */ skip this transform (and all module transforms) entirely.

Export

The plugin also supports (most of) the ES modules export syntax.

export function f() {};
export const c = 'c';
export { a, b };
export { a as b };
export default {};
export default X;
export { X as default };
export let v; v = 'v'; // NOTE that the value here is currently not a live binding (http://2ality.com/2015/07/es6-module-exports.html)

Export is a bit trickier if you want your exported module to be imported by code that does not include an import inter-op. If the importing code has an inter-op logic inserted by this plugin, then you don't need to worry and can disable the export inter-op features if desired.

Imagine a file like this:

export function two() {
  return 2;
}
export default {
  one() {
    return 1;
  },
};

Which might export a module that looks like:

{
    __esModule: true,
    default: {
        one() {
            return 1;
        }
    },
    two() {
        return 2;
    }
}

But in order to be usable in a standard sap.ui.require file, what we want is actually:

{
    one() {
        return 1;
    }
    two() {
        return 2;
    }
}

This plugin's terminology for that is 'collapsing'.

Export Collapsing to default export

The export inter-op features do their best to only return the default export rather than returning an ES module. To do this, it determines if all the named exports already exist on the default export (with the same value reference), or whether they can be added to it if there is not a naming conflict.

If there is a naming conflict or other reason why the named export cannot be added to the default export, the plugin will throw an error by default.

In order to determine which properties the default export already has, the plugin checks a few locations, if applicable.

  • In an object literal.
export default {
  prop: val,
};
// plugin knows about 'prop'
  • In a variable declaration literal or assigned afterwards.
const Module = {
  prop1: val,
};
Module.prop2 = val2;
export default Module;
// plugin knows about 'prop1' and 'prop2'
  • In an Object.assign(..) or _extends(..)
    • _extends is the named used by babel and typescript when compiling object spread.
    • This includes a recursive search for any additional objects used in the assign/extend which are defined in the upper block scope.
const object1 = {
  prop1: val,
};
const object2 = Object.assign({}, object1, {
  prop2: val,
});
export default object2;
// plugin knows about 'prop1' and 'prop2'

CAUTION: The plugin cannot check the properties on imported modules. So if they are used in Object.assign() or _extends(), the plugin will not be aware of its properties and may override them with a named export.

Example non-solvable issues

The following are not solvable by the plugin, and result in an error by default.

export function one() {
  return 1;
}

export function two() {
  return 2;
}

function one_string() {
  return "one";
}

const MyUtil = {
  // The plugin can't assign 'one' or 'two' to `exports` since there is a collision with a different definition.
  one: one_string,
  two: () => "two",
};
export default MyUtil;
sap.ui.define global export flag

If you need the global export flag on sap.ui.define, add @global to the JSDoc on the export default statement.

const X = {};

/**
 * @global
 */
export default X;

Outputs:

sap.ui.define(
  [],
  function() {
    const X = {};
    return X;
  },
  true
);

Also refer to the option exportAllGlobal below.

Minimal Wrapping

By default, the plugin will wrap everything in the file into the sap.ui.define factory function, if there is an import or an export.

However sometimes you may want to have some code run prior to the generated sap.ui.define call. In that case, set the property noWrapBeforeImport to true and the plugin will not wrap anything before the first import. If there are no imports, everything will still be wrapped.

There may be a future property to minimize wrapping in the case that there are no imports (i.e. only wrap the export).

Example:

const X = 1;
import A from "./a";
export default {
  A,
  X,
};

Outputs:

"use strict";

const X = 1;
sap.ui.define(["./a"], A => {
  return {
    A,
    X,
  };
});

Also refer to the neverUseStrict option below.

Top-Level Scripts (e.g. QUnit Testsuites)

By default, modules are converted to UI5 AMD-like modules using sap.ui.define. In some cases, it is necessary to include modules via script tags, such as for QUnit testsuites. Therefore, this Babel plugin supports converting modules into scripts using sap.ui.require instead of AMD-like modules using sap.ui.define. These modules can then be used as top-level scripts, which can be included via <script> tags in HTML pages. To mark a module as being converted into a sap.ui.require script, you need to add the comment /* @sapUiRequire */ at the top of the file.

Example:

/* @sapUiRequire */

// https://api.qunitjs.com/config/autostart/
QUnit.config.autostart = false;

// import all your QUnit tests here
void Promise.all([import("unit/controller/App.qunit")]).then(() => {
  QUnit.start();
});

will be converted to:

"use strict";

QUnit.config.autostart = false;
sap.ui.require([], function () {
  function __ui5_require_async(path) { /* ... */ }
  void Promise.all([__ui5_require_async("unit/controller/App.qunit")]).then(() => {
    QUnit.start();
  });
});

⚠️ Although sap.ui.define and sap.ui.require may appear similar from an API perspective, they have different behaviors. To understand these differences, please read the section titled "Using sap.ui.require instead of sap.ui.define on the top level" in the Troubleshooting for Loading Modules.

💡 The plugin detects the global usage of QUnit.config.autostart and moves this out of the sap.ui.require or sap.ui.define block automatically to ensure that the config is applied sychronously when loading the module. The move can be supressed with the configuration option noWrapQUnitConfigAutostart. If QUnit is imported, e.g. import QUnit from "qunit"; then this is detected and the autostart config is not moved as it must apply locally.

Excluding Modules or Imports from Transformation

Sometimes a module (or a single import within it) should be left as-is instead of being converted into a UI5 module. Two comment pragmas control this. Both are matched as block comments (/* ... */) whose trimmed content is exactly the pragma text — a // line comment or additional words (e.g. /* @ui5-no-wrap (worker) */) will not be recognized.

@ui5-no-wrap — keep a whole module as a native ES module

Adding /* @ui5-no-wrap */ anywhere in a file leaves the entire module unwrapped: it is not wrapped in sap.ui.define/sap.ui.require, and no import, export, dynamic import() or import.meta transformation is applied. TypeScript type annotations are still stripped, so the output stays a valid native ES module. This is useful for code that is loaded outside of the UI5 module system, for example a Web Worker.

Example:

/* @ui5-no-wrap */

import Button from "sap/m/Button";

interface WorkerMessage {
  type: string;
  payload: unknown;
}

export function handleMessage(msg: WorkerMessage): void {
  console.log(msg.type, msg.payload);
}

export default Button;

will be converted to:

import Button from "sap/m/Button";
export function handleMessage(msg) {
  console.log(msg.type, msg.payload);
}
export default Button;

⚠️ The output of a @ui5-no-wrap module is a native ES module (import/export are preserved) and is not a UI5 AMD module. It must be consumed accordingly (e.g. as an ES module in a Web Worker), not loaded via sap.ui.require/sap.ui.define.

@ui5-ignore-import — exclude a single import

Adding /* @ui5-ignore-import */ as the leading comment of an individual import statement or dynamic import() call leaves that one import untouched: it is not added to the sap.ui.define dependency array and dynamic imports are not rewritten to __ui5_require_async(...). All other imports in the module are still transformed normally.

Example (static and dynamic imports):

/* @ui5-ignore-import */
import WorkerUtil from "./worker-util";

import Button from "sap/m/Button";

async function load() {
  /* @ui5-ignore-import */
  const mod = await import("./lazy-module");

  const normal = await import("./transformed-module");
}

export default Button;

Here sap/m/Button becomes a sap.ui.define dependency and ./transformed-module is rewritten to __ui5_require_async(...), while ./worker-util and ./lazy-module are left exactly as written.

⚠️ A dynamic import() is an expression and is always safe to ignore. A static import declaration, however, is only valid at the top level of a module — so @ui5-ignore-import on a static import produces valid output only when the module is not wrapped (i.e. it has no exports, or it also carries @ui5-no-wrap). If the module is wrapped in sap.ui.define, the ignored import is left inside the factory function, which is not valid at runtime.

Converting ES classes into Control.extend(..) syntax

The plugin converts ES classes to Control.extend(..) syntax when it determines a class should be a UI5 managed object. Detection is automatic for the common cases — no annotation is required in a well-configured project.

Automatic conversion

A class is automatically converted when any of the following conditions is true:

  1. Known UI5 base class (autoConvertKnownUI5Classes, on by default) — the class's superclass is imported from a known UI5 module path such as sap/ui/core/mvc/Controller, sap/ui/core/Control, sap/ui/base/ManagedObject, etc. The full built-in list is in src/classes/visitor.js. Transitive inheritance is also resolved: if MyController extends BaseController and BaseController extends Controller, MyController is auto-converted regardless of the order in which Babel processes the files (ancestor files that have not been processed yet are read from disk on demand).

  2. Filename suffix (autoConvertUI5FileTypes, on by default) — the file's name ends with a recognised UI5 suffix:

    • .controller.(js|ts) — converted even without a namespace (preserves legacy behaviour).
    • .view.(js|ts|jsx|tsx) — converted when a namespace can be resolved.
  3. Explicit annotation — the class carries a @name, @alias, or @namespace JSDoc tag or decorator (see below).

All automatic paths except .controller.* require a resolvable namespace so the generated FQN is never a bare class name. Configure namespacePrefix, sourceFolders, or the file-path-based default to supply one (see File based namespace below).

To prevent a specific class from being converted, add the @nonui5 JSDoc tag or decorator to it.

Additional base-class paths

Use additionalUI5Classes to register project-specific base-class module paths that should trigger auto-conversion:

{
    "plugins": [
        ["transform-modules-ui5", {
            "additionalUI5Classes": ["my/app/BaseController"]
        }]
    ]
}

A warning is printed at the end of the build for any path that was configured but never matched by an import, helping you catch typos.

Note: The "never matched" warning fires once at Node.js process exit. It will appear in a normal one-shot build (babel src/ or webpack --mode production) but not in watch mode (babel --watch, webpack --watch, vite dev, etc.), because long-lived watch processes never reach process exit during normal development.

Configuring Name or Namespace

The plugin provides a few ways to set the class name or namespace used in the SAPClass.extend(...) call. When none of the overrides below are used, the name comes from the ES class name and the namespace from the file path (see File based namespace below).

File based namespace (default)

The default behaviour if no JSDoc or Decorator overrides are given is to use the file path to determine the namespace to prefix the class name with.

This is based on the relative path from either the babelrc sourceRoot property or the current working directory.

For library and app projects with a dedicated source folder (e.g. src/, test/, webapp/), use the sourceFolders option to map each folder to its namespace prefix. The folder name is stripped from the namespace and replaced by the mapped prefix:

{
    "plugins": [
        ["transform-modules-ui5", {
            "sourceFolders": {
                "src": "my.lib",
                "test": "my.lib.test"
            }
        }]
    ]
}

With this configuration:

  • src/control/MyButton.js → FQN my.lib.control.MyButton
  • test/integration/opaTest.js → FQN my.lib.test.integration.opaTest
  • A file in a folder not listed in sourceFolders gets an empty namespace and will not be auto-converted.

For monorepo / npm-workspace setups where a dependency project's files are transpiled alongside the main project but live outside its sourceRoot, use the dependencies option. Each entry describes one dependency project with its own sourceRoot and optional sourceFolders:

{
    "plugins": [
        ["transform-modules-ui5", {
            "sourceFolders": { "src": "my.app" },
            "dependencies": [
                {
                    "sourceRoot": "../my-lib",
                    "sourceFolders": { "src": "my.lib" }
                }
            ]
        }]
    ]
}

When a file is outside the primary sourceRoot, the plugin checks each dependency entry in order. The first entry whose sourceRoot contains the file is used to derive the namespace — applying that entry's sourceFolders exactly as the primary sourceFolders option works. Files that are outside every listed sourceRoot remain without a namespace (existing behaviour).

For simpler single-root projects you can still use namespacePrefix together with Babel's sourceRoot:

{
    "sourceRoot": "src/",
    "plugins": [
        ["transform-modules-ui5", {
            "namespacePrefix": "my.app"
        }],
        "other-plugins"
    ]
}

If the default file-based namespace does not work for you (perhaps the app name is not in the file hierarchy), there are a few way to override.

JSDoc

The simplest way to override the names is to use JSDoc. This approach will also work well with classes output from TypeScript if you configure TypeScript to generate ES6 or higher, and don't enable `removeComments``.

The recognized class-info JSDoc tags are (all matching is case-insensitive, so @nonui5 and @nonUI5 are equivalent):

Tag Kind Effect
@alias my.app.ClassName value Full FQN replacement — highest precedence (overrides @name, @namespace, and the file path).
@name my.app.ClassName value Full FQN replacement — overrides @namespace and the file path.
@namespace my.app value Overrides the file-path-derived namespace; class name is still appended.
@nonUI5 flag Opts this class out of UI5 conversion entirely.
@controller flag Marks this class as a controller (used with moveControllerPropsToOnInit).
@keepConstructor flag Prevents the constructor from being moved to onInit when moveControllerConstructorToOnInit is enabled.

You can set the @name/@alias directly or just the @namespace and have the name derived from the ES class name.

@alias — recommended for named extend-classes

Use @alias with the fully-qualified class name. @alias is the officially recommended JSDoc property used by UI5 source code itself to declare the module name.

/**
 * @alias my.app.AController
 */
class AController extends SAPController {
    ...
}

outputs:

const AController = SAPController.extend("my.app.AController", {
    ...
});
@name — for unnamed (anonymous) extend-classes

Use @name when the class has no local name (e.g. an anonymous class expression passed to return). @alias requires a local name to alias; @name does not.

/**
 * @name my.app.AController
 */
return class extends SAPController {
    ...
}
@namespace — namespace prefix for extend-classes

Use @namespace to set only the namespace prefix; the class name is derived automatically from the ES class name. This is useful when the file-based namespace does not match the desired UI5 namespace.

/**
 * @namespace my.app
 */
class AController extends SAPController {
    ...
}

outputs:

const AController = SAPController.extend("my.app.AController", {
    ...
});
Decorators

Alternatively, you can use decorators to override the namespace or name used. The decorators @alias, @name, @namespace, and @nonUI5 work the same as their JSDoc counterparts, but instead of a space you pass the string literal to the decorator function. Note that @controller and @keepConstructor are JSDoc-only and have no decorator equivalent.

NOTE that using a variable is currently not supported.

@alias('my.app.AController')
class AController extends SAPController {
    ...
}

ℹ️ Use @name('my.app.AController') when the class has no local name (anonymous class expression). Use @namespace('my.app') to supply only the namespace prefix and have the class name derived automatically.

Class Properties

The plugin supports converting class properties, and there are a few scenarios.

Static Class Props

Static class props are always added to the extend object. It's recommended to always use static if you want the property as part of the extend object. Some examples of this are metadata, renderer and any formatters or factories you need in views.

class MyControl extends SAPControl {
  static metadata = {...};
  static renderer = {...};
}

class Controller extends SAPController {
  static ThingFactory = ThingFactory;
  static ThingFormatter = ThingFormatter;
}

Instance Class Props

Instance props either get added to the constructor or to the onInit function (for controllers).

Before version 7.x, they could also get added directly to the SomeClass.extend(..) config object, but not anymore now. So if you still want a prop in the extend object, it's best to use a static prop. However, there are some exception where it is known that UI5 expects certain properties in the extend object, like renderer, metadata and overrides and some configurable cases related to controller extensions (see below).

Refer to the next section to see the logic for determining if constructor or onInit is used as the init function for class properties.

The logic for determining if the prop going into the controller/onInit or the extend object is whether it uses 'this' or needs 'this' context (i.e. arrow function).

In the bind method (either constructor or onInit), the properties get added after the super call (if applicable) and before any other statements, so that it's safe to use those properties.

class Controller extends SAPController {
  A = 1; // added to constructor or onInit (to extend object in v6 and lower)
  B = Imported.B; // added to constructor or onInit (to extend object in v6 and lower)
  C = () => true; // added to constructor or onInit
  D = this.B.C; // added to constructor or onInit
  E = func(this); // added to constructor or onInit
  F = func(this.A); // added to constructor or onInit

  onInit() {
    super.onInit();
    // --- Props get added here ---
    doThing(this.A, this.D);
  }
}
const Controller = SAPController.extend("...", {
  A: 1,
  B: Imported.B,
  onInit: function onInit() {
    if (typeof SAPController.prototype.onInit === "function") {
      SAPController.prototype.onInit.apply(this);
    }
    this.C = () => true;
    this.D = this.B.C;
    this.E = func(this);
    this.F = func(this.A);
    doThing(this.A, this.D);
  },
});

Special Class Property Handling for Controllers

The default class property behaviour of babel is to move the property into the constructor. This plugin has a moveControllerPropsToOnInit option that moves them to the onInit function rather than the constructor. This is useful since the onInit method is called after the view's controls have been created (but not yet rendered).

When that property is enabled, any class treated as a controller will use onInit instead of constructor for class-property initialisation. A class is treated as a controller when any of the following is true: its name contains "Controller", it has the @controller JSDoc tag, its filename ends with .controller.(js|ts), or its direct superclass is imported from sap/ui/core/mvc/Controller or sap/ui/core/mvc/ControllerExtension.

This is mostly beneficial for TypeScript projects that want easy access to controls without always casting them. In typescript, the byId(..) method of a controller returns a Control instance. Rather than continually casting that to the controller type such as sap.m.Input, it can be useful to use a class property.

/**
 * @name app.MyController
 * @controller
 */
class MyController extends Controller {
  input: SAPInput = this.byId("input") as SAPInput;

  constructor() {
    super();
  }

  onInit(evt: sap.ui.base.Event) {}
}

Results in:

const MyController = Controller.extend("app.MyController", {
  onInit(evt) {
    this.input = this.byId("input");
  },
});

Of course, the alternative would be to define and instantiate the property separately. Or to cast the control whenever it's used.

/**
 * @name app.MyController
 * @controller
 */
class MyController extends Controller {
  input: SAPInput;

  onInit(evt: sap.ui.base.Event) {
    this.input = this.byId("input") as SAPInput;
  }
}

Handling metadata and renderer

Because ES classes are not plain objects, you can't have an object property like 'metadata'.

This plugin allows you to configure metadata and renderer as class properties (static or not) and the plugin will convert it to object properties for the extend call.

class MyControl extends SAPClass {
    static renderer = MyControlRenderer;
    static metadata = {
        ...
    }
}

is converted to

const MyControl = SAPClass.extend('MyControl', {
    renderer: MyControlRenderer,
    metadata: {
        ...
    }
});

Properties related to Controller Extensions

The overrides class property (added in UI5 version 1.112) required for implementing a ControllerExtension will also be added to the extend object. (For backward compatibility with older UI5 runtimes, you can use overridesToOverride: true.)

/**
 * @namespace my.sample
 */
class MyExtension extends ControllerExtension {

  static overrides = {
    onPageReady: function () { }
  }
}

is converted to

const MyExtension = ControllerExtension.extend("my.sample.MyExtension", {
  overrides: {
    onPageReady: function () {}
  }
});
return MyExtension;

When a controller implemented by you uses pre-defined controller extensions, in JavaScript the respective extension class needs to be assigned to the extend object under an arbitrary property name like someExtension. Whenever the controller is instantiated, the UI5 runtime will instatiate the extension and this instance of the extension will then be available as this.someExtension inside your controller code.

While in the JavaScript code a controller class must be assigned in the extend object, the TypeScript compiler needs to see that the class property contains an extension instance. To support this, a dummy method ControllerExtension.use(...) was introduced in the UI5 type definitions in version 1.127. It is being downported to patches of older releases and should be available in the following (and higher) versions of OpenUI5, SAPUI5, @openui5/types and @sapui5/types:

  • 1.127.0
  • 1.126.1
  • 1.124.3
  • 1.120.18

The other releases in between are no longer maintained and will not get a downport. Neither will older releases.

For @types/openui5, versioning is different and downports are tbd with at least 1.120.4 being a likely target.

This method takes an extension class as argument and claims to return an instance, so TypeScript will allow you to work with an instance in your controller. However, behind the scenes, the method call is simply removed by this transformer plugin, so the UI5 runtime gets the extension class it needs to create a new instance of the extension for each controller instance. For these assignments, the transformer plugin also takes care that they remain inside the extend object in the resulting JavaScript code.

Example:

import Routing from "sap/fe/core/controllerextensions/Routing";
import ControllerExtension from "sap/ui/core/mvc/ControllerExtension";

/**
 * @namespace my.sample
 */
class MyController extends Controller {

  routing = ControllerExtension.use(Routing); // use the "Routing" extension provided by sap.fe

  someMethod() {
    this.routing.navigate(...);
  }
}

is converted to the proper Controller.extend(...) code as expected by the UI5 runtime:

// ...

const MyController = Controller.extend("my.sample.MyController", {
  routing: Routing, // now the Routing *class* is assigned as value, while above it appeared to be an instance
  
  someMethod: function() {
    this.routing.doSomething();
  }
});
return MyController;

NOTE: In order to have this transformer plugin recognize and remove the dummy method call, you MUST a) call it on the ControllerExtension base class (the module imported from sap/ui/core/mvc/ControllerExtension), not on a class deriving from it (even though it is inherited) and b) assign the extension right in the class definition as shown in the examples on this page (an "equal" sign is used, not a colon like in JavaScript, as this is now ES class syntax and not a configuration object). Any variation may cause the call not to be recognized and removed and lead to a runtime error. Calling ControllerExtension.use(...) with more or less than one argument will not only cause a TypeScript error, but will also make this transformer throw an error.
The removal only takes place when the class definition overall is recognized and transformed by this transformer. So when the class still is an ES6 class in the output, first fix this, then check again whether the ControllerExtension.use(...) call has been removed.

Some controller extensions allow implementing hooks or overriding their behavior. This can be done equally:

import Routing from "sap/fe/core/controllerextensions/Routing";
import ControllerExtension from "sap/ui/core/mvc/ControllerExtension";

/**
 * @namespace my.sample
 */
class MyController extends Controller {

  routing = ControllerExtension.use(Routing.override({
    someHook: function(...) { ... }
  })); // adapt the "Routing" extension provided by sap.fe

  someMethod() {
    this.routing.navigate(...);
  }
}

Static Properties

Since class properties are an early ES proposal, TypeScript's compiler (like babel's class properties transform) moves static properties outside the class definition, and moves instance properties inside the constructor (even if TypeScript is configured to output ESNext).

To support this, the plugin will also search for static properties outside the class definition. It does not search for them in the constructor, so be sure to define renderer and metadata as static props if TypeScript is used.

/** TypeScript **/
class MyControl extends SAPClass {
    static renderer: any = MyControlRenderer;
    static metadata: any = {
        ...
    };
}

/** TypeScript Output **/
class MyControl extends SAPClass {}
MyControl.renderer = MyControlRenderer;
MyControl.metadata = {
    ...
};

/** Final Output **/
const MyControl = SAPClass.extend('MyControl', {
    renderer: MyControlRenderer,
    metadata: {
        ...
    }
});

CAUTION The plugin does not search for metadata or renderer properties inside the constructor. So don't apply Babel's class property transform plugin before this one if you have metadata/renderer as instance properties (static properties are safe).

Comments

In case of defining a copyright comment in your source code (detected by a leading !) at the first place, the plugin ensures to include it also as leading comment in the generated file at the first place, e.g.:

/*!
 * ${copyright}
 */
import Control from "sap/ui/core/Control";

will be converted to:

/*!
 * ${copyright}
 */
sap.ui.define(["sap/ui/core/Control"], function(Control) { /* ... */ });

In general, comments are preserved, but for each class property/method whose position is changed, only the leading comment(s) are actively moved along with the member. Others may disappear.

Options

Imports

Option Default Description
noImportInteropPrefixes ['sap/'] A list of import path prefixes which never need an import interop.
libs ['.*'] A list of regular-expression patterns (as strings). Only import sources matching at least one pattern are converted into sap.ui.define dependencies; non-matching imports are stripped from the dependency list and placed before the define wrapper. The default '.*' matches every import, preserving existing behaviour.
modulesMap {} Mapping for an import's path. Accepts an object or a function.
suppressImportResolutionHints false When a dependency's .js file extension is stripped because the import resolves to a local file, a hint is logged to the console. Set to true to silence those hints (e.g. to keep build output clean).

Exports

Option Default Description
allowUnsafeMixedExports false No errors for unsafe mixed exports (mix of named and default export where named cannot be collapsed*).
noExportCollapse false Skip collapsing* named exports to the default export.
noExportExtend false Skip assigning named exports to the default export.
exportAllGlobal false Adds the export flag to all sap.ui.define files.

Wrapping

Option Default Description
noWrapBeforeImport false Does not wrap code before the first import (if there are imports).
noWrapQUnitConfigAutostart true Does not wrap QUnit.config.autostart inside the sap.ui.require or sap.ui.define block.
neverUseStrict false Disables the addition of the "use strict"; directive to the program or sap.ui.define callback function.

Class Conversion

Option Default Description
sourceFolders {} Maps source-folder names to namespace prefixes. The file's leading folder segment (relative to sourceRoot / cwd) is looked up in this map; the folder is stripped from the namespace path and the mapped value becomes the prefix. Files whose leading folder is not in the map get an empty namespace and will not be auto-converted. Example: { "src": "my.lib", "test": "my.lib.test" }.
dependencies [] Array of { sourceRoot, sourceFolders?, namespacePrefix? } entries describing dependency projects whose files live outside the primary sourceRoot (e.g. npm-workspace symlinks). The plugin iterates this array for out-of-root files and derives the namespace from the first matching entry. sourceFolders works exactly like the top-level option. When sourceFolders is absent, namespacePrefix is used as a flat prefix for all files in that dep — if neither is set, files in that dep get no namespace and will not be auto-converted. Example: [{ "sourceRoot": "../my-lib", "sourceFolders": { "src": "my.lib" } }].
namespacePrefix '' Prefix prepended to the namespace derived from the file path. When sourceFolders is set, this is used only as a fallback for files not matched by sourceFolders.
autoConvertAllExtendClasses false Converts all classes that extend an imported class. Version 6 default behaviour.
autoConvertUI5FileTypes true Converts classes in files whose name ends with a recognised UI5 file-type suffix (.controller.(js|ts), .view.(js|ts|jsx|tsx)), provided the class extends an imported class and has a resolvable namespace.
autoConvertControllerClass true Set to false to suppress auto-conversion for .controller.* files even when autoConvertUI5FileTypes is enabled.
autoConvertViewClass true Set to false to suppress auto-conversion for .view.* files even when autoConvertUI5FileTypes is enabled.
autoConvertKnownUI5Classes true Converts classes that extend a UI5 base class from a known module path (e.g. sap/ui/core/mvc/Controller, sap/ui/core/Control, sap/ui/base/ManagedObject — see the full list in src/classes/visitor.js), without requiring an explicit @alias or @namespace, provided the class has a resolvable namespace. Transitive inheritance is resolved across files.
additionalUI5Classes [] Extra module paths treated as known UI5 base classes, on top of the built-in list. Works independently of autoConvertKnownUI5Classes. Transitive resolution applies. Example: ["my/app/BaseController"].
neverConvertClass false Never convert classes to .extend() syntax.
moveControllerPropsToOnInit false Moves class properties in a controller to onInit instead of constructor.
moveControllerConstructorToOnInit false Moves existing constructor code in a controller to onInit. Enabling this also enables moveControllerPropsToOnInit. Use the @keepConstructor JSDoc tag on a specific class to opt out.
addControllerStaticPropsToExtend false Moves static props of a controller into the .extend() call. Useful for formatters.
onlyMoveClassPropsUsingThis false Old behaviour: only move instance class props that reference this. New default moves all instance props.
overridesToOverride false Renames the static overrides property to override inside ControllerExtension.extend(), enabling the overrides keyword with older UI5 versions.

* 'collapsing' named exports is a combination of simply ignoring them if their definition is the same as a property on the default export, and also assigning them to the default export.

Note: The plugin also makes use of Babel's standard sourceRoot option for file-path-based namespace computation (see File based namespace above).

Example

openui5-master-detail-app-ts, which is a fork of SAP's openui5-master-detail-app converted to TypeScript.

Credits

  • Thanks to MagicCube for the upstream initial work and to Ryan Murphy for continuing development and handing over the project to the UI5 community.

Contribute

Please do! Open an issue, or file a PR. Issues also welcome for feature requests.

Development Setup

After cloning the repository, run:

npm run setup

This installs all dependencies (including lifecycle scripts needed by some packages) and sets up Husky git hooks for commit linting and pre-commit checks.

Why not just npm install? The project uses ignore-scripts=true in .npmrc to protect against malicious postinstall scripts in dependencies. The setup script explicitly overrides this for the initial trusted install and then configures the git hooks.

License

MIT © 2019-2026 Ryan Murphy, UI5 community and contributors

About

A Babel transformer plugin for OpenUI5/SAPUI5. It allows you to develop UI5 applications by using the latest ECMAScript or TypeScript, including new syntax and objective oriented programming technology.

Topics

Resources

Contributing

Stars

33 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages