UNPKG

webcm

Version:

Demonstrative implementation of a web-based manager for utilising Managed Components

294 lines (260 loc) 9.34 kB
import { MCEventListener } from '@managed-components/types' import express, { Request, Response, RequestHandler } from 'express' import { IncomingMessage, ClientRequest } from 'http' import { createProxyMiddleware, responseInterceptor, } from 'http-proxy-middleware' import * as path from 'path' import { Client, ClientGeneric } from './client' import { ManagerGeneric, MCEvent } from './manager' import { getConfig } from './config' import { PERMISSIONS } from './constants' import { StaticServer } from './static-server' import _locreq from 'locreq' const locreq = _locreq(__dirname) const DEFAULT_TARGET = 'http://localhost:8000' if (process.env.NODE_ENV === 'production') { process.on('unhandledRejection', (reason: Error) => { console.log('Unhandled Rejection at:', reason.stack || reason) }) } type BasicServerConfig = { configPath?: string componentsFolderPath?: string url?: string } type CustomComponentServerConfig = BasicServerConfig & { customComponentPath?: string customComponentSettings?: Record<string, unknown> } type ServerConfig = BasicServerConfig | CustomComponentServerConfig export async function startServerFromConfig({ configPath, componentsFolderPath, url, ...args }: ServerConfig) { const config = getConfig(configPath) let componentsPath = '' if (componentsFolderPath) { componentsPath = path.resolve(componentsFolderPath) } else { console.log('Components folder path not provided') } const { hostname, port, trackPath, components } = config if ('customComponentPath' in args && args.customComponentPath) { console.log( `⚠️ Custom component ${args.customComponentPath} will run with all permissions enabled, use webcm.config.ts to change what permissions it gets` ) components.push({ path: path.resolve(args.customComponentPath), permissions: Object.values(PERMISSIONS), // use all permissions, it's just for testing settings: args.customComponentSettings || {}, }) } if (url) { if (!(url.startsWith('http://') || url.startsWith('https://'))) { url = 'http://' + url } config.target = url } else if (!config.target) { const server = new StaticServer(8000) server.start() console.log('Started a demo static server at localhost:8000') config.target = DEFAULT_TARGET } const manager = new ManagerGeneric({ components, trackPath, componentsFolderPath: componentsPath, }) await manager.init() const getDefaultPayload = () => ({ pageVars: [], fetch: [], execute: [], return: undefined, }) const handleClientCreated = ( req: Request, _: Response, clientGeneric: ClientGeneric ) => { const cookieName = 'webcm_clientcreated' const eventName = 'clientcreated' let clientAlreadyCreated = clientGeneric.cookies.get(cookieName) || '' if (!manager.listeners[eventName]) return for (const componentName of Object.keys(manager.listeners[eventName])) { if (clientAlreadyCreated.split(',')?.includes(componentName)) continue const event = new MCEvent(eventName, req) event.client = new Client(componentName as string, clientGeneric) clientAlreadyCreated = Array.from( new Set([...clientAlreadyCreated.split(','), componentName]) ).join(',') clientGeneric.set(cookieName, clientAlreadyCreated) manager.listeners[eventName][componentName]?.forEach( (fn: MCEventListener) => fn(event) ) } } const handleEvent = async ( eventType: string, req: Request, res: Response ) => { res.payload = getDefaultPayload() const clientGeneric = new ClientGeneric(req, res, manager, config) handleClientCreated(req, res, clientGeneric) if (manager.listeners[eventType]) { // slightly alter ecommerce payload if (eventType === 'ecommerce') { req.body.payload.ecommerce = { ...req.body.payload.data } delete req.body.payload.data } const event = new MCEvent(eventType, req) for (const componentName of Object.keys(manager.listeners[eventType])) { event.client = new Client(componentName, clientGeneric) await Promise.all( manager.listeners[eventType][componentName].map( (fn: MCEventListener) => fn(event) ) ) } res.payload.execute.push(manager.getInjectedScript(clientGeneric)) } return res.end(JSON.stringify(res.payload)) } const handleClientEvent = async (req: Request, res: Response) => { res.payload = getDefaultPayload() const event = new MCEvent(req.body.payload.event, req) const clientGeneric = new ClientGeneric(req, res, manager, config) const clientComponentNames = Object.entries( clientGeneric.webcmPrefs.listeners ) .filter(([, events]) => events.includes(req.body.payload.event)) .map(([componentName]) => componentName) for (const component of clientComponentNames) { event.client = new Client(component, clientGeneric) try { await manager.clientListeners[ req.body.payload.event + '__' + component ](event) } catch { console.error( `Error dispatching ${req.body.payload.event} to ${component}: it isn't registered` ) } } res.payload.execute.push(manager.getInjectedScript(clientGeneric)) res.end(JSON.stringify(res.payload)) } // 'event', 'ecommerce' 'pageview', 'client' are the standard types // 'remarketing', 'identify' or any other event type const handleTrack: RequestHandler = (req, res) => { const eventType = req.body.eventType if (eventType === 'client') { return handleClientEvent(req, res) } else { return handleEvent(eventType, req, res) } } const handleRequest = (req: Request, clientGeneric: ClientGeneric) => { if (!manager.listeners['request']) return const requestEvent = new MCEvent('request', req) for (const componentName of Object.keys(manager.listeners['request'])) { requestEvent.client = new Client(componentName, clientGeneric) manager.listeners['request'][componentName]?.forEach( (fn: MCEventListener) => fn(requestEvent) ) } } const app = express().use(express.json()) app.set('trust proxy', true) // Mount WebCM endpoint app.post(trackPath, handleTrack) // Mount components endpoints for (const route of Object.keys(manager.mappedEndpoints)) { app.all(route, async (req, res) => { const response = await manager.mappedEndpoints[route](req) for (const [headerName, headerValue] of response.headers.entries()) { res.set(headerName, headerValue) } res.status(response.status) let isDone = false const reader = response.body?.getReader() while (!isDone && reader) { const { value, done } = await reader.read() if (value) res.write(Buffer.from(value)) isDone = done } res.end() }) } // Mount components proxied endpoints for (const component of Object.keys(manager.proxiedEndpoints)) { for (const [path, proxyTarget] of Object.entries( manager.proxiedEndpoints[component] )) { const proxyEndpoint = '/webcm/' + component + path app.all(proxyEndpoint + '*', async (req, res, next) => { const proxy = createProxyMiddleware({ target: proxyTarget + req.path.replace(proxyEndpoint, ''), ignorePath: true, followRedirects: true, changeOrigin: true, }) proxy(req, res, next) }) } } // Mount static files for (const [filePath, fileTarget] of Object.entries(manager.staticFiles)) { app.use(filePath, express.static(path.join(componentsPath, fileTarget))) } // Listen to all normal requests app.use('**', (req, res, next) => { res.payload = getDefaultPayload() const clientGeneric = new ClientGeneric(req, res, manager, config) const proxySettings = { target: config.target, changeOrigin: true, selfHandleResponse: true, onProxyReq: ( _proxyReq: ClientRequest, req: IncomingMessage, _res: Response ) => { handleRequest(req as Request, clientGeneric) }, onProxyRes: responseInterceptor( async (responseBuffer, _proxyRes, proxyReq, _res) => { if (proxyReq.headers['accept']?.toLowerCase().includes('text/html')) { let response = responseBuffer.toString('utf8') as string response = await manager.processEmbeds(response) response = await manager.processWidgets(response) return response.replace( '<head>', `<head><script>${manager.getInjectedScript( clientGeneric )};webcm._processServerResponse(${JSON.stringify( res.payload )})</script>` ) } return responseBuffer } ), } const proxy = createProxyMiddleware(proxySettings) proxy(req, res, next) }) console.info( '\nWebCM, version', process.env.npm_package_version || locreq('package.json').version ) app.listen(port, hostname) console.info( `\n🚀 WebCM is now proxying ${config.target} at http://${hostname}:${port}\n\n` ) }