Skip to content

Commit 23c4ac6

Browse files
authored
feat: support sap.ui.require for @sapUiRequire annotated modules (#131)
QUnit testsuites using `QUnit.config.autostart = false` and `QUnit.start()` loaded via <script> tags must be loaded using `sap.ui.require`. With this change modules can be marked using `sap.ui.require` by using the annotation `/* @sapUiRequire */` in the program code.
1 parent afd2f7c commit 23c4ac6

File tree

11 files changed

+225
-12
lines changed

11 files changed

+225
-12
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ A more detailed feature list includes:
9090

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

93-
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.
93+
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.
9494

9595
#### Static Import
9696

@@ -341,6 +341,40 @@ sap.ui.define(["./a"], A => {
341341
342342
Also refer to the `neverUseStrict` option below.
343343
344+
### Top-Level Scripts (e.g. QUnit Testsuites)
345+
346+
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.
347+
348+
Example:
349+
350+
```js
351+
/* @sapUiRequire */
352+
353+
// https://api.qunitjs.com/config/autostart/
354+
QUnit.config.autostart = false;
355+
356+
// import all your QUnit tests here
357+
void Promise.all([import("unit/controller/App.qunit")]).then(() => {
358+
QUnit.start();
359+
});
360+
```
361+
362+
will be converted to:
363+
364+
```js
365+
"sap.ui.require([], function () {
366+
"use strict";
367+
368+
function __ui5_require_async(path) { /* ... */ }
369+
QUnit.config.autostart = false;
370+
void Promise.all([__ui5_require_async("unit/controller/App.qunit")]).then(() => {
371+
QUnit.start();
372+
});
373+
});
374+
```
375+
376+
> :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).
377+
344378
### Converting ES classes into Control.extend(..) syntax
345379
346380
By default, the plugin converts ES classes to `Control.extend(..)` syntax if the class extends from a class which has been imported.

packages/plugin/__test__/__snapshots__/test.js.snap

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,104 @@ exports[`preset-env preset-env-usage.js 1`] = `
16041604
});"
16051605
`;
16061606
1607+
exports[`sap-ui-require othermodule-annotation.js 1`] = `
1608+
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
1609+
"use strict";
1610+
1611+
Control.extend("my.Control", {});
1612+
});"
1613+
`;
1614+
1615+
exports[`sap-ui-require othermodule-annotation-end.js 1`] = `
1616+
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
1617+
"use strict";
1618+
1619+
Control.extend("my.Control", {});
1620+
});"
1621+
`;
1622+
1623+
exports[`sap-ui-require othermodule-annotation-middle.js 1`] = `
1624+
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
1625+
"use strict";
1626+
1627+
Control.extend("my.Control", {});
1628+
});"
1629+
`;
1630+
1631+
exports[`sap-ui-require othermodule-annotation-nested.js 1`] = `
1632+
"sap.ui.require(["sap/ui/core/Control"], function (Control) {
1633+
"use strict";
1634+
1635+
Control.extend("my.Control", {
1636+
onInit: function () {}
1637+
});
1638+
});"
1639+
`;
1640+
1641+
exports[`sap-ui-require othermodule-noannotation.js 1`] = `
1642+
"sap.ui.define(["sap/ui/core/Control"], function (Control) {
1643+
"use strict";
1644+
1645+
Control.extend("my.Control", {});
1646+
});"
1647+
`;
1648+
1649+
exports[`sap-ui-require testsuite-annotation.qunit.js 1`] = `
1650+
"sap.ui.require([], function () {
1651+
"use strict";
1652+
1653+
function __ui5_require_async(path) {
1654+
return new Promise(function (resolve, reject) {
1655+
sap.ui.require([path], function (module) {
1656+
if (!(module && module.__esModule)) {
1657+
module = module === null || !(typeof module === "object" && path.endsWith("/library")) ? {
1658+
default: module
1659+
} : module;
1660+
Object.defineProperty(module, "__esModule", {
1661+
value: true
1662+
});
1663+
}
1664+
resolve(module);
1665+
}, function (err) {
1666+
reject(err);
1667+
});
1668+
});
1669+
}
1670+
QUnit.config.autostart = false;
1671+
void Promise.all([__ui5_require_async("unit/controller/App.qunit")]).then(() => {
1672+
QUnit.start();
1673+
});
1674+
});"
1675+
`;
1676+
1677+
exports[`sap-ui-require testsuite-noannotation.qunit.js 1`] = `
1678+
"sap.ui.define([], function () {
1679+
"use strict";
1680+
1681+
function __ui5_require_async(path) {
1682+
return new Promise(function (resolve, reject) {
1683+
sap.ui.require([path], function (module) {
1684+
if (!(module && module.__esModule)) {
1685+
module = module === null || !(typeof module === "object" && path.endsWith("/library")) ? {
1686+
default: module
1687+
} : module;
1688+
Object.defineProperty(module, "__esModule", {
1689+
value: true
1690+
});
1691+
}
1692+
resolve(module);
1693+
}, function (err) {
1694+
reject(err);
1695+
});
1696+
});
1697+
}
1698+
QUnit.config.autostart = false;
1699+
void Promise.all([__ui5_require_async("unit/controller/App.qunit")]).then(() => {
1700+
QUnit.start();
1701+
});
1702+
});"
1703+
`;
1704+
16071705
exports[`typescript ts-class-anonymous.ts 1`] = `
16081706
"sap.ui.define(["sap/Class"], function (SAPClass) {
16091707
"use strict";
@@ -1680,6 +1778,7 @@ exports[`typescript ts-class-controller-extension-extended.ts 1`] = `
16801778
return MyExtendedController;
16811779
});"
16821780
`;
1781+
16831782
exports[`typescript ts-class-controller-extension-extended-error-1.ts 1`] = `
16841783
"ControllerExtension.use() must be called with exactly one argument but has 0
16851784
7 | */
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Control from "sap/ui/core/Control";
2+
3+
Control.extend("my.Control", {});
4+
5+
/* @sapUiRequire */
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Control from "sap/ui/core/Control";
2+
3+
/* @sapUiRequire */
4+
5+
Control.extend("my.Control", {});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Control from "sap/ui/core/Control";
2+
3+
Control.extend("my.Control", {
4+
onInit: function () {
5+
/* @sapUiRequire */
6+
},
7+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* @sapUiRequire */
2+
3+
import Control from "sap/ui/core/Control";
4+
5+
Control.extend("my.Control", {});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Control from "sap/ui/core/Control";
2+
3+
Control.extend("my.Control", {});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* @sapUiRequire */
2+
3+
// https://api.qunitjs.com/config/autostart/
4+
QUnit.config.autostart = false;
5+
6+
// import all your QUnit tests here
7+
void Promise.all([import("unit/controller/App.qunit")]).then(() => {
8+
QUnit.start();
9+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// https://api.qunitjs.com/config/autostart/
2+
QUnit.config.autostart = false;
3+
4+
// import all your QUnit tests here
5+
void Promise.all([import("unit/controller/App.qunit")]).then(() => {
6+
QUnit.start();
7+
});

packages/plugin/src/modules/helpers/wrapper.js

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { types as t } from "@babel/core";
1+
import { types as t, traverse } from "@babel/core";
22
import * as eh from "./exports";
33
import * as th from "../../utils/templates";
44
import * as ast from "../../utils/ast";
@@ -158,23 +158,27 @@ export function wrap(visitor, programNode, opts) {
158158
body.unshift(th.buildDefaultImportInterop());
159159
}
160160

161-
const define = generateDefine(
161+
// should we use sap.ui.require instead of sap.ui.define?
162+
let useSapUiRequire = hasUseSapUiRequire(visitor.parent.comments, body, true);
163+
164+
// generate the sap.ui.define or sap.ui.require
165+
const defineOrRequire = generateDefineOrRequire(
162166
body,
163167
imports,
164168
exportGlobal || opts.exportAllGlobal,
165-
hasUseStrict(programNode)
169+
useSapUiRequire
166170
);
167171

168172
// add the "use strict" directive if not on program node
169173
if (!opts.neverUseStrict && !hasUseStrict(programNode)) {
170-
const defineFnBody = define.expression.arguments[1].body;
171-
defineFnBody.directives = [
174+
const defineOrRequireFnBody = defineOrRequire.expression.arguments[1].body;
175+
defineOrRequireFnBody.directives = [
172176
t.directive(t.directiveLiteral("use strict")),
173-
...(defineFnBody.directives || []),
177+
...(defineOrRequireFnBody.directives || []),
174178
];
175179
}
176180

177-
programNode.body = [...preDefine, define];
181+
programNode.body = [...preDefine, defineOrRequire];
178182

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

192-
function generateDefine(body, imports, exportGlobal) {
196+
function hasUseSapUiRequire(comments, body, remove) {
197+
// detect the @sapUiRequire comment
198+
return comments.some((comment) => {
199+
let found = false;
200+
// check for existing comment block
201+
if (comment.type === "CommentBlock") {
202+
found = comment.value.trim() === "@sapUiRequire";
203+
}
204+
// remove the comment (if it is somewhere in the body)
205+
if (found && remove) {
206+
body?.forEach((node) => {
207+
traverse(node, {
208+
enter(path) {
209+
["leadingComments", "trailingComments", "innerComments"].forEach(
210+
(key) => {
211+
path.node[key] = path.node[key]?.filter((c) => c !== comment);
212+
}
213+
);
214+
},
215+
noScope: true,
216+
});
217+
});
218+
}
219+
return found;
220+
});
221+
}
222+
223+
function generateDefineOrRequire(body, imports, exportGlobal, useRequire) {
193224
const defineOpts = {
194225
SOURCES: t.arrayExpression(imports.map((i) => t.stringLiteral(i.src))),
195226
PARAMS: imports.map((i) => t.identifier(i.tmpName)),
196227
BODY: body,
197228
};
198-
return exportGlobal
199-
? th.buildDefineGlobal(defineOpts)
200-
: th.buildDefine(defineOpts);
229+
return useRequire
230+
? th.buildRequire(defineOpts)
231+
: exportGlobal
232+
? th.buildDefineGlobal(defineOpts)
233+
: th.buildDefine(defineOpts);
201234
}

0 commit comments

Comments
 (0)