Skip to content

Commit

Permalink
feat(ui5-tooling-modules): do not consider webc npm packages as libra…
Browse files Browse the repository at this point in the history
…ries
  • Loading branch information
petermuessig committed Sep 14, 2024
1 parent b6da7af commit 671fa8a
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 87 deletions.
143 changes: 78 additions & 65 deletions packages/ui5-tooling-modules/lib/rollup-plugin-webcomponents.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { join, dirname } = require("path");
const { readFileSync } = require("fs");
const { readFileSync, existsSync } = require("fs");
const WebComponentRegistry = require("./utils/WebComponentRegistry");

const { compile } = require("handlebars");
Expand All @@ -13,69 +13,89 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
log.warn("Skipping Web Components transformation as UI5 version is < 1.120.0");
}

// helper function to extract the npm package name from a module name
const getNpmPackageName = (source) => {
const npmPackageScopeRegEx = /^((?:(@[^/]+)\/)?([^/]+))(?:\/(.*))?$/;
return npmPackageScopeRegEx.exec(source)?.[1];
};

// helper function to load and compile a handlebars template
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");
// handlebars templates for the Web Components transformation
const webcTmplFnPackage = loadAndCompileTemplate("templates/Package.hbs");
const webcTmplFnControl = loadAndCompileTemplate("templates/WrapperControl.hbs");
const webcTmplFnPatches = loadAndCompileTemplate("templates/MonkeyPatches.hbs");

// helper function to load a NPM package and its custom elements metadata
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";
const npmPackagePath = dirname(packageJsonPath);
// check if the custom elements metadata file exists (fallback to custom-elements-internal.json for @ui5/webcomponents)
let metadataPath;
if (packageJson.customElements) {
const customElementsInternalPath = join(npmPackagePath, packageJson.customElements.replace("custom-elements.json", "custom-elements-internal.json"));
const customElementsPath = join(npmPackagePath, packageJson.customElements);
if (existsSync(customElementsInternalPath)) {
metadataPath = customElementsInternalPath;
} else if (existsSync(customElementsPath)) {
metadataPath = customElementsPath;
}
} else {
const customElementsInternalPath = join(npmPackagePath, "dist/custom-elements-internal.json");
const customElementsPath = join(npmPackagePath, "dist/custom-elements.json");
if (existsSync(customElementsInternalPath)) {
metadataPath = customElementsInternalPath;
log.warn(`The package.json of ${npmPackage} does not contain a "customElements" field. Using found "dist/custom-elements-internal.json"`);
} else if (existsSync(customElementsPath)) {
metadataPath = customElementsPath;
log.warn(`The package.json of ${npmPackage} does not contain a "customElements" field. Using found "dist/custom-elements.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 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
if (metadataPath) {
const customElementsMetadata = JSON.parse(readFileSync(metadataPath, { encoding: "utf-8" }));

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

// 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`,
});
}
// assign the dependencies
registryEntry.dependencies = libraryDependencies;

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

// helper function to lookup a Web Component class by its module name
const lookupWebComponentsClass = (source, emitFile) => {
let clazz;
if ((clazz = WebComponentRegistry.getClassDefinition(source))) {
Expand All @@ -101,6 +121,9 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
}
};

// list of external dependencies that are needed for the Web Components transformation
const externalDeps = ["sap/ui/core/webc/WebComponent", "sap/ui/core/webc/WebComponentRenderer", "sap/ui/core/Lib", "sap/ui/base/DataType", "sap/base/strings/hyphenate"];

