Giter VIP home page Giter VIP logo

ui5-community / babel-plugin-transform-modules-ui5 Goto Github PK

View Code? Open in Web Editor NEW
34.0 7.0 16.0 2.54 MB

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.

License: MIT License

JavaScript 90.89% TypeScript 8.99% Shell 0.11%
babel-plugin openui5 ui5

babel-plugin-transform-modules-ui5's Introduction

babel transform-ui5

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

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

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
        }]
    ]
}

At the time of writing the babel version is 7.3, which does not natively support class property syntax. To use that syntax also add the plugin @babel/plugin-syntax-class-properties.

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.

NOTE: The class transform might be split into its own plugin in the future.

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 (name, namespace, alias)
    • Decorators (name, namespace, alias)
    • 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.

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:

"sap.ui.require([], function () {
  "use strict";

  function __ui5_require_async(path) { /* ... */ }
  QUnit.config.autostart = false;
  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.

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

By default, the plugin converts ES classes to Control.extend(..) syntax if the class extends from a class which has been imported. So a class without a parent will not be converted to .extend() syntax.

There are a few options or some metadata you can use to control this.

Configuring Name or Namespace

The plugin provides a few ways to set the class name or namespace used in the SAPClass.extend(...) call.

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.

The plugin also supports supplying a namespace prefix in this mode, in case the desired namespace root is not a directory in the filesystem.

In order to pass the namespace prefix, pass it as a plugin option, and not a top-level babel option. Passing plugin options requires the array format for the plugin itself (within the outer plugins array).

