webcomponent: integrate PostCSS with Rollup and add error handling in OpenViduMeet component

- Added rollup-plugin-postcss to handle CSS imports in the project.
- Updated rollup.config.js to include PostCSS plugin for CSS injection and minification.
- Created a new styles.css file for component styling.
- Enhanced OpenViduMeet component to manage iframe loading states and display error messages.
- Implemented cleanup method in EventsManager to remove event listeners.
- Added TypeScript declaration for CSS module imports.
This commit is contained in:
Carlos Santos 2025-05-08 15:21:00 +02:00
parent 347d9472e0
commit 3a9f3c507d
7 changed files with 1459 additions and 22 deletions

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@
"jest-environment-jsdom": "^29.7.0",
"playwright": "^1.50.1",
"rollup": "^4.34.8",
"rollup-plugin-postcss": "^4.0.2",
"ts-jest": "^29.2.5",
"ts-jest-resolver": "^2.0.1",
"ts-node": "^10.9.2",

View File

@ -3,21 +3,67 @@ import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import terser from '@rollup/plugin-terser'
import postcss from 'rollup-plugin-postcss'
import fs from 'fs'
const production = !process.env.ROLLUP_WATCH
export default {
input: 'src/index.ts',
output: {
file: './dist/openvidu-meet.bundle.min.js',
format: 'iife',
name: 'OpenViduMeet',
sourcemap: true
sourcemap: !production
},
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
terser(),
resolve({
// Prioritize modern ES modules
mainFields: ['module', 'browser', 'main']
}),
commonjs({
// Optimize CommonJS conversion
transformMixedEsModules: true
}),
postcss({
inject: true, // This injects the CSS into the JS bundle
minimize: true,
// Don't extract CSS to a separate file
extract: false
}),
typescript({
tsconfig: './tsconfig.json',
declaration: false,
sourceMap: !production
}),
terser({
ecma: 2020, // Use modern features when possible
compress: {
drop_console: production, // Remove console.logs in production
drop_debugger: production,
pure_getters: true,
unsafe: true,
passes: 3, // Multiple passes for better minification
toplevel: true // Enable top-level variable renaming
},
// mangle: {
// properties: {
// regex: /^_|^iframe|^error|^load|^allowed|^command|^events/, // Mangle most internal properties
// reserved: [
// 'connectedCallback',
// 'disconnectedCallback', // Web Component lifecycle methods
// 'shadowRoot',
// 'attachShadow', // Shadow DOM APIs
// 'attributes',
// 'setAttribute' // Standard element properties
// ]
// },
// toplevel: true // Enable top-level variable renaming
// },
format: {
comments: false // Remove all comments
}
}),
{
name: 'copy-bundle',
writeBundle () {

View File

@ -0,0 +1,36 @@
:host {
display: block;
width: 100%;
height: 100%;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background-color: #f8f8f8;
border: 1px solid #e0e0e0;
padding: 20px;
box-sizing: border-box;
text-align: center;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-message {
color: #d32f2f;
font-size: 16px;
max-width: 80%;
}

View File

@ -11,6 +11,10 @@ export class EventsManager {
window.addEventListener('message', this.handleMessage.bind(this));
}
public cleanup() {
window.removeEventListener('message', this.handleMessage);
}
private handleMessage(event: MessageEvent) {
const message: OutboundEventMessage = event.data;
// Validate message origin (security measure)

View File

@ -2,6 +2,7 @@ import { WebComponentCommand } from '../models/command.model';
import { InboundCommandMessage } from '../models/message.type';
import { CommandsManager } from './CommandsManager';
import { EventsManager } from './EventsManager';
import styles from '../assets/css/styles.css';
/**
* The `OpenViduMeet` web component provides an interface for embedding an OpenVidu Meet room within a web page.
@ -27,7 +28,11 @@ export class OpenViduMeet extends HTMLElement {
private iframe: HTMLIFrameElement;
private commandsManager: CommandsManager;
private eventsManager: EventsManager;
//!FIXME: Insecure by default
private allowedOrigin: string = '*';
private loadTimeout: any;
private iframeLoaded = false;
private errorMessage: string | null = null;
constructor() {
super();
@ -52,30 +57,81 @@ export class OpenViduMeet extends HTMLElement {
this.updateIframeSrc();
}
private render() {
const style = document.createElement('style');
style.textContent = `
:host {
display: block;
width: 100%;
height: 100%;
disconnectedCallback() {
// Clean up resources
if (this.loadTimeout) {
clearTimeout(this.loadTimeout);
}
iframe {
width: 100%;
height: 100%;
border: none;
}
`;
this.shadowRoot?.appendChild(style);
this.shadowRoot?.appendChild(this.iframe);
this.iframe.onload = () => {
this.eventsManager.cleanup();
}
/**
* Renders the Web Component in the shadow DOM
*/
private render() {
// Add styles
const styleElement = document.createElement('style');
styleElement.textContent = styles;
this.shadowRoot?.appendChild(styleElement);
if (this.errorMessage) {
const errorContainer = document.createElement('div');
errorContainer.className = 'error-container';
const errorIcon = document.createElement('div');
errorIcon.className = 'error-icon';
errorIcon.textContent = '⚠️';
const errorMessageEl = document.createElement('div');
errorMessageEl.className = 'error-message';
errorMessageEl.textContent = this.errorMessage;
errorContainer.appendChild(errorIcon);
errorContainer.appendChild(errorMessageEl);
this.shadowRoot?.appendChild(errorContainer);
} else {
// Configure the iframe and Add it to the DOM
this.setupIframe();
this.shadowRoot?.appendChild(this.iframe);
}
}
/**
* Sets up the iframe with error handlers and loading timeout
*/
private setupIframe() {
// Clear any previous timeout
if (this.loadTimeout) {
clearTimeout(this.loadTimeout);
}
// Reset states
this.iframeLoaded = false;
// Set up load handlers
this.iframe.onload = (event: Event) => {
console.warn('Iframe loaded', event);
const message: InboundCommandMessage = {
command: WebComponentCommand.INITIALIZE,
payload: { domain: window.location.origin }
};
this.commandsManager.sendMessage(message);
this.iframeLoaded = true;
clearTimeout(this.loadTimeout);
this.loadTimeout = null;
this.iframe.onload = null;
};
// this.iframe.onload = this.handleIframeLoaded.bind(this);
this.iframe.onerror = (event: Event | string) => {
console.error('Iframe error:', event);
clearTimeout(this.loadTimeout);
this.showErrorState('Failed to load meeting');
};
// Set loading timeout
this.loadTimeout = setTimeout(() => {
if (!this.iframeLoaded) this.showErrorState('Loading timed out');
}, 10_000);
}
private updateIframeSrc() {
@ -99,7 +155,20 @@ export class OpenViduMeet extends HTMLElement {
this.iframe.src = url.toString();
}
// Public methods
/**
* Shows error state in the component UI
*/
private showErrorState(message: string) {
this.errorMessage = message;
// Re-render to show error state
while (this.shadowRoot?.firstChild) {
this.shadowRoot.removeChild(this.shadowRoot.firstChild);
}
this.render();
}
// ---- WebComponent Commands ----
// These methods send commands to the OpenVidu Meet iframe.
public endMeeting() {
const message: InboundCommandMessage = { command: WebComponentCommand.END_MEETING };

4
frontend/webcomponent/src/css.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.css' {
const content: string;
export default content;
}