Skip to content

Commit

Permalink
feat: support sap.ui.require for @sapUiRequire annotated modules (#131)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
petermuessig authored Jul 22, 2024
1 parent afd2f7c commit 23c4ac6
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 12 deletions.
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).
### 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) => {
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

0 comments on commit 23c4ac6

Please sign in to comment.