import * as interfaces from '../../dist_ts_interfaces/index.js'; import { type IServerOptions, type ISecurityHeaders, TypedServer } from '../classes.typedserver.js'; import * as plugins from '../plugins.js'; export interface IUtilityWebsiteServerConstructorOptions { /** Custom route handler to add additional routes */ addCustomRoutes?: (typedserver: TypedServer) => Promise; /** Application semantic version */ appSemVer?: string; /** Domain name for the website */ domain: string; /** Directory to serve static files from */ serveDir: string; /** RSS feed metadata */ feedMetadata?: IServerOptions['feedMetadata']; /** Enable/disable CORS (default: true) */ cors?: boolean; /** Enable/disable SPA fallback (default: true) */ spaFallback?: boolean; /** Security headers configuration */ securityHeaders?: ISecurityHeaders; /** Force SSL redirect (default: false) */ forceSsl?: boolean; /** Port to listen on (default: 3000) */ port?: number; } /** * the utility website server implements a best practice server for websites * It supports: * * live reload * * serviceworker * * pwa manifest */ export class UtilityWebsiteServer { public options: IUtilityWebsiteServerConstructorOptions; public typedserver: TypedServer; public typedrouter = new plugins.typedrequest.TypedRouter(); constructor(optionsArg: IUtilityWebsiteServerConstructorOptions) { this.options = optionsArg; } /** * Start the website server */ public async start(portArg?: number) { const port = portArg ?? this.options.port ?? 3000; this.typedserver = new TypedServer({ // Core settings cors: this.options.cors ?? true, serveDir: this.options.serveDir, domain: this.options.domain, port, // Development features injectReload: true, watch: true, // SPA support (enabled by default for modern web apps) spaFallback: this.options.spaFallback ?? true, // Security forceSsl: this.options.forceSsl ?? false, securityHeaders: this.options.securityHeaders, // PWA manifest manifest: { name: this.options.domain, short_name: this.options.domain, start_url: '/', display_override: ['window-controls-overlay'], lang: 'en', background_color: '#000000', scope: '/', }, // SEO features robots: true, sitemap: true, feedMetadata: this.options.feedMetadata, }); let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = { appHash: 'xxxxxx', appSemVer: this.options.appSemVer || 'x.x.x', }; // -> Service worker version info handler this.typedserver.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler('serviceworker_versionInfo', async () => { return lswData; }) ); // ads.txt handler this.typedserver.addRoute('/ads.txt', 'GET', async () => { const adsTxt = ['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n'; return new Response(adsTxt, { status: 200, headers: { 'Content-Type': 'text/plain' }, }); }); // Asset broker manifest handler this.typedserver.addRoute( '/assetbroker/manifest/:manifestAsset', 'GET', async (request: Request) => { let manifestAssetName = (request as any).params?.manifestAsset; if (manifestAssetName === 'favicon.png') { manifestAssetName = `favicon_${this.options.domain .replace('.', '') .replace('losslesscom', 'lossless')}@2x_transparent.png`; } const fullOriginAssetUrl = `https://assetbroker.lossless.one/brandfiles/00general/${manifestAssetName}`; console.log(`Getting ${manifestAssetName} from ${fullOriginAssetUrl}`); const smartRequest = plugins.smartrequest.SmartRequest.create(); const response = await smartRequest.url(fullOriginAssetUrl).get(); const arrayBuffer = await response.arrayBuffer(); return new Response(arrayBuffer, { status: 200, headers: { 'Content-Type': 'image/png' }, }); } ); // Add any custom routes if (this.options.addCustomRoutes) { await this.options.addCustomRoutes(this.typedserver); } // Subscribe to serve directory hash changes this.typedserver.serveDirHashSubject.subscribe((appHash: string) => { lswData = { appHash, appSemVer: '1.0.0', }; }); // Setup the typedrouter chain this.typedserver.typedrouter.addTypedRouter(this.typedrouter); // Start everything console.log('routes are all set. Starting up now!'); await this.typedserver.start(); console.log('typedserver started!'); } public async stop() { await this.typedserver.stop(); } }