Files
typedserver/ts/utilityservers/classes.websiteserver.ts

159 lines
5.0 KiB
TypeScript
Raw Normal View History

2024-05-14 15:28:09 +02:00
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { type IServerOptions, type ISecurityHeaders, TypedServer } from '../classes.typedserver.js';
2024-05-14 15:28:09 +02:00
import * as plugins from '../plugins.js';
export interface IUtilityWebsiteServerConstructorOptions {
/** Custom route handler to add additional routes */
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
/** Application semantic version */
2024-05-14 15:28:09 +02:00
appSemVer?: string;
/** Domain name for the website */
2024-05-14 15:28:09 +02:00
domain: string;
/** Directory to serve static files from */
2024-05-14 15:28:09 +02:00
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;
/** ads.txt entries (only served if configured) */
adsTxt?: string[];
2024-05-14 15:28:09 +02:00
}
/**
* 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
2024-05-14 15:28:09 +02:00
*/
public async start(portArg?: number) {
const port = portArg ?? this.options.port ?? 3000;
2024-05-14 15:28:09 +02:00
this.typedserver = new TypedServer({
// Core settings
cors: this.options.cors ?? true,
2024-05-14 15:28:09 +02:00
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
2024-05-14 15:28:09 +02:00
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
2024-05-14 15:28:09 +02:00
robots: true,
sitemap: true,
feedMetadata: this.options.feedMetadata,
2024-05-14 15:28:09 +02:00
});
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = {
appHash: 'xxxxxx',
appSemVer: this.options.appSemVer || 'x.x.x',
};
2024-05-14 15:28:09 +02:00
// -> Service worker version info handler
this.typedserver.typedrouter.addTypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
new plugins.typedrequest.TypedHandler('serviceworker_versionInfo', async () => {
return lswData;
})
);
2024-05-14 15:28:09 +02:00
// ads.txt handler (only if configured)
if (this.options.adsTxt && this.options.adsTxt.length > 0) {
this.typedserver.addRoute('/ads.txt', 'GET', async () => {
const adsTxt = this.options.adsTxt.join('\n') + '\n';
return new Response(adsTxt, {
status: 200,
headers: { 'Content-Type': 'text/plain' },
});
});
}
2024-05-14 15:28:09 +02:00
// Asset broker manifest handler
this.typedserver.addRoute(
2024-05-14 15:28:09 +02:00
'/assetbroker/manifest/:manifestAsset',
'GET',
async (request: Request) => {
let manifestAssetName = (request as any).params?.manifestAsset;
2024-05-14 15:28:09 +02:00
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' },
});
}
2024-05-14 15:28:09 +02:00
);
// Add any custom routes
2024-05-14 15:28:09 +02:00
if (this.options.addCustomRoutes) {
await this.options.addCustomRoutes(this.typedserver);
2024-05-14 15:28:09 +02:00
}
// Subscribe to serve directory hash changes
2024-05-14 15:28:09 +02:00
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
lswData = {
appHash,
appSemVer: '1.0.0',
};
});
// Setup the typedrouter chain
2024-05-14 15:28:09 +02:00
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
// Start everything
console.log('routes are all set. Starting up now!');
2024-05-14 15:28:09 +02:00
await this.typedserver.start();
console.log('typedserver started!');
}
public async stop() {
await this.typedserver.stop();
}
}