164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
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<any>;
|
|
/** 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;
|
|
/** ads.txt entries (only served if configured) */
|
|
adsTxt?: string[];
|
|
/** Response compression configuration (default: enabled with brotli + gzip) */
|
|
compression?: plugins.smartserve.ICompressionConfig | boolean;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
|
|
// Compression
|
|
compression: this.options.compression,
|
|
|
|
// 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<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
|
|
new plugins.typedrequest.TypedHandler('serviceworker_versionInfo', async () => {
|
|
return lswData;
|
|
})
|
|
);
|
|
|
|
// 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' },
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|