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(ui5-tooling-modules): seamless web components support phase 1 #1051

Merged
merged 60 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
ce85746
SEAMLESS WEB COMPONENTS
petermuessig Jul 26, 2024
2fd2dca
rollup-plugin-webcomponents
petermuessig Jul 30, 2024
772793d
Cleanup of rollup plugin
petermuessig Jul 31, 2024
241f070
Include the new Web Components Metadata processor
petermuessig Jul 31, 2024
cb48817
Introduce library for type registration
petermuessig Aug 1, 2024
d3351ce
Add sample for enum usage and live-change event
Thodd Aug 1, 2024
655a26b
Generate Library
petermuessig Aug 2, 2024
709a8d4
Refactor WebComponentRegistry
Thodd Aug 2, 2024
d82b691
Externalized templates
petermuessig Aug 6, 2024
d0e7e2e
Patch missing ValueState enum
Thodd Aug 9, 2024
d9bf951
Fix defaultValue casting to the correct JS type
Thodd Aug 9, 2024
8e07385
Resolve dependencies for metadata
petermuessig Aug 9, 2024
28d6980
Fix typo
petermuessig Aug 9, 2024
d80cd0c
Always use custom-elements-internal.json
petermuessig Aug 9, 2024
81807b7
Peter needs to debug
Thodd Aug 9, 2024
622ecf7
Bundling works again PUUUH
petermuessig Aug 9, 2024
8f002c9
Merge remote-tracking branch 'origin/main' into feat/SeamlessWebCompo…
petermuessig Aug 12, 2024
f7e2d8f
chore: cleanup of Main.controller.ts
petermuessig Aug 12, 2024
d09ab16
Merge remote-tracking branch 'origin/main' into feat/SeamlessWebCompo…
petermuessig Aug 13, 2024
b942a3e
chore: make the ui5-app work with the new approach
petermuessig Aug 14, 2024
3783680
chore: add superclass analysis cross package
Thodd Aug 14, 2024
848ea49
chore: move @ui5/webcomponents-fiori sample to a fragment
Thodd Aug 14, 2024
75f72fb
chore: enable cross-library dependencies
petermuessig Aug 15, 2024
f3cd284
chore: some cleanup of the webc rollup plugin
petermuessig Aug 15, 2024
efd7615
chore: complete @ui5/webcomponents-fiori.NotificationList example
Thodd Aug 15, 2024
71e44f1
chore: support object types with no default value
petermuessig Aug 15, 2024
dc88bfd
chore: add bigger webcomponents-fiori sample - DynamicPage
Thodd Aug 15, 2024
930629c
chore: support library dependencies plus full library module name
petermuessig Aug 15, 2024
952f528
chore: removing support for old import syntax + fix of esmodule plugin
petermuessig Aug 15, 2024
4ffd6d3
chore: support export aliases in modules
petermuessig Aug 16, 2024
31ad3a2
chore: finish fiori.DynamicPage example
Thodd Aug 16, 2024
c84398f
chore: cleanup outdated code for patching enums. enums are now correc…
Thodd Aug 19, 2024
44cc9ab
chore: detect requests to NPM packages
petermuessig Aug 19, 2024
d01534d
chore: add first draft for ui5 association handling
Thodd Aug 22, 2024
2274a3d
chore: fix for tests + better isolation of tests
petermuessig Aug 24, 2024
173324d
Merge branch 'main' into feat/SeamlessWebComponents
petermuessig Aug 24, 2024
3e66c64
chore: update snapshots for tests
petermuessig Aug 24, 2024
21f0e90
chore: try to fix the wdi5 test error
petermuessig Aug 24, 2024
5c6b801
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
53de7ab
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
cc7f2b6
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
d6a8789
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
c235b3b
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
45c7e1d
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
abd389a
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
0c9c546
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
10cb49b
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
e2b1898
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
59206b6
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
9961b3f
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
3bd82dc
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
4afc4c9
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
8c3712d
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
e07c1cf
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
f3976c6
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
0730363
chore: next try to fix the wdi5 issues
petermuessig Aug 24, 2024
9c548f5
chore: next try to fix the wdi5 issues
petermuessig Aug 25, 2024
f96be76
chore: next try to fix the wdi5 issues
petermuessig Aug 25, 2024
313996e
chore: next try to fix the wdi5 issues
petermuessig Aug 25, 2024
2f8e4c2
chore: cleanup of PR before release
petermuessig Aug 25, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ dist

# vscode settings - to each her/his own
.vscode/settings.json

# local workspace configs
ui5-workspace.yaml
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ pnpm-lock.yaml
/**/*.png
/**/*.md
/**/*.gen.d.ts
/**/*.hbs
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build-lib:ts": "pnpm --filter ui5-tslib build",
"build-simple": "pnpm --filter ui5-app-simple build",
"build-simple:ts": "pnpm --filter ui5-tsapp-simple build",
"build-webc:ts": "pnpm --filter ui5-tsapp-webc build",
"build-cds": "pnpm --filter cds-bookshop build",
"build-bookshop": "pnpm --filter ui5-bookshop-viewer build",
"clean": "pnpm --filter ui5-app clean",
Expand All @@ -28,6 +29,7 @@
"dev-lib:ts": "pnpm --filter ui5-tslib dev",
"dev-simple": "pnpm --filter ui5-app-simple dev",
"dev-simple:ts": "pnpm --filter ui5-tsapp-simple dev",
"dev-webc:ts": "pnpm --filter ui5-tsapp-webc dev",
"dev-cds": "pnpm --filter cds-bookshop watch",
"dev-bookshop": "pnpm --filter ui5-bookshop-viewer dev",
"dev-approuter": "pnpm --filter approuter dev",
Expand All @@ -37,6 +39,7 @@
"start-cdn:ts": "pnpm --filter ui5-tsapp start-cdn",
"start-simple": "pnpm --filter ui5-app-simple start",
"start-simple:ts": "pnpm --filter ui5-tsapp-simple start",
"start-webc:ts": "pnpm --filter ui5-tsapp-webc start",
"start-cds": "pnpm --filter cds-bookshop start",
"start-bookshop": "pnpm --filter ui5-bookshop-viewer start",
"start-approuter": "pnpm --filter approuter start",
Expand Down
37 changes: 30 additions & 7 deletions packages/ui5-tooling-modules/lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,29 @@ const chokidar = require("chokidar");
* @returns {Function} Middleware function to use
*/
module.exports = async function ({ log, resources, options, middlewareUtil }) {
const cwd = middlewareUtil.getProject().getRootPath() || process.cwd();
const projectNamespace = middlewareUtil.getProject().getNamespace();
const projectType = middlewareUtil.getProject().getType();
const project = middlewareUtil.getProject();
const cwd = project.getRootPath() || process.cwd();
const projectInfo = {
name: project.getName(),
version: project.getVersion(),
namespace: project.getNamespace(),
type: project.getType(),
rootPath: project.getRootPath(),
framework: {
name: project.getFrameworkName(),
version: project.getFrameworkVersion(),
},
};
const depProjects = middlewareUtil
.getDependencies()
.map((dep) => middlewareUtil.getProject(dep))
.filter((prj) => !prj.isFrameworkProject());
const depPaths = depProjects.map((prj) => prj.getRootPath());
const depReaderCollection = middlewareUtil.resourceFactory.createReaderCollection({
name: `Reader collection of project ${middlewareUtil.getProject().getName()}`,
name: `Reader collection of project ${project.getName()}`,
readers: [resources.rootProject, ...depProjects.map((prj) => prj.getReader())],
});
const { scan, getBundleInfo, getResource } = require("./util")(log);
const { scan, getBundleInfo, getResource, resolveModule } = require("./util")(log, projectInfo);

log.verbose(`Starting ui5-tooling-modules-middleware`);

Expand Down Expand Up @@ -99,7 +109,7 @@ module.exports = async function ({ log, resources, options, middlewareUtil }) {
log.warn(`Including module "${mod}" to bundle which has been requested dynamically! This module may not be packaged during the build!`);
modules.push(mod);
});
return getBundleInfo(modules, config, { cwd, projectNamespace, projectType, depPaths, isMiddleware: true });
return getBundleInfo(modules, config, { cwd, depPaths, isMiddleware: true });
})
.then((bundleInfo) => {
// finally, we watch the entries of the bundle
Expand All @@ -124,6 +134,11 @@ module.exports = async function ({ log, resources, options, middlewareUtil }) {
}
};

const getNpmPackageName = (source) => {
const npmPackageScopeRegEx = /^((?:(@[^/]+)\/)?([^/]+))(?:\/(.*))?$/;
return npmPackageScopeRegEx.exec(source)?.[1];
};

// return the middleware
return async (req, res, next) => {
// determine the request path
Expand All @@ -144,10 +159,18 @@ module.exports = async function ({ log, resources, options, middlewareUtil }) {

// check if the resource exists in node_modules
let resource = getResource(moduleName, { cwd, depPaths, isMiddleware: true });
let existsPackage;
if (!resource) {
// in some cases there is a request to a module of an NPM package and in this
// case we still need to trigger the bundle and watch process to create the
// bundle info from which we can extract the resource (e.g. webc libraries)
const npmPackage = getNpmPackageName(moduleName);
existsPackage = !!resolveModule(`${npmPackage}/package.json`, { cwd, depPaths, isMiddleware: true });
}

// if a resource has been found in node_modules, we will
// trigger the bundling process and watch the bundled resources
if (resource) {
if (resource || existsPackage) {
bundleAndWatch({ moduleName });
}

Expand Down
104 changes: 104 additions & 0 deletions packages/ui5-tooling-modules/lib/polyfills.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ module.exports = function (/* { log } = {} */) {
// keeping the name and just adding a "?query" to the end
// ensures that preserveModules will generate the original
// entry name for this entry.
return `${resolution.id}${PROXY_SUFFIX}`;
if (Object.keys(moduleInfo.attributes || {}).length === 0) {
return {
id: `${resolution.id}${PROXY_SUFFIX}`,
};
}
}
return null;
},
Expand Down
239 changes: 239 additions & 0 deletions packages/ui5-tooling-modules/lib/rollup-plugin-webcomponents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
const { join, dirname } = require("path");
const { readFileSync } = require("fs");
const WebComponentRegistry = require("./utils/WebComponentRegistry");

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

module.exports = function ({ log, resolveModule, framework, skip } = {}) {
// 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")) {
skip = true;
log.warn("Skipping Web Components transformation as UI5 version is < 1.120.0");
}

const getNpmPackageName = (source) => {
const npmPackageScopeRegEx = /^((?:(@[^/]+)\/)?([^/]+))(?:\/(.*))?$/;
return npmPackageScopeRegEx.exec(source)?.[1];
};

const loadAndCompileTemplate = (templatePath) => {
const templateFile = readFileSync(join(__dirname, templatePath), { encoding: "utf-8" });
return compile(templateFile);
};

const libTemplateFn = loadAndCompileTemplate("templates/Library.hbs");
const webccTemplateFn = loadAndCompileTemplate("templates/WebComponentControl.hbs");
const webcmpTemplateFn = loadAndCompileTemplate("templates/WebComponentMonkeyPatches.hbs");

const loadNpmPackage = (npmPackage, emitFile) => {
let registryEntry = WebComponentRegistry.getPackage(npmPackage);
if (!registryEntry) {
const packageJsonPath = resolveModule(`${npmPackage}/package.json`);
if (packageJsonPath) {
const packageJson = require(packageJsonPath);
// for all UI5 Web Components packages we use the internal custom elements metadata
if (/^@ui5\/webcomponents/.test(packageJson.name)) {
packageJson.customElements = /* packageJson.customElements || */ "dist/custom-elements-internal.json";
}
if (!registryEntry && packageJson.customElements) {
// load the dependent Web Component packages
const libraryDependencies = [];
Object.keys(packageJson.dependencies || {}).forEach((dep) => {
const package = loadNpmPackage(dep, emitFile);
if (package) {
libraryDependencies.push(package.namespace);
}
});

// load custom elements metadata
const metadataPath = resolveModule(join(npmPackage, packageJson.customElements));
if (metadataPath) {
const customElementsMetadata = require(metadataPath);

// first time registering a new Web Component package
const npmPackagePath = dirname(packageJsonPath);
registryEntry = WebComponentRegistry.register({
customElementsMetadata,
namespace: npmPackage,
npmPackagePath,
});

// assign the dependencies
registryEntry.dependencies = libraryDependencies;

// each library has a library module (with a concrete name) that needs to be emitted
emitFile({
type: "chunk",
id: `${npmPackage}/library`,
name: `${npmPackage}/library`,
});
}
}
}
}
return registryEntry;
};

const lookupWebComponentsClass = (source, emitFile) => {
let clazz;
if ((clazz = WebComponentRegistry.getClassDefinition(source))) {
return clazz;
}

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

const registryEntry = loadNpmPackage(npmPackage, emitFile);
if (registryEntry) {
const metadata = registryEntry;
let modulePath = resolveModule(source);
if (modulePath) {
modulePath = modulePath.substr(metadata.npmPackagePath.length + 1);
const moduleName = `${npmPackage}/${modulePath}`;
const clazz = WebComponentRegistry.getClassDefinition(moduleName);
// TODO: base classes must be ignored as UI5Element is flagged as custom element although it is a base class
if (clazz && clazz.customElement && npmPackage !== "@ui5/webcomponents-base") {
return clazz;
}
}
}
};

return {
name: "webcomponents",
async resolveId(source, importer /*, options*/) {
if (skip) {
return null;
}
const importerModuleInfo = this.getModuleInfo(importer);
const isImporterUI5Module = importerModuleInfo?.attributes?.ui5Type;

if (!importer || isImporterUI5Module) {
if (source === "sap/ui/core/webc/WebComponent" || source === "sap/ui/core/Lib" || source === "sap/ui/base/DataType") {
// mark Ui5 runtime dependencies as external
// to avoid warnings about missing dependencies
return {
id: source,
external: true,
};
}

let clazz;
if ((clazz = lookupWebComponentsClass(source, this.emitFile))) {
const modulePath = `${clazz.package}/${clazz.module}`;
const absModulePath = resolveModule(modulePath);
// id needs to be the resolved name to later be able to assign the generated code to the correct module
// utils.js => const resolvedModule = modules.find((mod) => module?.facadeModuleId?.startsWith(mod.path));
return {
id: absModulePath + "?ui5Type=control", // the query parameter is needed to avoid cycles as we overlay the WebComponent class
attributes: {
ui5Type: "control",
modulePath,
absModulePath,
clazz,
},
};
} else if (source.endsWith("/library.js") || source.endsWith("/library")) {
const npmPackage = getNpmPackageName(source);
let package;
if ((package = WebComponentRegistry.getPackage(npmPackage))) {
const modulePath = `${npmPackage}/library.js`;
const absModulePath = `${package.npmPackagePath}/library.js`;
return {
id: absModulePath,
attributes: {
ui5Type: "library",
modulePath,
absModulePath,
package,
},
};
}
}
}
},

async load(id) {
if (skip) {
return null;
}
const moduleInfo = this.getModuleInfo(id);
if (moduleInfo.attributes.ui5Type === "library") {
let lib = moduleInfo.attributes.package;
const { namespace } = lib;

// compile the library metadata
const metadataObject = {
apiVersion: 2,
name: namespace,
dependencies: ["sap.ui.core", ...lib.dependencies],
types: Object.keys(lib.enums).map((enumName) => `${namespace}.${enumName}`),
elements: [
/* do we have any? */
],
controls: Object.keys(lib.customElements).map((elementName) => `${namespace}.${elementName}`),
interfaces: Object.keys(lib.interfaces).map((interfaceName) => `${namespace}.${interfaceName}`),
designtime: `${namespace}/designtime/library.designtime`,
extensions: {
flChangeHandlers: {
"@ui5/webcomponents.Avatar": {
hideControl: "default",
unhideControl: "default",
},
"@ui5/webcomponents.Button": "@ui5/webcomponents-flexibility.Button",
},
},
noLibraryCSS: true,
};
const metadata = JSON.stringify(metadataObject, undefined, 2);

// generate the library code
const code = libTemplateFn({
metadata,
namespace,
enums: lib.enums,
dependencies: lib.dependencies.map((dep) => `${dep}/library`),
});
// 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)
if (namespace === "@ui5/webcomponents-base" && lt(framework?.version || "0.0.0", "1.128.0")) {
const monkeyPatches = webcmpTemplateFn();
return `${monkeyPatches}\n${code}`;
}
return code;
} else if (moduleInfo.attributes.ui5Type === "control") {
let clazz = moduleInfo.attributes.clazz;
// 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, {
library: `${ui5Metadata.namespace}.library`,
});
const metadata = JSON.stringify(metadataObject, undefined, 2);
const webcClass = moduleInfo.attributes.absModulePath; // is the absolute path of the original Web Component class

// Determine the superclass UI5 module name and import it
let webcBaseClass = "sap/ui/core/webc/WebComponent";
if (clazz.superclass?._ui5metadata) {
const { module } = clazz.superclass;
const { namespace } = clazz.superclass._ui5metadata;
webcBaseClass = `${namespace}/${module}`;
}

// generate the WebComponentControl code
const code = webccTemplateFn({
ui5Class,
namespace,
metadata,
webcClass,
webcBaseClass,
});
return code;
}
return null;
},
};
};
Loading
Loading