{
    "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``.

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

@name and @alias behave the same; @name was used originally but the @alias JSDoc property is used in UI5 source code, so support for that was added.

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

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

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

Will all output:

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

Alternatively, you can use decorators to override the namespace or name used. The same properties as JSDoc will work, but instead of a space, pass the string literal to the decorator function.

NOTE that using a variable is currently not supported.

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

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

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

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

Don't convert class

If you have a class which extends from an import that you don't want to convert to .extend(..) syntax, you can add the @nonui5 (case insensitive) jsdoc or decorator to it. Also see Options below for overriding the default behaviour.

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 with 'Controller' in the name or namespace, or having the JSDoc @controller will be treated as a controller.

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 currently search in the constructor (but will in the future) 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 currently 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

  • noImportInteropPrefixes (Default ['sap/']) A list of import path prefixes which never need an import inter-opt.
  • modulesMap (Default {}) Mapping for an import's path. Accepts object or function.

Exports

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

Wrapping

  • noWrapBeforeImport (Default: false) Does not wrap code before the first import (if there are imports).

Class Conversion

  • namespacePrefix (Default: '') Prefix to apply to namespace derived from directory.
  • autoConvertAllExtendClasses (Default false). Converts all classes by default, provided they extend from an imported class. Version 6 default behaviour.
  • autoConvertControllerClass (Default true). Converts the classes in a .controller.js file by default, if it extends from an imported class. Use @nonui5 if there are multiple classes in a controller file which extend from an import.
  • neverConvertClass (Default: false) Never convert classes to SAPClass.extend() syntax.
  • moveControllerPropsToOnInit (Default: false) Moves class props in a controller to the onInit method instead of constructor.
  • moveControllerConstructorToOnInit (Default: false) Moves existing constructor code in a controller to the onInit method. Enabling will auto-enable moveControllerPropsToOnInit.
  • addControllerStaticPropsToExtend (Default: false) Moves static props of a controller to the extends call. Useful for formatters.
  • onlyMoveClassPropsUsingThis (Default: false) Set to use old behavior where only instance class props referencing this would be moved to the constructor or onInit. New default is to always move instance props.
  • overridesToOverride (Default: false) Changes the name of the static overrides to override when being added to ControllerExtension.extend() allowing to use the new overrides keyword with older UI5 versions
  • neverUseStrict (Default: false) Disables the addition of the "use strict"; directive to the program or sap.ui.define callback function.

* '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.

** This plugin also makes use of babel's standard sourceRoot option.

TODO: more options and better description.

Other Similar Plugins

sergiirocks babel-plugin-transform-ui5 is a great choice if you use webpack. It allows you to configure which import paths to convert to sap.ui.define syntax and leaves the rest as ES2015 import statements, which allows webpack to load them in. This plugin will have that functionality soon. Otherwise this plugin handles more edge cases with named exports, class conversion, and typescript output support.

Example

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

Building with Webpack

Take a look at ui5-loader (we have not tried this).

Modularization / Preload

UI5 supports Modularization through a mechanism called preload, which can compile many JavaScript and xml files into just one preload file.

Some preload plugins:

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.

TODO

  • libs support, like sergiirocks'
  • See if we can export a live binding (getter?)
  • Configuration options
    • Export interop control
    • Others..

Contribute

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

License

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

babel-plugin-transform-modules-ui5's People

Contributors

akudev avatar dependabot[bot] avatar guillaumedespommaresap avatar magiccube avatar nlunets avatar petermuessig avatar r-murphy avatar tobiasqueck avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

babel-plugin-transform-modules-ui5's Issues

Class method conversion can cause conflicts and infinite callstack

The following class converts in a way that causes the method to call itself rather than the external function.

class MyClass extends UI5Class {
  doSomething() {
    doSomething(); // call a standalone function with the same name
  }
}

// Called by MyClass method
function doSomething() {
 // something
}

Converts to:

const MyClass = UI5Class.extend("MyClass", {
   doSomething: function doSomething() {
     doSomething(); // calls the named function assigned to the object prop, rather than the external function
  }
});

// Never called!
function doSomething() {
 // something
}

The functions assigned to the props don't actually need named, but they can be useful for stack traces. So I'll use the babel functionality to generate a unique id from a given id.

Support for require / define with module name

Hello.

Besides controller classes etc. we also have a "main" class that is not loaded bye the UI5 module loader framework, but basically with a <script> tag. (This is because we are developing a widget for sap analytics cloud - which includes our main JS file like this and instantiates the widgets as an HTML component.)

This is how the transpiled code looks like:
sap.ui.define([], function () {
...
/**

  • @namespace ...
    */
    class XYZWidget extends HTMLElement {

This is giving an error in the console:
Modules that use an anonymous define() call must be loaded with a require() call; they must not be executed via script tag or nested into other modules. All other usages will fail in future releases or when standard AMD loaders are used or when ui5loader runs in async mode. Now using substitute name anonymous1.js - sap.ui.ModuleSystem

While the generated define call is the same for controllers as well, controllers are "required" by other modules, and during this process the module system can determine the module name automatically - even if the module name is not specified explicitly in the define() call of the module itself.

I think for flexibility it would make sense to provide an option to control:

  1. Whether define or require is generated during transpilation
  2. The explicit module name to be used for the define()

Probably decorators on the default export could be used for that?

What do you think?

Best regards,
Oliver

Babel 7 upgrade

Hi @r-murphy,

as Babel 7 is now available, do you plan a plugin upgrade in order to remove compatibility warning?
https://babeljs.io/docs/en/v7-migration
I've never developped a Babel plugin, so not sure I can easily find what to do regarding breaking changes, but doesn't appear to have much issues, as I'm already using this plugin with babel 7 :-)

Fix import of modules named with `-`

In my project, I import a dependency with a - in its name. I don't do this directly, as it is an external non-ui5 library, so I have set up a ui5 shim consisting of a js file and a dts file for the types (eventually moving to the modules tooling is the plan, but here we are):

// unleash.js
sap.ui.loader.config({
  shim: {
    "myapp/lib/unleash-proxy-client/main.min": {
      amd: true,
      exports: "unleash"
    }
  }
});
sap.ui.define(["myapp/lib/unleash-proxy-client/main.min"], unleash => unleash);
// unleash.d.ts
export * from "unleash-proxy-client";

This hasn't caused problems so far, as the dts file is ignored in the babelrc, as per the sample application. Now, with the new karma-ui5-transpile plugin (which works really nice despite this), the following error in the coverage preprocessor of karma pops up:

webapp\lib\unleash.d.ts: Unexpected token, expected "," (3:59)

  1 | "use strict";
  2 |
> 3 | sap.ui.define(["unleash-proxy-client"], function (__unleash-proxy-client) {
    |                                                            ^       
  4 |   var __exports = {
  5 |     __esModule: true
  6 |   };

This means that the babel-plugin-transform-modules-ui5 generates an invalid identifier __unleash-proxy-client for the dependency. I think the issue might be fixed easily by just replacing - with _ in https://github.com/ui5-community/babel-plugin-transform-modules-ui5/blob/main/packages/plugin/src/modules/visitor.js#L7. There is already cleanImportSource which does something like this, but I don't know why this isn't called.

Perhaps this isn't a problem of the babel plugin altogether, but a problem of the karma plugin (which shouldn't try to transpile the dts file), in which case I can also open an issue over at the other repository as well -- please just let me know!

Thank you for the great work with the UI5 typescript support!

Handling Circular Dependencies

Hi @petermuessig,

whenever custom TypeScript sources contain circular dependencies (e.g. A imports B and B imports A), then any UI5 app will throw an exception, saying that one of those dependencies is undefined.

The reason is that all import statements are transformed to sap.ui.define which cannot handle circular dependencies.
Using sap.ui.require would work in those cases.

One might argue that circular dependencies should be prevented in the first place, but I have an example (in combination with odata2ts) for which circular dependencies are a valid use case (actually, OData allows for that by virtue of its navigation model).

Workaround: Dynamic Imports

The workaround that would come to mind is using dynamic imports in the custom TS files which would then be transpiled to sap.ui.require.

However, that would be really cumbersome and intransparent for users... in my case, generating TS artefacts, it's additionally hard to nigh impossible.

Example App

Here's an example app

'const exports' templates are not transformed by preset-env

Hello,

I am trying to transform a Typescript enum into a ui5/js resource compatible with IE11 ^^

For that I need this babel configuration:

{
    "ignore": [
        "**/*.d.ts"
    ],
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": ["last 2 Chrome versions", "last 2 Firefox versions", "last 2 Edge versions", "ie >= 11"],
                "debug": true
            }
        ],
        "transform-ui5",
        "@babel/preset-typescript"
    ],
    "plugins": [
        [
            "@babel/plugin-proposal-class-properties"
        ],
        [
            "@babel/plugin-syntax-decorators",
            {
                "legacy": true
            }
        ]
    ]
}

The TS file is defined as follows:

export enum SessionFailure {

    INVALID_LOGIN_OR_PASSWORD = 0
}

And the resulting js is:

"use strict";

sap.ui.define([], function () {
  var SessionFailure;

  (function (SessionFailure) {
    SessionFailure[SessionFailure["INVALID_LOGIN_OR_PASSWORD"] = 0] = "INVALID_LOGIN_OR_PASSWORD";
  })(SessionFailure || (SessionFailure = {}));

  const __exports = {
    __esModule: true
  };
  __exports.SessionFailure = SessionFailure;
  return __exports;
});

My issue is that preset-env is tranforming all my const or let declarations into var. No issue with that. Even SessionFailure itself is transformed. BUT "const __exports = {" is not taken into account. Which causes many issue in further processing steps in my build.

I know that const exports is a template from this preset. I am not sure if here I am making a bug report or only a bad use of this preset. Could you help me ?

Thanks.
Guillaume

Imports with a dash in the name throws an error.

When attempting to import a module for side effects only the transormation will throw an error.
IE: Doing this type of import:

import "MyWorkOrdersVeg/vendor/pouchdb/pouchdb";
import  "MyWorkOrdersVeg/vendor/pouchdb/pouchdb-find";
import "MyWorkOrdersVeg/vendor/pouchdb/pouchdb-upsert";

will throw this error:

SyntaxError: unknown: Unexpected token, expected , (1:169)
> 1 | sap.ui.define(["bchui5/util/BCHLogger", "bchui5/util/common", "pouchdb", "pouchdb-upsert", "pouchdb-find"], function (BCHLogger, bchui5_util_common, __pouchdb, __pouchdb-upsert, __pouchdb-find) {
    |                                                                                                                                                                          ^
  2 |     const guid = bchui5_util_common["guid"];
  3 | 
  4 |     const PouchDB = window.PouchDB;
    at Parser.pp$5.raise (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:4454:13)
    at Parser.pp.unexpected (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:1761:8)
    at Parser.pp.expect (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:1749:33)
    at Parser.pp$2.parseBindingList (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3101:12)
    at Parser.pp$1.parseFunctionParams (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:2395:22)
    at Parser.pp$1.parseFunction (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:2385:8)
    at Parser.pp$3.parseFunctionExpression (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3760:17)
    at Parser.pp$3.parseExprAtom (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3722:19)
    at Parser.pp$3.parseExprSubscripts (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3494:19)
    at Parser.pp$3.parseMaybeUnary (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3474:19)
    at Parser.pp$3.parseExprOps (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3404:19)
    at Parser.pp$3.parseMaybeConditional (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3381:19)
    at Parser.pp$3.parseMaybeAssign (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3344:19)
    at Parser.pp$3.parseExprListItem (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:4312:16)
    at Parser.pp$3.parseCallExpressionArguments (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3573:20)
    at Parser.pp$3.parseSubscripts (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3533:31)
    at Parser.pp$3.parseExprSubscripts (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3504:15)
    at Parser.pp$3.parseMaybeUnary (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3474:19)
    at Parser.pp$3.parseExprOps (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3404:19)
    at Parser.pp$3.parseMaybeConditional (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3381:19)
    at Parser.pp$3.parseMaybeAssign (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3344:19)
    at Parser.pp$3.parseExpression (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:3306:19)
    at Parser.pp$1.parseStatement (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:1906:19)
    at Parser.pp$1.parseBlockBody (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:2268:21)
    at Parser.pp$1.parseTopLevel (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:1778:8)
    at Parser.parse (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:1673:17)
    at parse (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babylon/lib/index.js:7305:37)
    at File.parse (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babel-core/lib/transformation/file/index.js:517:15)
    at File.parseCode (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babel-core/lib/transformation/file/index.js:602:20)
    at /Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babel-core/lib/transformation/pipeline.js:49:12
    at File.wrap (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babel-core/lib/transformation/file/index.js:564:16)
    at Pipeline.transform (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/node_modules/babel-core/lib/transformation/pipeline.js:47:17)
    at Object.transpileFileAsync (/Users/leonho/Documents/BCH/bch-dev-tools/dt-transpiler/lib/handlers/js.js:104:17)
    at <anonymous>:null:null

However if I hack it by exporting it to a random variable it will work:

import * as _ignore1 from "MyWorkOrdersVeg/vendor/pouchdb/pouchdb";
import * as _ignore2 from "MyWorkOrdersVeg/vendor/pouchdb/pouchdb-find";
import * as _ignore3 from "MyWorkOrdersVeg/vendor/pouchdb/pouchdb-upsert";
import BCHLogger from "bchui5/util/BCHLogger";
import { guid } from "bchui5/util/common";
import * as _ignore4 from "pouchdb";
import * as _ignore5 from "pouchdb-upsert";
import * as _ignore6 from "pouchdb-find";
console.log(_ignore1, _ignore2, _ignore3);
const PouchDB = (window as any).PouchDB as PouchDB.Static;

Class without ClassName does not work

Hi,

I'm facing issue regarding the creation of class inside a file without class name.

Example:
... return new (class implements IClipboardContent { getClipboardContentType() { ...

An error occurs:

TypeError: _<filepath>_.ts: Cannot destructure property 'name' of 'node.id' as it is undefined.
    at PluginPass.enter (_<Project path>_\node_modules\babel-plugin-transform-modules-ui5\dist\classes\visitor.js:34:15)

By adding a class name, it works fine.
return new (class ClipboardContent implements IClipboardContent { getClipboardContentType() {
Best regards.

Issue when exporting default anonymous function or object w/ types

Hello,

I have a strange behavior with this preset in conjunction with Typescript when generating default export for functions.

if a function is exported as default in a file :

export default (date: Date): string => {
    return "";
};

then output in js is incorrect with types remaining where they should be gone

sap.ui.define([], function () {

  const __exports = (date: Date): string => {
    return "";
  };

  return __exports;
});

If method is not default, for instance :

export function test(date: Date): string {
    return "";
}

then output is valid

sap.ui.define([], function () {
  function test(date) {
    return "";
  }

  __exports.test = test;
  return __exports;
});

I'm using preset version 7.0.0-rc.7 with @babel/core: 7.3.3 and @babel/preset-typescript: 7.3.3

.babelrc looks like :

{
    "ignore": [
        "**/*.d.ts"
    ],
    "presets": [
        "transform-ui5",
        "@babel/preset-typescript"
    ],
    "plugins": [
        [
            "@babel/plugin-syntax-decorators",
            {
                "legacy": true
            }
        ]
    ]
}

And if I remove transform-ui5 from presets list, default function has no more types.

Hope this issue is clear enough. I will test with latest version of the preset later today (rc8)

New feature: configurable mapping from ES6 module names to UI5 import names

I've been thinking about how to handle polyfills in my UI5 application. #14 and #15 are issues I stumbled across during my experiments, but #14 would become irrelevant if this is implemented and #15 is tied to an experimental feature in @babel/preset-env and probably not a good long-term solution anyways.

I would like to get this setup to work:

  • configure @babel/preset-env with useBuiltIns: false,
  • add import "@babel/polyfill" at the top of Component.js,
  • copy @babel/polyfill/dist/browser.min.js into my project and, finally:
  • let babel-plugin-transform-modules-ui5 rewrite the canonical polyfill module name into the relative URL of the packaged polyfill library.

For example:

import "@babel/polyfill"
import UIComponent from "sap/ui/core/UIComponent"

/** @namespace app */
export default class Component extends UIComponent {
  //...
}

Desired result:

sap.ui.define(["./vendor/browser-polyfill", "sap/ui/core/UIComponent"], function (__vendor_browser_polyfill, UIComponent) {
  // ...
});

To achieve this, the preset could take a new option:

let babelOptions = {
  presets: [
    ["transform-ui5", {
      modulesMap: {
        ["@babel/polyfill"]: "./vendor/browser-polyfill"
      }
    }]
  ]
}

Supporting a mapper function would be nice but should not be required due to complications with other packages (e.g. broccoli-babel-transpiler requires that the babel options object is serializable).

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Files with TS only assets still get transpiled

Problem: Suppose a TS app has a interfaces directory. After transpilation to JS, those files are too transpiled but left empty. UI5 tooling then bundles them but that's another thing.

The predefined setup does not handle this case out-of-the box. I've searched for babel plugins / settings but the results were not very good.

Babel does have ignore property, but when --copy-files is enabled, then the interfaces are properly not processed but still copied in the dist, which is bad. There's also this issue but the additional flag also does not function perfectly.

To handle the issue therefore an additional build step can be added (e.g filtering or deleting unneeded files).

Before doing this however, I wanted to know whether there's possibility to add this behavior in this plugin. Otherwise additional customization steps for the building of TS UI5 apps need to be taken.

Thanks for any info in advance & BR

Move Babel helpers into the sap.ui.define wrapper if possible

Babel replaces certain keywords with function calls when the semantics have changed between language versions. For example, typeof(id) gets transpiled to _typeof(id) and function _typeof(obj) { /* magic */ } (which handles typeof(Symbol()) === "symbol"s in older browsers) is injected near the top of the file.

The builder from @ui5/project expects that modules have an empty top level scope except for the sap.ui.define call. However, the babel-created _typeof function counts as a symbol in that scope; this yields a warning message:

WARN lbt:bundle:Builder **** warning: module app/Component.js requires top level scope and can only be embedded as a string (requires 'eval')

This is probably not a very serious error. "Requires 'eval'" refers to the UI5 loader having to eval a string read from the generated preload file, so there's no security issue either.

Still, would it be possible to move these Babel-generated functions into the sap.ui.define factory if all other code was wrapped into the factory anyways? Is this perhaps related to #15 and the injected imports?

Plugin ordering issue with super.method() calls inside an arrow function

Issue encountered when running with babel-preset-env when it loads the arrow function transform.

Specifically something like this code where the super call is inside an arrow function.

class Dialog extends SAPDialog {
  fn() {
    setTimeout(() => {
      super.fn();
    }}, 1000);
  }
}

Outputs (where this is wrong):

var D2 = SAPDialog.extend("bchui5.m.D2", {
    fn: function fn() {
        setTimeout(function () {
            SAPDialog.prototype.close.call(this);
        }, 1000);
    }
});

But should be:

var Dialog = SAPDialog.extend("bchui5.m.D2", {
    fn: function fn() {
        var _this = this;
        setTimeout(function () {
            SAPDialog.prototype.close.call(_this);
        }, 1000);
    }
});

Currently the arrow function transform seems to run first.

Only convert ES classes into Control.extend(..) syntax when controller.js suffix is used

Right now, the default behavior is that every class with extends property is converted to a
Control.extend(...) syntax for UI5 compatibility.

From my understanding :

  • Control.extend(...) is a mechanism dedicated to UI controls.
  • A controller by convention from UI5 is suffixed by .controller.js
  • a @nonui5 annotation has to be used to keep classical extends usage.

My proposal would be to use a by default convention where :

  • all js files with suffix .controller.js with extends have to be converted to UI5 syntax
  • all other files must be kept as it, as they probably aren't controllers.

Maybe there is a good reason why you choosed to use your plugin like that ? @r-murphy Thanks for the great work, it is really valuable, especially in conjunction with Typescript !

Issues with multiple exports where some definitions have been cleaned by other plugins

Context:

I'm using this plugin with https://babeljs.io/docs/en/babel-preset-typescript preset, writing code in Typescript. My configuration is using two presets :

  1. the typescript preset in order to convert our typescript code into javascript code
  2. Your plugin, encapsulated into a preset in order to be ran after typescript preset.

I was forced to do so, otherwise some dependencies/exports were not properly removed (i.e. interfaces were removed from files but still imported) as the typescript preset is no more able to shake tree if sap.require transformations already occurred.

Issue with exports :
This plugin does not seem to take into account previously removed exports. Let's take an example :

Initial Example.ts file

export const MY_CONSTANT = "constant";
export interface IMyInterface { id: string; }

If using the typescript preset only, the output Example.js file should look like :

export const MY_CONSTANT = "constant";

Interface is removed, because it has no meaning in Javascript code.

But, if using typescript preset, then your plugin, the output would look like :

sap.ui.define([], function () {
  const MY_CONSTANT = "constant";
  const __exports = {
    __esModule: true
  };
  __exports.MY_CONSTANT = MY_CONSTANT ;
  __exports.IMyInterface = IMyInterface ;
  return __exports;
});

The interface exports is restored, but no definition of the interface can be found. Is there any way in Babel from your plugin to check that a expression has been removed by another plugin prior to export step ? I'm still really novice with Babel plugin mechanism, so hard to find easy answer here.

Thanks for your help !

Support for parameter properties (shorthand constructor)

At the moment this plugin doesn't seem to support parameter properties (explained here).

Imagine the following code:

export default class BaseFragment extends ManagedObject {

    view: View;

    constructor(public controller: BaseController) {
        super();
        this.view = controller.getView();
    }
}

This should automatically generate a public property controller and assign the value which was passed to the constructor.

This works for regular classes but as soon as I inherit from UI5 classes (and the code will be transpiled to sap.ui.define(...)) I receive the error Property params[0] of FunctionExpression expected node to be of a type ["Identifier","Pattern","RestElement"] but instead got "TSParameterProperty".

Property getters and setters

ES6 defines classes with property getters / setters:

class Dummy {
  get thing() { return this._thing }
  set thing(value) { this._thing = value }
}

Unfortunately, classes handled by the UI5 transformation are missing out on a lot of Babel's class features (see e.g. #23), and getters/setters are among those. The plugin produces a class declaration such as this:

var Dummy = ManagedObject.extend("dummy.Dummy", {
  // ...
  get thing() { /* ... */ },
}

There are two problems with this:

  1. This won't work with IE11. Babel's preset-env handles getters/setters in normal ES6 classes, but fails to fix this; ordering of presets in the Babel config doesn't affect the result.
  2. UI5 will iterate over all properties of the object given to extend which "accidentally" triggers execution of the getter with this being the aforementioned object instead of an instance of the class.

Would it be possible to change the UI5 transformation so that it only creates a skeleton class through UI5's .extend with the metadata, renderer and constructor set, and delegates creation/declaration of everything else to the usual Babel mechanisms?

'super' not being replaced when extending UI5 class

If I create a custom control extending a UI5 control like this:

import ComboBox from "sap/m/ComboBox";

@namespace("sap.test")
class Son extends ComboBox {
  constructor(something) {
    super(something);
  }

  public method(): any {
    super.init(this, arguments);
  }

  public setSelectedKey(selectedKey: string): any {
    super.setSelectedKey.apply(this, arguments);
  }
}

The transform-ui5 module is expected to use the ComboBox.extend syntax and replace my super calls for ComboBox.prototype calls, but this doesn't happen when calling a method with apply. The result for the snippet above is this:

sap.ui.define(["sap/m/ComboBox"], function (ComboBox) {
  const Son = ComboBox.extend("sap.test.Son", {
    constructor: function _constructor(something) {
      ComboBox.prototype.constructor.call(this, something);
    },
    method: function _method() {
      ComboBox.prototype.init.call(this, this, arguments);
    },
    setSelectedKey: function _setSelectedKey(selectedKey) {
      super.setSelectedKey.apply(this, arguments);
    }
  });
});

I have an example on this repo: https://github.com/lucasheim/transform-ui5-playground

`moveControllerPropsToOnInit` should not move explicit constructor content

When the moveControllerPropsToOnInit option is enabled and a custom constructor is present at the same time, the entire content of the constructor is moved to onInit():

Input:

import Controller from "sap/ui/core/mvc/Controller"
/** @namespace example */
export default class AppController extends Controller {
  input = this.byId("input")
  constructor(id, opts) {
    logger.trace("before super")
    super(id, opts)
    logger.trace("after super")
  }
}

Output:

"use strict";

sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
  /** @namespace example */
  var AppController = Controller.extend("example.AppController", {
    onInit: function onInit() {
      this.input = this.byId("input");
      logger.trace("before super");
      logger.trace("after super");

      if (typeof Controller.prototype.onInit === 'function') {
        Controller.prototype.onInit.apply(this, arguments);
      }
    }
  });
  return AppController;
});

While it's understandably necessary to move constructor content after super to where it is executed after the moved props have been initialized, statements that appear before super should remain in the constructor and run before the call to the super constructor.

An example where this is required is when trying to pass data from the HTML page through a component to the application's root view - i.e., I would like the transpiled code to look something like this:

UIComponent.extend("my.Component", {
  constructor: function(id, settings) {
    this._componentSettings = settings;
    UIComponent.apply(this, arguments);
  },
  createContent: function() {
    var settings = this._componentSettings;
    delete this._componentSettings;
    var rootView = this.getMetadata().getRootView();
    rootView.viewData = settings;
    return sap.ui.view(rootView);
  }
});

Fantastic work on the plugin, by the way. With this in an easy-to-use build pipeline I can finally force inspire my colleagues to learn modern JS! 😈

'export type' being transpiled

When I write a typescript file like this:

export type testType = string | boolean;

export class testClass {
  public testMethod() {
    console.log("test");
  }
}

And run it using babel's typescript-preset, it transpiles to this:

export class testClass {
  testMethod() {
    console.log("test");
  }

}

As the type is something only important to Typescript, it shouldn't be transpiled. But, when I go through typescript-preset and then through transform-modules-ui5, this is the result:

sap.ui.define([], function () {
  class testClass {
    testMethod() {
      console.log("test");
    }

  }

  var __exports = {
    __esModule: true
  };
  __exports.testType = testType;
  __exports.testClass = testClass;
  return __exports;
});

The type ends up being transpiled: __exports.testType = testType;, even though it's not on Typescript's transpiling. Maybe there's a need to test the exports of a file before outputting them to JS, as testType ends up having no reference in the file and we get a runtime error.

I have an example on this repo: https://github.com/lucasheim/transform-ui5-playground

Support for UI5-converted classes with class decorators

I'm working on a concise, easily readable API for class mixins, such as this:

import Controller from "sap/ui/core/mvc/Controller"
import { mixin } from "../lib/decorators"
import RoutingSupport from "../lib/RoutingSupport"

@namespace("example.controller")
@mixin(RoutingSupport)
export default class AppController extends Controller { /* ... */ }

mixin is a simple wrapper that returns a decorator function; the decorator function should eventually be called by Babel's decorators API and receives a descriptor which it uses to register a finisher. The finisher function then gets called with the class' constructor function as its argument and... well, this part is still very much WIP. 😊

Unfortunately, babel-plugin-transform-classes-ui5 doesn't support the very first step: while it respects the @namespace decorator and triggers UI5-transformation for the class, the @mixin decorators are ignored and Babel's decorators API is not invoked.

Any chance to get this working?


FWIW, Babel desugars a decorated class into something like this:

var TheClass = _decorate([mixin], function(_initialize, _BaseClass) {
  var TheClass = function(_BaseClass2) {
    /* CONSTRUCTOR FACTORY BODY */
  }(_BaseClass);

  return {
    F: TheClass,
    d: [ /* PROPERTY/METHOD DESCRIPTORS */ ]
  };
}, BaseClass);

The first commented-out section is basically the usual class constructor factory, with the big difference being that method and property definitions are moved into the second commented-out section, each wrapped into a descriptor object so that the decorators API can process them further.

This indicates to me that if support for method/property decorators is omitted until some future version, supporting just class decorators should™ be fairly straight forward - just pull in Babel's decorators machinery and feed it the constructor function returned by UI5's extend...

Class props using 'this' should be injected into constructor or onInit.

Class props using 'this' should be injected into constructor or onInit.

Currently they are added to the extend object, where 'this' has no meaning.

class A extends B {
  thing = this.getThing();
}

Current:

const A = B.extend('A', {
  thing: this.getThing(), // invalid use of 'this'
}

Expected:

const A = B.extend('A', {
  constructor: function() {
    this.thing = this.getThing();
  }
}

Module imports generated by other Babel plugins are ignored

When configured to do so, @babel/preset-env can emit import or require statements at the top of each transpiled file to provide exactly those polyfills the code requires.

Example:

export default class Example {
  buildMap() { return new Map().set("foo", "bar") }
}

Babel options:

let babelOptions = {
  plugins: ["@babel/proposal-class-properties"],
  presets: [
    ["@babel/env", {
      targets: {
        chrome: "64",
        firefox: "58",
        ie: "11"
      },
      modules: false,
      useBuiltIns: "usage"
    }],
    ["transform-ui5", {
      autoConvertControllerClass: false,
      moveControllerPropsToOnInit: true
    }]
  ]
}

(Note: useBuiltIns: "usage" is an experimental feature of @babel/preset-env.)

Result:

import "core-js/modules/web.dom.iterable";
import "core-js/modules/es6.array.iterator";
import "core-js/modules/es6.string.iterator";
import "core-js/modules/es6.map";

// ...

sap.ui.define([], function () {
  // ...
});

I have tried to reorder the "@babel/env" and "transform-ui5" presets in my Babel configuration, but babel-plugin-transform-modules-ui5 appears to be unable to transform the imports generated by other plugins.

Importing certain modules cause syntax errors in transpiled output

When importing modules such as import "@babel/polyfill", the local variable name generated by babel-plugin-transform-modules-ui5 is not correct syntax:

Example:

import "@babel/polyfill"

Result:

sap.ui.define(["@babel/polyfill"], function(__@babel_polyfill) {
  // ...
});

The @ is not allowed here.

Can't use with babel-preset-env

When using with babel-preset-env and babel-plugin-syntax-class-properties, the transpiling operation fails:

  • if using class properties, Babel returns the following error message :

    Missing class properties transform

  • otherwise, UI5 classes are still transpiled as if they were standard JS classes

.babelrc :

{
	"sourceMaps" : true,
	"minified" : false,
	"presets" : [
		["env", {
			"debug": true,
			"modules" : false
		}]
	],
	"plugins" : ["syntax-class-properties","transform-modules-ui5"]
}

Computed props are not correctly moved

Computed props are not correctly moved during conversion. The identifier of the computed prop is used directly as the prop name.

Input:

const name = Symbol("_name");
class Test extends Controller {
  [name] = 1;
  static [name] = 2;
}

Output:

const name = Symbol("_name");
const Test = Controller.extend("my.TestController", {
    constructor: function constructor() {
        Controller.prototype.constructor.apply(this, arguments);
        this.name = 1;
    }
});
Test.name = 2;

Expected

const name = Symbol("_name");
const Test = Controller.extend("my.TestController", {
      constructor: function constructor() {
        Controller.prototype.constructor.apply(this, arguments);
        this[name] = 1;
    }
});
Test[name] = 2;

modules with @scope end up in dependency load failure

hey there!

i am using the plugin to transform my typescript code to ui5 which works pretty well.
But i have one issue:

We're hosting a onPremise git for the npm registry, which works with @scopes.
So my package is @scope/package-name.

As soon as i import the package and try to run the page, i'll get the following error:

The following error occurred while displaying routing target with name 'Overview': ModuleError: Failed to resolve dependencies of '<Application>/controller/Overview.controller.js' -> '@<scope>/<module>.js': failed to load '@<scope>/<module>.js' from https://sapui5.hana.ondemand.com/resources/@<scope>/<module>.js: script load error -
For sure, my package does not exist in the sapui5 hana ondemand resources :-)

The generated define also includes the non existing module:
sap.ui.define(["../model/Formatter", "./BaseController.controller", "@<scope>/<module>", "sap/ui/core/mvc/Controller"], function (__Formatter, __BaseController, __Validator, Controller) {

Any idea how to get this working again with @scope-modules?

Thanks!

BR
Pascal

Allow feature `moveControllerPropsToOnInit` to be overriden with a local annotation, per declaration

Hello,

In our projects we always activate the feature moveControllerPropsToOnInit=true so that controls are assigned in onInit function time so that their programmatic configuration is possible.

But some UI5 patterns, like formatters in our case, would require an exception. A formatter pattern as UI5 expects it is defined in a formatter variable defined in the controller. Of course they are other ways of defining it, but this is one of the most convenient, a lot more than core:require of functions. In TypeScript, this would look like :

@namespace("ccCockpit.tree.node.comparator.timeSlot.controller")
export default class ElseController extends DisplayBaseController {

    public formatter = {
        formatElseMessageStrip
    };

    constructor(name: string | object[]) {
        super(name);
    }
}

Control in View:
<MessageStrip text="{parts: ['Node>/property/key', {value: 'valueCategory_i18n'}], formatter: '.formatter.formatElseMessageStrip'}" />

Issue: doing so is most probably the better way of writing a formatter, it is aligned with UI5 coding conventions, samples ... but assignment code is moved to onInit and it looks to be late for UI5 binding when creating the control, and formatter remains undefined in the control's binding

So maybe a feature like:

   @movePropsToConstructor
    public formatter = {
        formatElseMessageStrip
    };

Could help in such situation.

File based namespace does not work

I tried not to use JSDoc @namespace or decorator @namespace() and also specified the sourceRoot setting in .babelrc but my class was not converted to BaseClass.extend(...) form.

I have debugged the plugin and have found that function shouldConvertClass does not check classInfo.fileNamespace in the following if statement:

  if (
    classInfo.name ||
    classInfo.alias ||
    classInfo.controller ||
    classInfo.namespace
  ) {
    return true;
  }

If I add classInfo.fileNamespace:

  if (
    classInfo.name ||
    classInfo.alias ||
    classInfo.controller ||
    classInfo.namespace ||
    classInfo.fileNamespace // <- this is added
  ) {
    return true;
  }

then my class is converted.

"noWrapBeforeImport" option fails for imports without interop

noWrapBeforeImport (back then called minimalWrapping) was introduced here.

It works for the testcase because an interop is needed and hence a "deconstructor" is created.
Only with a deconstructor, firstImport is set and the deconstructors replace the import in the body.

The wrapping logic only runs with firstImport and uses the first deconstructor in the body as marker for the first import.

With a file like this, however, there is no interop (because "sap/" is the default for no-interop), no "firstImport" and no marker in the body where the first import appears:

const x = 1; // This should not be part of sap-ui-define - but wrongly is

import Button from "sap/m/Button";

const b = new Button(); // This gets wrapped

Hence the result is wrong - the first line is wrapped into the sap.ui.define call:

sap.ui.define(["sap/m/Button"], function (Button) {
  const x = 1; // This should not be part of sap-ui-define - but wrongly is

  const b = new Button(); // This gets wrapped
});

There is also NO WAY to figure out where the first import is (to only wrap the subsequent lines) because the import is completely removed from the body (replaced by zero "deconstructors").

I am willing to create a PR (actually trying to), but could use some guidance what approach would be accepted: e.g. mark the first node after the first import by just setting some property and use this mark for knowing where to split the code? Or generate some dummy node to the body at the location of the first import, which is later on removed again? Any other way how this should be done?

By the way, would you agree that noWrapBeforeImport should actually be the default behavior in a new major version? It feels just more logical, I think. (see below)

Missing Static Class Props

Hi,

We would like to use XML Composite in our code, but right now only some static props are handled, especially metadata and renderer. For XMLComposite, we would need in addition at least fragment, as shown in the linked constructor.

The easy way would be to add it to the classes:
at the two places :
https://github.com/r-murphy/babel-plugin-transform-modules-ui5/blob/b95fbfeb0da8937d79aad50d16eb91cbdddb2cba/packages/plugin/src/classes/helpers/classes.js#L58
https://github.com/r-murphy/babel-plugin-transform-modules-ui5/blob/b95fbfeb0da8937d79aad50d16eb91cbdddb2cba/packages/plugin/src/classes/helpers/classes.js#L118

but maybe being able to configure such behavior would help ?

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

[FEATURE REQUEST] Change the architecture/lifecycle of the plugin (run in exit phase)

As mentioned by @Elberet in #23 and #25, to benefit from other plugins transformation process, the plugin should run in the Program#exit phase. This ensures that e.g. custom code for TypeScript isn't needed to process e.g. the constructor parameter properties, or to let the decorator plugin handle them. But therefore the plugin still needs to partially run in the Program#enter phase to collect informations about imports, custom decorators or any processing relevant information which may get lost during the transformation phase.

Cannot transform sap.ui.define when no function is provided

Hello,

I noticed that the babel-plugin-transform-modules-ui5 cannot transform such definition which is valid from a UI5 perspective (even if a sap.ui.require could be an alternative !)

sap.ui.define("", [
    "foo/bar/MyResource"
]); 

Error:

TypeError: C:\project\test\unit\AllTests.ts: Cannot read property 'params' of undefined
    at getRequiredParamsOfSAPUIDefine (C:\project\node_modules\babel-plugin-transform-modules-ui5\dist\classes\visitor.js:154:23)
    at PluginPass.CallExpression (C:\project\node_modules\babel-plugin-transform-modules-ui5\dist\classes\visitor.js:111:32)
    at NodePath._call (C:\project\node_modules\@babel\traverse\lib\path\context.js:53:20)
    at NodePath.call (C:\project\node_modules\@babel\traverse\lib\path\context.js:40:17)
    at NodePath.visit (C:\project\node_modules\@babel\traverse\lib\path\context.js:90:31)
    at TraversalContext.visitQueue (C:\project\node_modules\@babel\traverse\lib\context.js:110:16)
    at TraversalContext.visitSingle (C:\project\node_modules\@babel\traverse\lib\context.js:79:19)
    at TraversalContext.visit (C:\project\node_modules\@babel\traverse\lib\context.js:138:19)
    at Function.traverse.node (C:\project\node_modules\@babel\traverse\lib\index.js:76:17)
    at NodePath.visit (C:\project\node_modules\@babel\traverse\lib\path\context.js:97:18) {
  code: 'BABEL_TRANSFORM_ERROR'
}

You can find a workardound by defining it that way:

sap.ui.define("", [
      "foo/bar/MyResource"
], () => {
    // Empty
});

Issue is located in babel-plugin-transform-modules-ui5\dist\classes\visitor.js

And I made a local fix by checking checking callbackNode against undefined.

function getRequiredParamsOfSAPUIDefine(path, node) {
  const defineArgs = node.arguments;
  const callbackNode = defineArgs.find(argNode => _core.types.isFunction(argNode));
  **HERE** 
  return callbackNode.params; // Identifier
}

But I don't know how the plugin itself works and if this is viable.
Do you think a fix could be provided. Using sap.ui.define with no content helps when for example you are running OPA tests with karma. They can be loaded, but you do not need to define a function. Maybe there are other use cases.

Thanks

namespacePrefix does not work with sourceRoot

The documentation is not correct about the usage or the sourceRoot property. It show the property as relative to the babel configuration file:

https://github.com/r-murphy/babel-plugin-transform-modules-ui5/blob/f4554f7a304879961a1f9cd097c5f5acf3bfe712/README.md#L360-L368

The problem is that the filename is absolute in this function so filename.startsWith(sourceRoot) is always false

https://github.com/r-murphy/babel-plugin-transform-modules-ui5/blob/f4554f7a304879961a1f9cd097c5f5acf3bfe712/packages/plugin/src/classes/helpers/classes.js#L271-L286

Also, if we set the sourceRoot property to path.join(__dirname, 'src') the dir variable equals to [""] for files at the root. In the end, the namespace has an extra dot.

Instance property initializers aren't always moved to the constructor when necessary

Consider this example:

import * as Util from "./lib/util"
import Controller from "sap/ui/core/mvc/Controller"
@namespace("app")
export default class AppController extends Controller {
  static metadata = { /* ... */ }
  onClick = Util.debounce(100, () => this.onClickDebounced())
  onClickDebounced() {
    console.log("click!")
  }
}

In the example above, the onClick initializer is not moved into the constructor:

sap.ui.define([/* ... */], function(Controller, Util) {
  var _this = this;

  var App = Controller.extend("app.AppController", {
    // ...
    onClick: Util.debounce(100, function() {
      return _this.onClickDebounced();
    })
    // ...
  });
  return App;
});

From what I can see, the initializer is moved only when this is accessed in the initializer's scope. When this is accessed in a lambda expression (which binds this lexically and thus inherits it from the outer scope), the initializer isn't moved and gets transpiled as though it were on the UI5 factory method's scope.

I have a sneaking suspicion that this is related to the changed discussed in #15 as well, because as long as the argument to debounce() does not have to be transpiled, the initializer gets moved as intended. For example, onClick = Util.debounce(100, function(){ this.onClickDebounced(); }.bind(this)) works correctly...

Compilation error with recent version of Babel parser

Hi,

We recently upgraded babel stack and started to have issues with Typescript integration. Basically, if a file only contains type declarations, like :

export interface IMyInterface {
    id: string;
}

then Babel will type it as an ExportNamedDeclaration

(see this fixture with input and output)

However, from babel plugin transform UI5 side, it seems that such node is not skipped, leading to an error and impossibility to compile :

SyntaxError: C:\<my-interface>.ts: Unknown ExportNamedDeclaration shape. (This is an error on an internal node. Probably an internal error.)
    at File.buildCodeFrameError (C:\<my-project>\node_modules\@babel\core\lib\transformation\file\file.js:244:12)
    at NodePath.buildCodeFrameError (C:\<my-project>\node_modules\@babel\traverse\lib\path\index.js:133:21)
    at PluginPass.ExportNamedDeclaration (C:\<my-project>\node_modules\babel-plugin-transform-modules-ui5\dist\modules\visitor.js:244:18)
    at newFn (C:\<my-project>\node_modules\@babel\traverse\lib\visitors.js:171:21)
    at NodePath._call (C:\<my-project>\node_modules\@babel\traverse\lib\path\context.js:53:20)
    at NodePath.call (C:\<my-project>\node_modules\@babel\traverse\lib\path\context.js:40:17)
    at NodePath.visit (C:\<my-project>\node_modules\@babel\traverse\lib\path\context.js:90:31)
    at TraversalContext.visitQueue (C:\<my-project>\node_modules\@babel\traverse\lib\context.js:110:16)
    at TraversalContext.visitSingle (C:\<my-project>\node_modules\@babel\traverse\lib\context.js:79:19)
    at TraversalContext.visit (C:\<my-project>\node_modules\@babel\traverse\lib\context.js:138:19) {
  code: 'BABEL_TRANSFORM_ERROR'

For me, a quick win would be to skip visiting such node from the plugin. For instance, here we should add a check for the node type, like :

 if (node.type === "ExportNamedDeclaration") {
    return;
  }

Maybe this is a bit to simple and would need some refinements ?

Any help would be welcomed, as I'm not confident with my AST parsing comprehension :-)

Best regards

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.