return {
name: "webcomponents",
async resolveId(source, importer /*, options*/) {
Expand All @@ -112,7 +135,7 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {

if (!importer || isImporterUI5Module) {
// resolve UI5 modules (hypenate is needed for WebComponentsMonkeyPatches and could be removed again)
if (["sap/ui/core/webc/WebComponent", "sap/ui/core/Lib", "sap/ui/base/DataType", "sap/base/strings/hyphenate"].includes(source)) {
if (externalDeps.includes(source)) {
// mark Ui5 runtime dependencies as external
// to avoid warnings about missing dependencies
return {
Expand All @@ -121,6 +144,7 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
};
}

const npmPackage = getNpmPackageName(source);
let clazz;
if ((clazz = lookupWebComponentsClass(source, this.emitFile))) {
const modulePath = `${clazz.package}/${clazz.module}`;
Expand All @@ -136,16 +160,15 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
clazz,
},
};
} else if (source.endsWith("/library.js") || source.endsWith("/library")) {
const npmPackage = getNpmPackageName(source);
} else if (new RegExp(`^${npmPackage}(.js)?$`).test(source)) {
let package;
if ((package = WebComponentRegistry.getPackage(npmPackage))) {
const modulePath = `${npmPackage}/library.js`;
const absModulePath = `${package.npmPackagePath}/library.js`;
const modulePath = `${npmPackage}.js`;
const absModulePath = `${package.npmPackagePath}.js`;
return {
id: absModulePath,
attributes: {
ui5Type: "library",
ui5Type: "package",
modulePath,
absModulePath,
package,
Expand All @@ -161,46 +184,35 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
return null;
}
const moduleInfo = this.getModuleInfo(id);
if (moduleInfo.attributes.ui5Type === "library") {
if (moduleInfo.attributes.ui5Type === "package") {
let lib = moduleInfo.attributes.package;
const { namespace } = lib;
const { namespace, version } = lib;

// compile the library metadata
const metadataObject = {
apiVersion: 2,
name: namespace,
dependencies: ["sap.ui.core", ...lib.dependencies],
version,
dependencies: ["sap.ui.core"],
types: Object.keys(lib.enums).map((enumName) => `${namespace}.${enumName}`),
interfaces: Object.keys(lib.interfaces).map((interfaceName) => `${namespace}.${interfaceName}`),
controls: Object.keys(lib.customElements).map((elementName) => `${namespace}.${elementName}`),
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({
const code = webcTmplFnPackage({
metadata,
namespace,
enums: lib.enums,
dependencies: lib.dependencies.map((dep) => `${dep}/library`),
dependencies: lib.dependencies,
});
// 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();
const monkeyPatches = webcTmplFnPatches();
return `${monkeyPatches}\n${code}`;
}
return code;
Expand All @@ -212,6 +224,7 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
const namespace = ui5Metadata.namespace;
const metadataObject = Object.assign({}, ui5Metadata, {
library: `${ui5Metadata.namespace}.library`,
designtime: `${ui5Metadata.namespace}/designtime/${clazz.name}.designtime`,
});
const metadata = JSON.stringify(metadataObject, undefined, 2);
const webcClass = moduleInfo.attributes.absModulePath; // is the absolute path of the original Web Component class
Expand All @@ -225,7 +238,7 @@ module.exports = function ({ log, resolveModule, framework, skip } = {}) {
}

// generate the WebComponentControl code
const code = webccTemplateFn({
const code = webcTmplFnControl({
ui5Class,
namespace,
metadata,
Expand Down
18 changes: 0 additions & 18 deletions packages/ui5-tooling-modules/lib/templates/Library.hbs

This file was deleted.

19 changes: 19 additions & 0 deletions packages/ui5-tooling-modules/lib/templates/Package.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { registerEnum } from "sap/ui/base/DataType";
{{#each dependencies}}
import "{{this}}";
{{/each}}

const pkg = {
"_ui5metadata": {{{metadata}}}
};

{{#each enums}}
pkg["{{name}}"] = {
{{#each members}}
"{{name}}": "{{name}}",
{{/each}}
};
registerEnum("{{../namespace}}.{{name}}", pkg["{{name}}"]);
{{/each}}

export default pkg;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import WebComponentClass from "{{webcClass}}";
import "{{namespace}}/library";
import "{{namespace}}";
import WebComponentBaseClass from "{{webcBaseClass}}";

export default WebComponentBaseClass.extend("{{ui5Class}}", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ const _classAliases = {};
class RegistryEntry {
#customElementsMetadata = {};

constructor({ customElementsMetadata, namespace, npmPackagePath }) {
constructor({ customElementsMetadata, namespace, npmPackagePath, version }) {
this.#customElementsMetadata = customElementsMetadata;
this.namespace = namespace;
this.npmPackagePath = npmPackagePath;
this.version = version;

this.customElements = {};
this.classes = {};
Expand Down Expand Up @@ -390,10 +391,10 @@ class RegistryEntry {
}

const WebComponentRegistry = {
register({ customElementsMetadata, namespace, npmPackagePath }) {
register({ customElementsMetadata, namespace, npmPackagePath, version }) {
let entry = _registry[namespace];
if (!entry) {
entry = _registry[namespace] = new RegistryEntry({ customElementsMetadata, namespace, npmPackagePath });
entry = _registry[namespace] = new RegistryEntry({ customElementsMetadata, namespace, npmPackagePath, version });

// track all classes also via their module name,
// so we can access them faster during resource resolution later on
Expand Down

0 comments on commit 671fa8a

Please sign in to comment.