Skip to content

Commit

Permalink
feat(ui5-tooling-modules): support webc scoping and enablement (#1083)
Browse files Browse the repository at this point in the history
* feat(ui5-tooling-modules): support webc scoping and enablement

* fix(ui5-tooling-modules): disable busy indicator enrichment

* feat(ui5-tooling-modules): add support for ariaLabelledBy, add form sample

* feat(ui5-tooling-modules): add config options to control webc rollup plugin

---------

Co-authored-by: Thorsten Hochreuter <[email protected]>
  • Loading branch information
petermuessig and Thodd authored Oct 1, 2024
1 parent 2d273ba commit 87dfc9f
Show file tree
Hide file tree
Showing 14 changed files with 629 additions and 89 deletions.
23 changes: 23 additions & 0 deletions packages/ui5-tooling-modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,38 @@ The following configuration options are relevant for the `task` and the `middlew
- 'fs'
- '*'
```
&nbsp;

- *chunksPath*: `boolean|string`
Relative path for the chunks to be stored into (if value is `true`, chunks are put into the closest modules folder which was the default behavior in the `3.5.x` release of the tooling extension)
&nbsp;

- *legacyDependencyResolution*: `boolean`
Re-enables the legacy dependency resolution of the tooling extension which allows to use entry points from `devDependencies` of the project. By default, only the `dependencies` maintained in the projects' `package.json` and the transitive dependencies are considered for the entry points and all other entry points are ignored. (available since new minor version `3.7.0` which introduces a new dependency resolution for `dependencies` only)
&nbsp;

- *minify*: `boolean` *experimental feature*
Flag to indicate that the generated code should be minified (in case of excluding thirdparty resources from minification in general, this option can be used to minify just the generated code)
&nbsp;

The following configuration options are relevant for the `task` and the `middleware` which allow you to directly configure rollup plugins which is used as follows:

```yaml
configuration:
pluginOptions: # map of plugin options
webcomponents: # the name of the rollup plugin
skip: true # configuration
```

The available plugin configuration options are:

- *pluginOptions.webcomponents.skip*: `boolean`
Flag to skip the transformation of Web Components to UI5 Controls a.k.a. *Seamless Web Components* support. This allows to directly require the Web Components modules from NPM packages and use them as UI5 Controls. The NPM packages providing Web Components must include a [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) declared in the `customElements` field in the `package.json`. (defaults to `false`)
&nbsp;

- *pluginOptions.webcomponents.scoping*: `boolean`
Flag to disable the [Custom Elements Scoping](https://sap.github.io/ui5-webcomponents/docs/advanced/scoping/) of UI5 Web Components. This allows to load multiple versions of UI5 Web Components into a single application without conflicts. This feature is enabled by default and can be disabled if needed. (defaults to `true`)
&nbsp;

The following configuration options are just relevant for the `task`:

Expand Down
38 changes: 33 additions & 5 deletions packages/ui5-tooling-modules/lib/rollup-plugin-webcomponents.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
const { join, dirname } = require("path");
const { readFileSync, existsSync } = require("fs");
const { createHash } = require("crypto");

const WebComponentRegistry = require("./utils/WebComponentRegistry");

const { compile } = require("handlebars");
const { lt, gte } = require("semver");

module.exports = function ({ log, resolveModule, framework, skip } = {}) {
// TODO:
// - enabled - disabled mapping
// - Externalize UI5 Web Components specific code
module.exports = function ({ log, resolveModule, framework, options } = {}) {
// derive the configuration from the provided options
let { skip, scoping, enrichBusyIndicator } = Object.assign({ skip: false, scoping: true, enrichBusyIndicator: false }, options);

// TODO: maybe we should derive the minimum version from the applications package.json
// instead of the framework version (which might be a different version)
if (!gte(framework?.version || "0.0.0", "1.120.0")) {
Expand All @@ -25,6 +33,12 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
return compile(templateFile);
};

// helper function to create a short hash (to scope the UI5 Web Components)
const createShortHash = ({ name, version }) => {
return createHash("shake256", { outputLength: 4 }).update(`${name}@${version}`).digest("hex");
};
const ui5WebCScopeSuffix = !!scoping && createShortHash(require(join(process.cwd(), "package.json")));

// handlebars templates for the Web Components transformation
const webcTmplFnPackage = loadAndCompileTemplate("templates/Package.hbs");
const webcTmplFnControl = loadAndCompileTemplate("templates/WrapperControl.hbs");
Expand Down Expand Up @@ -97,14 +111,14 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {

// helper function to lookup a Web Component class by its module name
const lookupWebComponentsClass = (source, emitFile) => {
// determine npm package
const npmPackage = getNpmPackageName(source);

let clazz;
if ((clazz = WebComponentRegistry.getClassDefinition(source))) {
if (npmPackage !== "@ui5/webcomponents-base" && (clazz = WebComponentRegistry.getClassDefinition(source))) {
return clazz;
}

// determine npm package
const npmPackage = getNpmPackageName(source);

const registryEntry = loadNpmPackage(npmPackage, emitFile);
if (registryEntry) {
const metadata = registryEntry;
Expand Down Expand Up @@ -209,6 +223,9 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
namespace,
enums: lib.enums,
dependencies: lib.dependencies,
isBaseLib: namespace === "@ui5/webcomponents-base",
scopeSuffix: ui5WebCScopeSuffix,
enrichBusyIndicator,
});
// include the monkey patches for the Web Components base library
// only for UI5 versions < 1.128.0 (otherwise the monkey patches are not needed anymore)
Expand All @@ -219,11 +236,22 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
return code;
} else if (moduleInfo.attributes.ui5Type === "control") {
let clazz = moduleInfo.attributes.clazz;
// determine whether the clazz is based on the UI5Element superclass
let superclass = clazz.superclass,
isUI5Element = false;
while (superclass) {
if (superclass?.name === "UI5Element") {
isUI5Element = true;
break;
}
superclass = superclass.superclass;
}
// Extend the superclass with the WebComponent class and export it
const ui5Metadata = clazz._ui5metadata;
const ui5Class = `${ui5Metadata.namespace}.${clazz.name}`;
const namespace = ui5Metadata.namespace;
const metadataObject = Object.assign({}, ui5Metadata, {
tag: isUI5Element && ui5WebCScopeSuffix ? `${ui5Metadata.tag}-${ui5WebCScopeSuffix}` : ui5Metadata.tag, // only add the suffix for UI5 Web Components (scoping support)
library: `${ui5Metadata.namespace}.library`, // if not defined, the library is derived from the namespace
designtime: `${ui5Metadata.namespace}/designtime/${clazz.name}.designtime`, // add a default designtime
});
Expand Down
14 changes: 14 additions & 0 deletions packages/ui5-tooling-modules/lib/templates/Package.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
{{#if isBaseLib}}
{{#if scopeSuffix}}
import "@ui5/webcomponents-base/dist/features/OpenUI5Support.js";
import { setCustomElementsScopingSuffix } from "@ui5/webcomponents-base/dist/CustomElementsScope.js";
setCustomElementsScopingSuffix("{{scopeSuffix}}");
{{/if}}

{{#if enrichBusyIndicator}}
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import OpenUI5Enablement from "@ui5/webcomponents-base/dist/features/OpenUI5Enablement.js";
OpenUI5Enablement.enrichBusyIndicatorSettings(UI5Element);
{{/if}}
{{/if}}

import { registerEnum } from "sap/ui/base/DataType";
{{#each dependencies}}
import "{{this}}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default WebComponentBaseClass.extend("{{ui5Class}}", {
// TODO: Quick solution to fix a conversion between "number" and "core.CSSSize".
// WebC attribute is a number and is written back to the Control wrapper via core.WebComponent base class.
// The control property is defined as a "sap.ui.core.CSSSize".
setProperty: function(sPropName, v, bSupressInvalidate) {
setProperty: function(sPropName, v, bSupressInvalidate) {
if (sPropName === "width" || sPropName === "height") {
if (!isNaN(v)) {
v += "px";
Expand Down
12 changes: 6 additions & 6 deletions packages/ui5-tooling-modules/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -782,13 +782,13 @@ module.exports = function (log, projectInfo) {
* @param {string[]} config.mainFields an order of main fields to check in package.json
* @param {rollup.InputPluginOption[]} [config.beforePlugins] rollup plugins to be executed before
* @param {rollup.InputPluginOption[]} [config.afterPlugins] rollup plugins to be executed after
* @param {object} [config.pluginOptions] configuration options for the rollup plugins (e.g. webcomponents)
* @param {string} [config.generatedCode] ES compatibility of the generated code (es5, es2015)
* @param {object} [config.inject] the inject configuration for @rollup/plugin-inject
* @param {boolean} [config.isMiddleware] flag if the getResource is called by the middleware
* @param {boolean} [config.skipTransformWebComponents] flag to skip the transformation of web components to UI5 controls
* @returns {rollup.RollupOutput} the build output of rollup
*/
createBundle: async function createBundle(moduleNames, { cwd, depPaths, mainFields, beforePlugins, afterPlugins, generatedCode, inject, isMiddleware, skipTransformWebComponents } = {}) {
createBundle: async function createBundle(moduleNames, { cwd, depPaths, mainFields, beforePlugins, afterPlugins, pluginOptions, generatedCode, inject, isMiddleware } = {}) {
const { walk } = await import("estree-walker");
const bundle = await rollup.rollup({
input: moduleNames,
Expand Down Expand Up @@ -830,7 +830,7 @@ module.exports = function (log, projectInfo) {
return that.resolveModule(moduleName, { cwd, depPaths, mainFields });
},
framework: projectInfo?.framework,
skip: skipTransformWebComponents,
options: pluginOptions?.["webcomponents"],
}),
// once the node polyfills are injected, we can
// resolve the modules from node_modules
Expand Down Expand Up @@ -890,12 +890,12 @@ module.exports = function (log, projectInfo) {
* and modules which are returned as bundling information.
* @param {string|string[]} moduleNames name of the module (e.g. "chart.js/auto") or an array of module names
* @param {object} [config] configuration
* @param {object} [config.pluginOptions] configuration options for the rollup plugins (e.g. webcomponents)
* @param {boolean} [config.skipCache] skip the module cache
* @param {boolean} [config.persistentCache] flag whether the cache should be persistent
* @param {boolean} [config.debug] debug mode
* @param {boolean|string} [config.chunksPath] the relative path for the chunks to be stored (defaults to "chunks", if value is true, chunks are put into the closest modules folder)
* @param {boolean|string[]} [config.skipTransform] flag or array of globs to verify whether the module transformation should be skipped
* @param {boolean} [config.skipTransformWebComponents] flag to skip the transformation of web components to UI5 controls
* @param {boolean|string[]} [config.keepDynamicImports] List of NPM packages for which the dynamic imports should be kept or boolean (defaults to true)
* @param {string} [config.generatedCode] ES compatibility of the generated code (es5, es2015)
* @param {string} [config.minify] minify the code generated by rollup
Expand All @@ -908,7 +908,7 @@ module.exports = function (log, projectInfo) {
*/
getBundleInfo: async function getBundleInfo(
moduleNames,
{ skipCache, persistentCache, debug, chunksPath, skipTransform, skipTransformWebComponents, keepDynamicImports, generatedCode, minify, inject } = {},
{ pluginOptions, skipCache, persistentCache, debug, chunksPath, skipTransform, keepDynamicImports, generatedCode, minify, inject } = {},
{ cwd, depPaths, isMiddleware } = {},
) {
cwd = cwd || process.cwd();
Expand Down Expand Up @@ -982,7 +982,7 @@ module.exports = function (log, projectInfo) {
minify,
inject,
isMiddleware,
skipTransformWebComponents,
pluginOptions,
};
if (modules.length === 1) {
options.afterPlugins.push(dynamicImports({ moduleName: modules[0].name, keepDynamicImports }));
Expand Down
36 changes: 29 additions & 7 deletions packages/ui5-tooling-modules/lib/utils/WebComponentRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,16 @@ class RegistryEntry {
let parsedType = typeInfo?.text;
if (parsedType?.indexOf("|") > 0) {
const types = parsedType.split("|").map((s) => s.trim());
// "htmlelement | string" is an association, e.g. the @ui5-webcomponents/Popover#opener
if (types[0] === "HTMLElement" && types[1] === "string") {

// case 1: "htmlelement | string" is an association, e.g. the @ui5-webcomponents/Popover#opener
if (types.includes("HTMLElement") && types.includes("string")) {
return {
isAssociation: true,
origType: "HTMLElement",
ui5Type: "sap.ui.core.Control",
};
}

// UI5 normally accepts only one type for a property, except if "any" is used
// in this case we just use the first one as the primary type
parsedType = types[0];
Expand All @@ -152,7 +154,7 @@ class RegistryEntry {

// complex types have a reference to other things, e.g. enums
if (typeInfo?.references) {
// case 1: enum type -> easy
// case 2: enum type -> easy
if (this.enums[parsedType]) {
return {
origType: parsedType,
Expand All @@ -161,7 +163,7 @@ class RegistryEntry {
};
}

// case 2: interface type -> theoretically this should this be a 0..n aggregation... but really?
// case 3: interface type -> theoretically this should this be a 0..n aggregation... but really?
const interfaceOrClassType = this.#checkForInterfaceOrClassType(parsedType);

if (interfaceOrClassType) {
Expand All @@ -176,7 +178,7 @@ class RegistryEntry {
};
}

// case 3: check for cross package type reference
// case 4: check for cross package type reference
const refPackage = WebComponentRegistry.getPackage(typeInfo.references[0]?.package);
if (refPackage?.enums?.[parsedType]) {
return {
Expand All @@ -193,7 +195,7 @@ class RegistryEntry {
multiple,
};
} else {
// primitive types
// case 5: primitive types
return {
origType: parsedType,
ui5Type: this.#normalizeType(parsedType),
Expand Down Expand Up @@ -222,7 +224,27 @@ class RegistryEntry {
#processMembers(classDef, ui5metadata, propDef) {
// field -> property
if (propDef.kind === "field") {
const ui5TypeInfo = this.#extractUi5Type(propDef.type);
let ui5TypeInfo = this.#extractUi5Type(propDef.type, propDef.name);

// [ Accessibility ]
// 1. ACC attributes have webc internal typing and will be defaulted to "object" ob UI5 side.
// 2. "accessibleNameRef" must be mapped to "ariaLabelledBy"
if (propDef.name === "accessibilityAttributes") {
ui5TypeInfo.ui5Type = "object";
ui5TypeInfo.isUnclear = false;
} else if (propDef.name === "accessibleNameRef") {
ui5metadata.associations["ariaLabelledBy"] = {
type: "sap.ui.core.Control",
multiple: true,
mapping: {
type: "property",
to: "accessibleNameRef",
// formatter is implemented in sap.ui.core.webc.WebComponent
formatter: "_getAriaLabelledByForRendering",
},
};
return;
}

// DEBUG
if (ui5TypeInfo.isUnclear) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5048,10 +5048,6 @@ sap.ui.define(['sap/ui/base/DataType', 'sap/base/strings/hyphenate', 'sap/ui/cor
"tag": "ui5-checkbox",
"interfaces": [],
"properties": {
"accessibleNameRef": {
"type": "string",
"mapping": "property"
},
"accessibleName": {
"type": "string",
"mapping": "property"
Expand Down Expand Up @@ -5119,7 +5115,17 @@ sap.ui.define(['sap/ui/base/DataType', 'sap/base/strings/hyphenate', 'sap/ui/cor
"multiple": true
}
},
"associations": {},
"associations": {
"ariaLabelledBy": {
"type": "sap.ui.core.Control",
"multiple": true,
"mapping": {
"type": "property",
"to": "accessibleNameRef",
"formatter": "_getAriaLabelledByForRendering"
}
}
},
"events": {
"change": {}
},
Expand All @@ -5132,7 +5138,7 @@ sap.ui.define(['sap/ui/base/DataType', 'sap/base/strings/hyphenate', 'sap/ui/cor
// TODO: Quick solution to fix a conversion between "number" and "core.CSSSize".
// WebC attribute is a number and is written back to the Control wrapper via core.WebComponent base class.
// The control property is defined as a "sap.ui.core.CSSSize".
setProperty: function(sPropName, v, bSupressInvalidate) {
setProperty: function(sPropName, v, bSupressInvalidate) {
if (sPropName === "width" || sPropName === "height") {
if (!isNaN(v)) {
v += "px";
Expand Down
Loading

0 comments on commit 87dfc9f

Please sign in to comment.