diff --git a/.eslintignore b/.eslintignore
index fac21f80da9..129d53100bd 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -5,6 +5,9 @@ packages/base/types
# build results for charts package
packages/charts/dist
+# build results for cli package
+packages/cli/dist
+
# build results for main package
packages/main/dist
packages/main/ssr
diff --git a/.github/workflows/test-web-components.yml b/.github/workflows/test-web-components.yml
deleted file mode 100644
index 0865cf29075..00000000000
--- a/.github/workflows/test-web-components.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Test (Web Components)
-
-on:
- push:
- branches:
- - 'main'
- paths:
- - 'packages/main/src/webComponents/**/*'
- pull_request:
- paths:
- - 'packages/main/src/webComponents/**/*'
-
-jobs:
- cypress-test:
- name: 'Cypress'
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Cypress run
- uses: cypress-io/github-action@v5
- with:
- build: yarn build
- browser: chrome
- component: true
- spec: |
- packages/main/src/webComponents
-
- - uses: actions/upload-artifact@v4
- if: always()
- with:
- name: web-components-coverage
- path: temp/cypress-coverage
diff --git a/.storybook/components/DocsHeader.tsx b/.storybook/components/DocsHeader.tsx
index 8cb76017a8d..5d02573ce73 100644
--- a/.storybook/components/DocsHeader.tsx
+++ b/.storybook/components/DocsHeader.tsx
@@ -66,8 +66,8 @@ export const DocsHeader = ({ since }) => {
-
+
);
};
diff --git a/package.json b/package.json
index b2b3b5e4d3a..4d8aeb7f1ba 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,9 @@
"prettier:all": "prettier --write --config ./prettier.config.cjs \"packages/**/*.{js,jsx,ts,tsx,mdx,json,md}\"",
"lint": "eslint packages --ext .ts,.tsx",
"lerna:version-dryrun": "lerna version --conventional-graduate --no-git-tag-version --no-push",
- "create-webcomponents-wrapper": "node --experimental-json-modules ./packages/main/scripts/create-web-components-wrapper.mjs && node --experimental-json-modules ./scripts/generate-theming-parameters.js",
+ "wrappers:main": "WITH_WEB_COMPONENT_IMPORT_PATH='../../internal/withWebComponent.js' INTERFACES_IMPORT_PATH='../../types/index.js' node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents --out ./packages/main/src/webComponents --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/playground/)'",
+ "wrappers:fiori": "WITH_WEB_COMPONENT_IMPORT_PATH='../../internal/withWebComponent.js' INTERFACES_IMPORT_PATH='../../types/index.js' node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents-fiori --out ./packages/main/src/webComponents --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/playground/)'",
+ "create-webcomponents-wrapper": "yarn run wrappers:main && yarn run wrappers:fiori && prettier --log-level silent --write ./packages/main/src/webComponents && eslint --fix ./packages/main/src/webComponents/*/index.tsx",
"chromatic": "cross-env STORYBOOK_ENV=chromatic npx chromatic --build-script-name build:storybook",
"postinstall": "husky && yarn prepare",
"create-cypress-commands-docs": "typedoc && rimraf temp/typedoc"
@@ -35,9 +37,9 @@
"@storybook/react": "7.6.12",
"@storybook/react-vite": "7.6.12",
"@storybook/theming": "7.6.12",
- "@ui5/webcomponents": "1.21.2",
- "@ui5/webcomponents-fiori": "1.21.2",
- "@ui5/webcomponents-icons": "1.21.2",
+ "@ui5/webcomponents": "1.22.0",
+ "@ui5/webcomponents-fiori": "1.22.0",
+ "@ui5/webcomponents-icons": "1.22.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"remark-gfm": "^3.0.1",
@@ -55,9 +57,10 @@
"@types/node": "^20.0.0",
"@types/react": "^18.2.23",
"@types/react-dom": "^18.2.7",
+ "@types/turndown": "^5.0.4",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
- "@ui5/webcomponents-tools": "1.21.2",
+ "@ui5/webcomponents-tools": "1.22.0",
"@vitejs/plugin-react": "^4.2.0",
"chromatic": "^10.0.0",
"cssnano": "^6.0.1",
diff --git a/packages/base/package.json b/packages/base/package.json
index 7432d79f1e0..33688525c26 100644
--- a/packages/base/package.json
+++ b/packages/base/package.json
@@ -30,7 +30,7 @@
},
"peerDependencies": {
"@types/react": "*",
- "@ui5/webcomponents-base": "~1.21.0",
+ "@ui5/webcomponents-base": "~1.22.0",
"react": "^16.14.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
diff --git a/packages/charts/package.json b/packages/charts/package.json
index 76d3f0bc950..c5f65c076e8 100644
--- a/packages/charts/package.json
+++ b/packages/charts/package.json
@@ -34,8 +34,8 @@
"recharts": "2.11.0"
},
"peerDependencies": {
- "@ui5/webcomponents-react": "~1.24.0",
- "@ui5/webcomponents-react-base": "~1.24.0",
+ "@ui5/webcomponents-react": "~1.25.0",
+ "@ui5/webcomponents-react-base": "~1.25.0",
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
"react-jss": "^10.10.0"
},
diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore
new file mode 100644
index 00000000000..be3f0c7e2b0
--- /dev/null
+++ b/packages/cli/.gitignore
@@ -0,0 +1,2 @@
+dist
+test
\ No newline at end of file
diff --git a/packages/cli/package.json b/packages/cli/package.json
new file mode 100644
index 00000000000..a13dcd7e065
--- /dev/null
+++ b/packages/cli/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@ui5/webcomponents-react-cli",
+ "private": true,
+ "description": "CLI for UI5 Web Components for React",
+ "author": "SAP SE (https://www.sap.com)",
+ "license": "Apache-2.0",
+ "version": "1.22.0",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "main": "./dist/index.js",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "bin": {
+ "ui5-wcr": "./dist/bin/index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/ui5-webcomponents-react",
+ "directory": "packages/cli"
+ },
+ "scripts": {
+ "clean": "rimraf dist"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "dedent": "^1.5.1",
+ "turndown": "^7.1.2"
+ }
+}
diff --git a/packages/cli/src/bin/index.ts b/packages/cli/src/bin/index.ts
new file mode 100755
index 00000000000..b2087be1d32
--- /dev/null
+++ b/packages/cli/src/bin/index.ts
@@ -0,0 +1,55 @@
+#!/usr/bin/env node
+import { resolve } from 'node:path';
+import { parseArgs } from 'node:util';
+import * as process from 'process';
+
+const options = {
+ packageName: {
+ type: 'string' as const
+ },
+ out: {
+ type: 'string' as const,
+ short: 'o'
+ },
+ additionalComponentNote: {
+ type: 'string' as const
+ }
+};
+const { values, positionals } = parseArgs({ options, allowPositionals: true });
+
+const [command] = positionals;
+
+console.log(command);
+
+switch (command) {
+ case 'create-wrappers':
+ const { packageName, out, additionalComponentNote } = values;
+ const missingParameters = [];
+ if (!packageName) {
+ missingParameters.push('--packageName');
+ }
+ if (!out) {
+ missingParameters.push('--out');
+ }
+
+ if (missingParameters.length > 0) {
+ console.error(`
+ Missing parameters: ${missingParameters.join(', ')}
+ Example: ui5-wcr create-wrappers --packageName @ui5/webcomponents --out ./src/components
+
+ Please add the missing parameters and try again.
+ `);
+ process.exit(1);
+ }
+
+ const createWrapperModule = await import('../scripts/create-wrappers/main.js');
+
+ const outDir = resolve(process.cwd(), values.out!);
+ // eslint-disable-next-line @typescript-eslint/await-thenable
+ await createWrapperModule.default(packageName!, outDir, { additionalComponentNote });
+ process.exit(0);
+ break;
+ default:
+ console.warn('Unknown command', command);
+ process.exit(1);
+}
diff --git a/packages/cli/src/scripts/create-wrappers/AbstractRenderer.ts b/packages/cli/src/scripts/create-wrappers/AbstractRenderer.ts
new file mode 100644
index 00000000000..7301440533c
--- /dev/null
+++ b/packages/cli/src/scripts/create-wrappers/AbstractRenderer.ts
@@ -0,0 +1,19 @@
+import type { WebComponentWrapper } from './WebComponentWrapper.js';
+
+export enum RenderingPhase {
+ imports = 'imports',
+ attributes = 'attributes',
+ props = 'props',
+ domRef = 'domRef',
+ component = 'component',
+ exports = 'exports'
+}
+
+export abstract class AbstractRenderer {
+ public readonly phase!: RenderingPhase;
+
+ public prepare(context: WebComponentWrapper) {
+ // optional
+ }
+ abstract render(context: WebComponentWrapper): string;
+}
diff --git a/packages/cli/src/scripts/create-wrappers/AttributesRenderer.ts b/packages/cli/src/scripts/create-wrappers/AttributesRenderer.ts
new file mode 100644
index 00000000000..f0cdd869a49
--- /dev/null
+++ b/packages/cli/src/scripts/create-wrappers/AttributesRenderer.ts
@@ -0,0 +1,102 @@
+import type * as CEM from '@ui5/webcomponents-tools/lib/cem/types-internal.d.ts';
+import dedent from 'dedent';
+import {
+ mapWebComponentTypeToPrimitive,
+ propDescriptionFormatter,
+ snakeCaseToCamelCase
+} from '../../util/formatters.js';
+import { resolveReferenceImports } from '../../util/referenceResolver.js';
+import { AbstractRenderer, RenderingPhase } from './AbstractRenderer.js';
+import { WebComponentWrapper } from './WebComponentWrapper.js';
+
+const loggedTypes = new Set();
+
+function mapWebComponentTypeToTsType(type: string) {
+ const primitive = mapWebComponentTypeToPrimitive(type);
+ if (primitive) {
+ return primitive;
+ }
+ switch (type) {
+ case 'HTMLElement | string | undefined':
+ case 'HTMLElement | string':
+ // opener props only accept strings as prop types
+ return 'string';
+
+ default:
+ if (!loggedTypes.has(type)) {
+ console.log('-> Attributes type', type);
+ loggedTypes.add(type);
+ }
+ return type;
+ }
+}
+
+export class AttributesRenderer extends AbstractRenderer {
+ public phase = RenderingPhase.attributes;
+
+ private _attributes: CEM.ClassField[] = [];
+
+ setAttributes(value: CEM.Attribute[]) {
+ this._attributes = value.toSorted((a, b) => a.name.localeCompare(b.name)) as CEM.ClassField[];
+ return this;
+ }
+
+ private descriptionBuilder(attribute: CEM.ClassField) {
+ const parts: string[] = [];
+
+ parts.push(` * ${propDescriptionFormatter(attribute.description ?? '')}`);
+ if (attribute.default && attribute.default.length > 0 && attribute.default !== '""') {
+ parts.push(` * @default ${attribute.default}`);
+ }
+
+ if (attribute.deprecated) {
+ parts.push(` * @deprecated ${attribute.deprecated}`);
+ }
+
+ return `/**\n${parts.join('\n')}\n */`;
+ }
+
+ private propTyping(attribute: CEM.ClassField, context: WebComponentWrapper) {
+ let type = attribute.type?.text ?? 'unknown';
+ type = mapWebComponentTypeToTsType(type);
+
+ const references = attribute.type?.references;
+ const isEnum = references != null && references?.length > 0;
+
+ if (isEnum) {
+ type += ` | keyof typeof ${type}`;
+ }
+ if (attribute._ui5validator === 'CSSColor') {
+ type = `CSSProperties['color']`;
+ } else if (attribute._ui5validator === 'CSSSize') {
+ type = `CSSProperties['width'] | CSSProperties['height']`;
+ }
+
+ context.addAttribute(snakeCaseToCamelCase(attribute.name), type);
+
+ return `${snakeCaseToCamelCase(attribute.name)}?: ${type};`;
+ }
+
+ prepare(context: WebComponentWrapper) {
+ for (const attribute of this._attributes) {
+ // special css handling
+ if (attribute._ui5validator === 'CSSSize' || attribute._ui5validator === 'CSSColor') {
+ context.addTypeImport('react', 'CSSProperties');
+ } else {
+ resolveReferenceImports(attribute.type?.references ?? [], context);
+ }
+ }
+ }
+
+ render(context: WebComponentWrapper): string {
+ return dedent`
+ interface ${context.componentName}Attributes {
+ ${this._attributes
+ .map((attribute) => {
+ return `${this.descriptionBuilder(attribute)}\n${this.propTyping(attribute, context)}`;
+ })
+ .join('\n\n')}
+ }
+ `;
+ }
+}
diff --git a/packages/cli/src/scripts/create-wrappers/ComponentRenderer.ts b/packages/cli/src/scripts/create-wrappers/ComponentRenderer.ts
new file mode 100644
index 00000000000..b8b7a7ca0d1
--- /dev/null
+++ b/packages/cli/src/scripts/create-wrappers/ComponentRenderer.ts
@@ -0,0 +1,99 @@
+import type * as CEM from '@ui5/webcomponents-tools/lib/cem/types-internal.d.ts';
+import dedent from 'dedent';
+import { snakeCaseToCamelCase, summaryFormatter } from '../../util/formatters.js';
+import { AbstractRenderer, RenderingPhase } from './AbstractRenderer.js';
+import { WebComponentWrapper } from './WebComponentWrapper.js';
+
+export class ComponentRenderer extends AbstractRenderer {
+ public phase = RenderingPhase.component;
+
+ private dynamicImportPath: string | undefined;
+ private attributes: CEM.ClassField[] = [];
+ private slots: CEM.Slot[] = [];
+ private events: CEM.Event[] = [];
+ private description: string = '';
+ private note: string = '';
+ private isAbstract: boolean = false;
+
+ setDynamicImportPath(value: string) {
+ this.dynamicImportPath = value;
+ return this;
+ }
+
+ setAttributes(attrs: CEM.ClassField[]) {
+ this.attributes.push(...attrs);
+ return this;
+ }
+
+ setSlots(slots: CEM.Slot[]) {
+ this.slots.push(...slots);
+ return this;
+ }
+
+ setEvents(events: CEM.Event[]) {
+ this.events.push(...events);
+ return this;
+ }
+
+ setDescription(value: string) {
+ this.description = value;
+ return this;
+ }
+
+ setNote(value: string) {
+ this.note = value;
+ return this;
+ }
+
+ setIsAbstract(value: boolean) {
+ this.isAbstract = value;
+ return this;
+ }
+
+ prepare(context: WebComponentWrapper) {
+ context.exportSet.add(context.componentName);
+ }
+
+ render(context: WebComponentWrapper): string {
+ let comment = `/**\n * ${summaryFormatter(this.description)}\n *\n`;
+
+ if (this.isAbstract) {
+ comment += ' * @abstract\n';
+ }
+ if (this.note) {
+ comment += ` * __Note__: ${this.note}\n`;
+ }
+ comment += '*/';
+
+ const component = dedent`
+ const ${context.componentName} = withWebComponent<${context.componentName}PropTypes, ${
+ context.componentName
+ }DomRef>('${context.tagName}',
+ [${this.attributes
+ .filter((attr) => attr.type?.text !== 'boolean')
+ .map((attr) => `'${snakeCaseToCamelCase(attr.name)}'`)
+ .toSorted((a, b) => a.localeCompare(b))
+ .join(', ')}],
+ [${this.attributes
+ .filter((attr) => attr.type?.text === 'boolean')
+ .map((attr) => `'${snakeCaseToCamelCase(attr.name)}'`)
+ .toSorted((a, b) => a.localeCompare(b))
+ .join(', ')}],
+ [${this.slots
+ ?.filter((slot) => slot.name !== 'default')
+ .map((slot) => `'${snakeCaseToCamelCase(slot.name)}'`)
+ .toSorted((a, b) => a.localeCompare(b))
+ .join(', ')}],
+ [${this.events
+ ?.map((event) => `'${event.name}'`)
+ .toSorted((a, b) => a.localeCompare(b))
+ .join(', ')}],
+ () => import('${this.dynamicImportPath}')
+ );
+
+ ${context.componentName}.displayName = '${context.componentName}';
+ `;
+
+ return [comment, component].join('\n');
+ }
+}
diff --git a/packages/cli/src/scripts/create-wrappers/DomRefRenderer.ts b/packages/cli/src/scripts/create-wrappers/DomRefRenderer.ts
new file mode 100644
index 00000000000..6e070a51258
--- /dev/null
+++ b/packages/cli/src/scripts/create-wrappers/DomRefRenderer.ts
@@ -0,0 +1,167 @@
+import type * as CEM from '@ui5/webcomponents-tools/lib/cem/types-internal.d.ts';
+import dedent from 'dedent';
+import { mapWebComponentTypeToPrimitive, propDescriptionFormatter } from '../../util/formatters.js';
+import { resolveReferenceImports } from '../../util/referenceResolver.js';
+import { AbstractRenderer, RenderingPhase } from './AbstractRenderer.js';
+import { WebComponentWrapper } from './WebComponentWrapper.js';
+
+const loggedTypes = new Set();
+
+function mapWebComponentTypeToTsType(type: string = 'unknown') {
+ const primitive = mapWebComponentTypeToPrimitive(type);
+ if (primitive) {
+ return primitive;
+ }
+ switch (type) {
+ case 'function':
+ return 'Function';
+ case 'HTMLElement':
+ // we need to extend HTMLElement with | EventTarget to allow opening popovers from event handlers
+ // example: