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",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"playwright": "^1.50.1",
|
"playwright": "^1.50.1",
|
||||||
"rollup": "^4.34.8",
|
"rollup": "^4.34.8",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-jest-resolver": "^2.0.1",
|
"ts-jest-resolver": "^2.0.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
@ -3,21 +3,67 @@ import resolve from '@rollup/plugin-node-resolve'
|
|||||||
import commonjs from '@rollup/plugin-commonjs'
|
import commonjs from '@rollup/plugin-commonjs'
|
||||||
import typescript from '@rollup/plugin-typescript'
|
import typescript from '@rollup/plugin-typescript'
|
||||||
import terser from '@rollup/plugin-terser'
|
import terser from '@rollup/plugin-terser'
|
||||||
|
import postcss from 'rollup-plugin-postcss'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/index.ts',
|
input: 'src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: './dist/openvidu-meet.bundle.min.js',
|
file: './dist/openvidu-meet.bundle.min.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
name: 'OpenViduMeet',
|
name: 'OpenViduMeet',
|
||||||
sourcemap: true
|
sourcemap: !production
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve({
|
||||||
commonjs(),
|
// Prioritize modern ES modules
|
||||||
typescript({ tsconfig: './tsconfig.json' }),
|
mainFields: ['module', 'browser', 'main']
|
||||||
terser(),
|
}),
|
||||||
|
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',
|
name: 'copy-bundle',
|
||||||
writeBundle () {
|
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));
|
window.addEventListener('message', this.handleMessage.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
window.removeEventListener('message', this.handleMessage);
|
||||||
|
}
|
||||||
|
|
||||||
private handleMessage(event: MessageEvent) {
|
private handleMessage(event: MessageEvent) {
|
||||||
const message: OutboundEventMessage = event.data;
|
const message: OutboundEventMessage = event.data;
|
||||||
// Validate message origin (security measure)
|
// Validate message origin (security measure)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { WebComponentCommand } from '../models/command.model';
|
|||||||
import { InboundCommandMessage } from '../models/message.type';
|
import { InboundCommandMessage } from '../models/message.type';
|
||||||
import { CommandsManager } from './CommandsManager';
|
import { CommandsManager } from './CommandsManager';
|
||||||
import { EventsManager } from './EventsManager';
|
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.
|
* 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 iframe: HTMLIFrameElement;
|
||||||
private commandsManager: CommandsManager;
|
private commandsManager: CommandsManager;
|
||||||
private eventsManager: EventsManager;
|
private eventsManager: EventsManager;
|
||||||
|
//!FIXME: Insecure by default
|
||||||
private allowedOrigin: string = '*';
|
private allowedOrigin: string = '*';
|
||||||
|
private loadTimeout: any;
|
||||||
|
private iframeLoaded = false;
|
||||||
|
private errorMessage: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -52,30 +57,81 @@ export class OpenViduMeet extends HTMLElement {
|
|||||||
this.updateIframeSrc();
|
this.updateIframeSrc();
|
||||||
}
|
}
|
||||||
|
|
||||||
private render() {
|
disconnectedCallback() {
|
||||||
const style = document.createElement('style');
|
// Clean up resources
|
||||||
style.textContent = `
|
if (this.loadTimeout) {
|
||||||
:host {
|
clearTimeout(this.loadTimeout);
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
iframe {
|
this.eventsManager.cleanup();
|
||||||
width: 100%;
|
}
|
||||||
height: 100%;
|
|
||||||
border: none;
|
/**
|
||||||
}
|
* Renders the Web Component in the shadow DOM
|
||||||
`;
|
*/
|
||||||
this.shadowRoot?.appendChild(style);
|
private render() {
|
||||||
this.shadowRoot?.appendChild(this.iframe);
|
// Add styles
|
||||||
this.iframe.onload = () => {
|
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 = {
|
const message: InboundCommandMessage = {
|
||||||
command: WebComponentCommand.INITIALIZE,
|
command: WebComponentCommand.INITIALIZE,
|
||||||
payload: { domain: window.location.origin }
|
payload: { domain: window.location.origin }
|
||||||
};
|
};
|
||||||
this.commandsManager.sendMessage(message);
|
this.commandsManager.sendMessage(message);
|
||||||
|
this.iframeLoaded = true;
|
||||||
|
clearTimeout(this.loadTimeout);
|
||||||
|
this.loadTimeout = null;
|
||||||
this.iframe.onload = 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() {
|
private updateIframeSrc() {
|
||||||
@ -99,7 +155,20 @@ export class OpenViduMeet extends HTMLElement {
|
|||||||
this.iframe.src = url.toString();
|
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() {
|
public endMeeting() {
|
||||||
const message: InboundCommandMessage = { command: WebComponentCommand.END_MEETING };
|
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