Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support sap.ui.require for @sapUiRequire annotated modules #131

Merged
merged 1 commit into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ A more detailed feature list includes:

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

The plugin will wrap any code having import/export statements in an sap.ui.define. If there is no import/export, it won't be wrapped.
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

Expand Down Expand Up @@ -341,6 +341,40 @@ sap.ui.define(["./a"], A => {

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:

```js
/* @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:

```js
"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();
});
});
```

> :warning: 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](https://ui5.sap.com/#/topic/4363b3fe3561414ca1b030afc8cd30ce).

petermuessig marked this conversation as resolved.
Show resolved Hide resolved
### 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.
Expand Down
99 changes: 99 additions & 0 deletions packages/plugin/__test__/__snapshots__/test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,104 @@ exports[`preset-env preset-env-usage.js 1`] = `
});"
`;

exports[`sap-ui-require othermodule-annotation.js 1`] = `
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
"use strict";

Control.extend("my.Control", {});
});"
`;

exports[`sap-ui-require othermodule-annotation-end.js 1`] = `
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
"use strict";

Control.extend("my.Control", {});
});"
`;

exports[`sap-ui-require othermodule-annotation-middle.js 1`] = `
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
"use strict";

Control.extend("my.Control", {});
});"
`;

exports[`sap-ui-require othermodule-annotation-nested.js 1`] = `
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
"use strict";

Control.extend("my.Control", {
onInit: function () {}
});
});"
`;

exports[`sap-ui-require othermodule-noannotation.js 1`] = `
"sap.ui.define(["sap/ui/core/Control"], function (Control) {
"use strict";

Control.extend("my.Control", {});
});"
`;

exports[`sap-ui-require testsuite-annotation.qunit.js 1`] = `
"sap.ui.require([], function () {
"use strict";

function __ui5_require_async(path) {
return new Promise(function (resolve, reject) {
sap.ui.require([path], function (module) {
if (!(module && module.__esModule)) {
module = module === null || !(typeof module === "object" && path.endsWith("/library")) ? {
default: module
} : module;
Object.defineProperty(module, "__esModule", {
value: true
});
}
resolve(module);
}, function (err) {
reject(err);
});
});
}
QUnit.config.autostart = false;
void Promise.all([__ui5_require_async("unit/controller/App.qunit")]).then(() => {
QUnit.start();
});
});"
`;

exports[`sap-ui-require testsuite-noannotation.qunit.js 1`] = `
"sap.ui.define([], function () {
"use strict";

function __ui5_require_async(path) {
return new Promise(function (resolve, reject) {
sap.ui.require([path], function (module) {
if (!(module && module.__esModule)) {
module = module === null || !(typeof module === "object" && path.endsWith("/library")) ? {
default: module
} : module;
Object.defineProperty(module, "__esModule", {
value: true
});
}
resolve(module);
}, function (err) {
reject(err);
});
});
}
QUnit.config.autostart = false;
void Promise.all([__ui5_require_async("unit/controller/App.qunit")]).then(() => {
QUnit.start();
});
});"
`;

exports[`typescript ts-class-anonymous.ts 1`] = `
"sap.ui.define(["sap/Class"], function (SAPClass) {
"use strict";
Expand Down Expand Up @@ -1680,6 +1778,7 @@ exports[`typescript ts-class-controller-extension-extended.ts 1`] = `
return MyExtendedController;
});"
`;

exports[`typescript ts-class-controller-extension-extended-error-1.ts 1`] = `
"ControllerExtension.use() must be called with exactly one argument but has 0
7 | */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Control from "sap/ui/core/Control";

Control.extend("my.Control", {});

/* @sapUiRequire */
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Control from "sap/ui/core/Control";

/* @sapUiRequire */

Control.extend("my.Control", {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Control from "sap/ui/core/Control";

Control.extend("my.Control", {
onInit: function () {
/* @sapUiRequire */
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* @sapUiRequire */

import Control from "sap/ui/core/Control";

Control.extend("my.Control", {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Control from "sap/ui/core/Control";

Control.extend("my.Control", {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* @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();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// 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();
});
55 changes: 44 additions & 11 deletions packages/plugin/src/modules/helpers/wrapper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { types as t } from "@babel/core";
import { types as t, traverse } from "@babel/core";
import * as eh from "./exports";
import * as th from "../../utils/templates";
import * as ast from "../../utils/ast";
Expand Down Expand Up @@ -158,23 +158,27 @@ export function wrap(visitor, programNode, opts) {
body.unshift(th.buildDefaultImportInterop());
}

const define = generateDefine(
// should we use sap.ui.require instead of sap.ui.define?
let useSapUiRequire = hasUseSapUiRequire(visitor.parent.comments, body, true);

// generate the sap.ui.define or sap.ui.require
const defineOrRequire = generateDefineOrRequire(
body,
imports,
exportGlobal || opts.exportAllGlobal,
hasUseStrict(programNode)
useSapUiRequire
);

// add the "use strict" directive if not on program node
if (!opts.neverUseStrict && !hasUseStrict(programNode)) {
const defineFnBody = define.expression.arguments[1].body;
defineFnBody.directives = [
const defineOrRequireFnBody = defineOrRequire.expression.arguments[1].body;
defineOrRequireFnBody.directives = [
t.directive(t.directiveLiteral("use strict")),
...(defineFnBody.directives || []),
...(defineOrRequireFnBody.directives || []),
];
}

programNode.body = [...preDefine, define];
programNode.body = [...preDefine, defineOrRequire];

// if a copyright comment is present we append it to the new program node
if (copyright && visitor.parent) {
Expand All @@ -189,13 +193,42 @@ function hasUseStrict(node) {
);
}

function generateDefine(body, imports, exportGlobal) {
function hasUseSapUiRequire(comments, body, remove) {
// detect the @sapUiRequire comment
return comments.some((comment) => {
petermuessig marked this conversation as resolved.
Show resolved Hide resolved
let found = false;
// check for existing comment block
if (comment.type === "CommentBlock") {
found = comment.value.trim() === "@sapUiRequire";
}
// remove the comment (if it is somewhere in the body)
if (found && remove) {
body?.forEach((node) => {
traverse(node, {
enter(path) {
["leadingComments", "trailingComments", "innerComments"].forEach(
(key) => {
path.node[key] = path.node[key]?.filter((c) => c !== comment);
}
);
},
noScope: true,
});
});
}
return found;
});
}

function generateDefineOrRequire(body, imports, exportGlobal, useRequire) {
const defineOpts = {
SOURCES: t.arrayExpression(imports.map((i) => t.stringLiteral(i.src))),
PARAMS: imports.map((i) => t.identifier(i.tmpName)),
BODY: body,
};
return exportGlobal
? th.buildDefineGlobal(defineOpts)
: th.buildDefine(defineOpts);
return useRequire
? th.buildRequire(defineOpts)
: exportGlobal
? th.buildDefineGlobal(defineOpts)
: th.buildDefine(defineOpts);
}
6 changes: 6 additions & 0 deletions packages/plugin/src/utils/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export const buildAssign = template(`
OBJECT.NAME = VALUE;
`);

export const buildRequire = template(`
sap.ui.require(SOURCES, function (PARAMS) {
BODY;
});
`);

export const buildDefine = template(`
sap.ui.define(SOURCES, function (PARAMS) {
BODY;
Expand Down
Loading