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:
parent
347d9472e0
commit
3a9f3c507d
1277
frontend/webcomponent/package-lock.json
generated
1277
frontend/webcomponent/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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 () {
|
||||
|
||||
36
frontend/webcomponent/src/assets/css/styles.css
Normal file
36
frontend/webcomponent/src/assets/css/styles.css
Normal 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%;
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
4
frontend/webcomponent/src/css.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.css' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user