feat: Refactor TypedServer to use SmartServe and introduce new request handlers

- Removed legacy servertools and Express dependencies in favor of SmartServe.
- Introduced DevToolsHandler and TypedRequestHandler for handling specific routes.
- Added support for custom route registration with regex parsing.
- Implemented sitemap and feed handling with dedicated helper classes.
- Enhanced HTML response handling with reload script injection.
- Updated UtilityServiceServer and UtilityWebsiteServer to utilize new TypedServer API.
- Removed deprecated compression options and Express-based route handling.
- Added comprehensive request handling for various endpoints including robots.txt, manifest.json, and sitemap.
- Improved error handling and response formatting across the server.
This commit is contained in:
2025-12-02 20:26:34 +00:00
parent 1c0c88a7cb
commit c17d6dac35
13 changed files with 902 additions and 705 deletions

View File

@@ -60,7 +60,7 @@
"dependencies": {
"@api.global/typedrequest": "^3.1.10",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^3.0.1",
"@api.global/typedsocket": "^3.1.1",
"@cloudflare/workers-types": "^4.20251202.0",
"@design.estate/dees-comms": "^1.0.27",
"@push.rocks/lik": "^6.2.2",
@@ -82,6 +82,7 @@
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartserve": "^1.1.0",
"@push.rocks/smartsitemap": "^2.0.4",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smarttime": "^4.1.1",

680
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import * as interfaces from '../dist_ts_interfaces/index.js';
import * as servertools from './servertools/index.js';
import { type TCompressionMethod } from './servertools/classes.compressor.js';
import { DevToolsHandler } from './controllers/controller.devtools.js';
import { TypedRequestHandler } from './controllers/controller.typedrequest.js';
export interface IServerOptions {
/**
@@ -15,16 +15,6 @@ export interface IServerOptions {
*/
injectReload?: boolean;
/**
* enable compression
*/
enableCompression?: boolean;
/**
* choose a preferred compression method
*/
preferredCompressionMethod?: TCompressionMethod;
/**
* watch the serve directory?
*/
@@ -34,7 +24,6 @@ export interface IServerOptions {
/**
* a default answer given in case there is no other handler.
* @returns
*/
defaultAnswer?: () => Promise<string>;
@@ -42,13 +31,14 @@ export interface IServerOptions {
* will try to reroute traffic to an ssl connection using headers
*/
forceSsl?: boolean;
/**
* allows serving manifests
*/
manifest?: plugins.smartmanifest.ISmartManifestConstructorOptions;
/**
* the port to listen on
* can be overwritten when actually starting the server
*/
port?: number | string;
publicKey?: string;
@@ -57,6 +47,7 @@ export interface IServerOptions {
feed?: boolean;
robots?: boolean;
domain?: string;
/**
* convey information about the app being served
*/
@@ -66,22 +57,45 @@ export interface IServerOptions {
blockWaybackMachine?: boolean;
}
export class TypedServer {
// static
// nothing here yet
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'ALL';
export interface IRouteHandler {
(request: Request): Promise<Response | null>;
}
export interface IRegisteredRoute {
pattern: string;
regex: RegExp;
paramNames: string[];
method: THttpMethod;
handler: IRouteHandler;
}
export class TypedServer {
// instance
public options: IServerOptions;
public server: servertools.Server;
public smartServe: plugins.smartserve.SmartServe;
public smartwatchInstance: plugins.smartwatch.Smartwatch;
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
public serveHash: string = '000000';
public typedsocket: plugins.typedsocket.TypedSocket;
public typedrouter = new plugins.typedrequest.TypedRouter();
// Sitemap and Feed helpers
private sitemapHelper: SitemapHelper;
private feedHelper: FeedHelper;
private smartmanifestInstance: plugins.smartmanifest.SmartManifest;
// Request handlers
private devToolsHandler: DevToolsHandler;
private typedRequestHandler: TypedRequestHandler;
// Custom route handlers (for addRoute API)
private customRoutes: IRegisteredRoute[] = [];
public lastReload: number = Date.now();
public ended = false;
constructor(optionsArg: IServerOptions) {
const standardOptions: IServerOptions = {
port: 3000,
@@ -95,43 +109,74 @@ export class TypedServer {
...optionsArg,
};
this.server = new servertools.Server(this.options);
// add routes to the smartexpress instance
this.server.addRoute(
'/typedserver/:request',
new servertools.Handler('ALL', async (req, res) => {
switch (req.params.request) {
case 'devtools':
res.setHeader('Content-Type', 'text/javascript');
res.status(200);
const devtoolsContent = await plugins.fsInstance.file(paths.injectBundlePath).encoding('utf8').read();
res.write(devtoolsContent);
res.end();
break;
case 'reloadcheck':
console.log('got request for reloadcheck');
res.setHeader('Content-Type', 'text/plain');
res.status(200);
if (this.ended) {
res.write('end');
res.end();
return;
}
res.write(this.lastReload.toString());
res.end();
break;
default:
res.status(404);
res.write('Unknown request type');
res.end();
break;
}
// Initialize handlers
this.devToolsHandler = new DevToolsHandler({
getLastReload: () => this.lastReload,
getEnded: () => this.ended,
});
this.typedRequestHandler = new TypedRequestHandler(this.typedrouter);
}
/**
* Access sitemap URLs (for adding/replacing)
*/
public get sitemap() {
return this.sitemapHelper;
}
/**
* Add a custom route handler
* Supports Express-style path patterns like '/path/:param' and '/path/*splat'
* @param path - The route path pattern
* @param method - HTTP method (GET, POST, PUT, DELETE, PATCH, ALL)
* @param handler - Async function that receives Request and returns Response or null
*/
public addRoute(
path: string,
method: THttpMethod,
handler: IRouteHandler
): void {
// Convert Express-style path to regex
const paramNames: string[] = [];
let regexPattern = path
// Handle named parameters :param
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
})
);
this.server.addRoute(
'/typedrequest',
new servertools.HandlerTypedRouter(this.typedrouter)
);
// Handle wildcard *splat (matches everything including slashes)
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '(.*)';
});
// Ensure exact match
regexPattern = `^${regexPattern}$`;
this.customRoutes.push({
pattern: path,
regex: new RegExp(regexPattern),
paramNames,
method,
handler,
});
}
/**
* Parse route parameters from a path using a registered route
*/
private parseRouteParams(
route: IRegisteredRoute,
pathname: string
): Record<string, string> | null {
const match = pathname.match(route.regex);
if (!match) return null;
const params: Record<string, string> = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
return params;
}
/**
@@ -145,52 +190,60 @@ export class TypedServer {
);
}
if (this.options.serveDir) {
this.server.addRoute(
'/{*splat}',
new servertools.HandlerStatic(this.options.serveDir, {
responseModifier: async (responseArg) => {
if (plugins.path.parse(responseArg.path).ext === '.html') {
let fileString = responseArg.responseContent.toString();
const fileStringArray = fileString.split('<head>');
if (this.options.injectReload && fileStringArray.length === 2) {
fileStringArray[0] = `${fileStringArray[0]}<head>
<!-- injected by @apiglobal/typedserver start -->
<script async defer type="module" src="/typedserver/devtools"></script>
<script>
globalThis.typedserver = {
lastReload: ${this.lastReload},
versionInfo: ${JSON.stringify({}, null, 2)},
}
</script>
<!-- injected by @apiglobal/typedserver stop -->
`;
fileString = fileStringArray.join('');
console.log('injected typedserver script.');
responseArg.responseContent = Buffer.from(fileString);
} else if (this.options.injectReload) {
console.log('Could not insert typedserver script - no <head> tag found');
}
}
const headers = responseArg.headers;
headers.appHash = this.serveHash;
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
headers['Pragma'] = 'no-cache';
headers['Expires'] = '0';
return {
headers,
path: responseArg.path,
responseContent: responseArg.responseContent,
travelData: responseArg.travelData,
};
},
serveIndexHtmlDefault: true,
enableCompression: this.options.enableCompression,
preferredCompressionMethod: this.options.preferredCompressionMethod,
})
);
const port =
typeof this.options.port === 'string'
? parseInt(this.options.port, 10)
: this.options.port || 3000;
// Initialize optional helpers
if (this.options.sitemap) {
this.sitemapHelper = new SitemapHelper(this.options.domain);
}
if (this.options.feed) {
this.feedHelper = new FeedHelper();
}
if (this.options.manifest) {
this.smartmanifestInstance = new plugins.smartmanifest.SmartManifest(this.options.manifest);
}
// Build SmartServe options
const smartServeOptions: plugins.smartserve.ISmartServeOptions = {
port,
hostname: '0.0.0.0',
tls:
this.options.privateKey && this.options.publicKey
? {
key: this.options.privateKey,
cert: this.options.publicKey,
}
: undefined,
websocket: {
typedRouter: this.typedrouter,
onConnectionOpen: (peer) => {
peer.tags.add('typedserver_frontend');
console.log(`WebSocket connected: ${peer.id}`);
},
onConnectionClose: (peer) => {
console.log(`WebSocket disconnected: ${peer.id}`);
},
},
static: this.options.serveDir
? {
root: this.options.serveDir,
index: ['index.html'],
etag: true,
}
: undefined,
};
this.smartServe = new plugins.smartserve.SmartServe(smartServeOptions);
// Set up custom request handler for all custom routes
this.smartServe.setHandler(async (request: Request): Promise<Response | null> => {
return this.handleRequest(request);
});
// Setup file watching
if (this.options.watch && this.options.serveDir) {
try {
this.smartwatchInstance = new plugins.smartwatch.Smartwatch([this.options.serveDir]);
@@ -202,20 +255,21 @@ export class TypedServer {
await this.createServeDirHash();
} catch (error) {
console.error('Failed to initialize file watching:', error);
// Continue without file watching rather than crashing
}
}
// lets start the server
await this.server.start();
// Start the server
await this.smartServe.start();
console.log(`TypedServer listening on port ${port}`);
// Setup TypedSocket using SmartServe integration
try {
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
this.typedrouter,
this.server
this.typedsocket = plugins.typedsocket.TypedSocket.fromSmartServe(
this.smartServe,
this.typedrouter
);
// lets setup typedrouter
// Setup typedrouter handlers
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
return {
@@ -225,10 +279,235 @@ export class TypedServer {
);
} catch (error) {
console.error('Failed to initialize TypedSocket:', error);
// Continue without WebSocket support rather than crashing
}
}
/**
* Main request handler - routes to appropriate sub-handlers
*/
private async handleRequest(request: Request): Promise<Response | null> {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
// DevTools handler
let response = await this.devToolsHandler.handle(request);
if (response) return response;
// TypedRequest handler
response = await this.typedRequestHandler.handle(request);
if (response) return response;
// Custom routes (registered via addRoute)
for (const route of this.customRoutes) {
if (route.method === 'ALL' || route.method === method) {
const params = this.parseRouteParams(route, path);
if (params !== null) {
// Attach params to request for handler to access
(request as any).params = params;
response = await route.handler(request);
if (response) return response;
}
}
}
// Robots.txt
if (this.options.robots && this.options.domain && path === '/robots.txt' && method === 'GET') {
return this.handleRobots();
}
// Manifest.json
if (this.options.manifest && path === '/manifest.json' && method === 'GET') {
return this.handleManifest();
}
// Sitemap
if (this.options.sitemap && path === '/sitemap' && method === 'GET') {
return this.handleSitemap();
}
// Sitemap News
if (this.options.sitemap && path === '/sitemap-news' && method === 'GET') {
return this.handleSitemapNews();
}
// Feed
if (this.options.feed && path === '/feed' && method === 'GET') {
return this.handleFeed();
}
// App version
if (this.options.appVersion && path === '/appversion' && method === 'GET') {
return new Response(this.options.appVersion, {
status: 200,
headers: { 'Content-Type': 'text/plain' },
});
}
// HTML injection for reload (if enabled)
if (this.options.injectReload && this.options.serveDir) {
response = await this.handleHtmlWithInjection(request);
if (response) return response;
}
// Not handled - let SmartServe handle (static files, etc.)
return null;
}
/**
* Handle robots.txt request
*/
private handleRobots(): Response {
const waybackBlock = this.options.blockWaybackMachine
? `
User-Agent: ia_archiver
Disallow: /
`
: '';
const content = `
User-agent: Googlebot-News
Disallow: /account
Disallow: /login
User-agent: *
Disallow: /account
Disallow: /login
${waybackBlock}
Sitemap: https://${this.options.domain}/sitemap
Sitemap: https://${this.options.domain}/sitemap-news
`;
return new Response(content.trim(), {
status: 200,
headers: { 'Content-Type': 'text/plain' },
});
}
/**
* Handle manifest.json request
*/
private handleManifest(): Response {
return new Response(this.smartmanifestInstance.jsonString(), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
/**
* Handle sitemap request
*/
private async handleSitemap(): Promise<Response> {
const sitemapXmlString = await this.sitemapHelper.createSitemap();
return new Response(sitemapXmlString, {
status: 200,
headers: { 'Content-Type': 'application/xml' },
});
}
/**
* Handle sitemap-news request
*/
private async handleSitemapNews(): Promise<Response> {
if (!this.options.articleGetterFunction) {
return new Response('no article getter function defined.', { status: 500 });
}
const sitemapNewsXml = await this.sitemapHelper.createSitemapNews(
await this.options.articleGetterFunction()
);
return new Response(sitemapNewsXml, {
status: 200,
headers: { 'Content-Type': 'application/xml' },
});
}
/**
* Handle feed request
*/
private async handleFeed(): Promise<Response> {
if (!this.options.feedMetadata) {
return new Response('feed metadata is missing', { status: 500 });
}
if (!this.options.articleGetterFunction) {
return new Response('no article getter function defined.', { status: 500 });
}
const xmlString = await this.feedHelper.createFeed(
this.options.feedMetadata,
await this.options.articleGetterFunction()
);
return new Response(xmlString, {
status: 200,
headers: { 'Content-Type': 'application/xml' },
});
}
/**
* Handle HTML files with reload script injection
*/
private async handleHtmlWithInjection(request: Request): Promise<Response | null> {
const url = new URL(request.url);
const requestPath = url.pathname;
// Check if this is a request for an HTML file or root
if (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.')) {
try {
let filePath = requestPath === '/' ? 'index.html' : requestPath.slice(1);
if (!filePath.endsWith('.html') && !filePath.includes('.')) {
filePath = plugins.path.join(filePath, 'index.html');
}
const fullPath = plugins.path.join(this.options.serveDir, filePath);
// Security check
if (!fullPath.startsWith(this.options.serveDir)) {
return new Response('Forbidden', { status: 403 });
}
let fileContent = (await plugins.fsInstance
.file(fullPath)
.encoding('utf8')
.read()) as string;
// Inject reload script
if (fileContent.includes('<head>')) {
const injection = `<head>
<!-- injected by @apiglobal/typedserver start -->
<script async defer type="module" src="/typedserver/devtools"></script>
<script>
globalThis.typedserver = {
lastReload: ${this.lastReload},
versionInfo: ${JSON.stringify({}, null, 2)},
}
</script>
<!-- injected by @apiglobal/typedserver stop -->
`;
fileContent = fileContent.replace('<head>', injection);
console.log('injected typedserver script.');
}
return new Response(fileContent, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
appHash: this.serveHash,
},
});
} catch (error) {
// Fall through to default handling
}
}
return null;
}
/**
* reloads the page
*/
@@ -238,14 +517,17 @@ export class TypedServer {
console.warn('TypedSocket not initialized, skipping client notifications');
return;
}
try {
const connections = await this.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
const connections = await this.typedsocket.findAllTargetConnectionsByTag(
'typedserver_frontend'
);
for (const connection of connections) {
const pushTime = this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
'pushLatestServerChangeTime',
connection
);
const pushTime =
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
'pushLatestServerChangeTime',
connection
);
pushTime.fire({
time: this.lastReload,
});
@@ -260,7 +542,7 @@ export class TypedServer {
*/
public async stop(): Promise<void> {
this.ended = true;
const stopWithErrorHandling = async (
stopFn: () => Promise<unknown>,
componentName: string
@@ -271,24 +553,24 @@ export class TypedServer {
console.error(`Error stopping ${componentName}:`, err);
}
};
const tasks: Promise<void>[] = [];
// Stop server
if (this.server) {
tasks.push(stopWithErrorHandling(() => this.server.stop(), 'server'));
// Stop SmartServe
if (this.smartServe) {
tasks.push(stopWithErrorHandling(() => this.smartServe.stop(), 'SmartServe'));
}
// Stop TypedSocket
// Stop TypedSocket (in SmartServe mode, this is a no-op but good for cleanup)
if (this.typedsocket) {
tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
}
// Stop file watcher
if (this.smartwatchInstance) {
tasks.push(stopWithErrorHandling(() => this.smartwatchInstance.stop(), 'file watcher'));
}
await Promise.all(tasks);
}
@@ -306,11 +588,62 @@ export class TypedServer {
this.serveDirHashSubject.next(this.serveHash);
} catch (error) {
console.error('Failed to create serve directory hash:', error);
// Use a timestamp-based hash as fallback
const fallbackHash = Date.now().toString(16).slice(-6);
this.serveHash = fallbackHash;
console.log('Using fallback hash: ' + fallbackHash);
this.serveDirHashSubject.next(fallbackHash);
}
}
}
}
// ============================================================================
// Helper Classes
// ============================================================================
/**
* Sitemap helper class
*/
class SitemapHelper {
private smartSitemap = new plugins.smartsitemap.SmartSitemap();
public urls: plugins.smartsitemap.IUrlInfo[] = [];
constructor(domain?: string) {
if (domain) {
this.urls.push({
url: `https://${domain}/`,
timestamp: Date.now(),
frequency: 'daily',
});
}
}
async createSitemap(): Promise<string> {
return this.smartSitemap.createSitemapFromUrlInfoArray(this.urls);
}
async createSitemapNews(articles: plugins.tsclass.content.IArticle[]): Promise<string> {
return this.smartSitemap.createSitemapNewsFromArticleArray(articles);
}
replaceUrls(urlsArg: plugins.smartsitemap.IUrlInfo[]) {
this.urls = urlsArg;
}
addUrls(urlsArg: plugins.smartsitemap.IUrlInfo[]) {
this.urls = this.urls.concat(urlsArg);
}
}
/**
* Feed helper class
*/
class FeedHelper {
private smartfeedInstance = new plugins.smartfeed.Smartfeed();
async createFeed(
feedMetadata: plugins.smartfeed.IFeedOptions,
articles: plugins.tsclass.content.IArticle[]
): Promise<string> {
return this.smartfeedInstance.createFeedFromArticleArray(feedMetadata, articles);
}
}

View File

@@ -0,0 +1,53 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
/**
* DevTools controller for TypedServer
* Handles /typedserver/devtools and /typedserver/reloadcheck endpoints
*/
@plugins.smartserve.Route('/typedserver')
export class DevToolsController {
private getLastReload: () => number;
private getEnded: () => boolean;
constructor(options: { getLastReload: () => number; getEnded: () => boolean }) {
this.getLastReload = options.getLastReload;
this.getEnded = options.getEnded;
}
@plugins.smartserve.Get('/devtools')
async getDevtools(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
const devtoolsContent = (await plugins.fsInstance
.file(paths.injectBundlePath)
.encoding('utf8')
.read()) as string;
return new Response(devtoolsContent, {
status: 200,
headers: {
'Content-Type': 'text/javascript',
},
});
}
@plugins.smartserve.Get('/reloadcheck')
async reloadCheck(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
console.log('got request for reloadcheck');
if (this.getEnded()) {
return new Response('end', {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
});
}
return new Response(this.getLastReload().toString(), {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
});
}
}

View File

@@ -0,0 +1,43 @@
import * as plugins from '../plugins.js';
/**
* TypedRequest handler for type-safe RPC endpoint
*/
export class TypedRequestHandler {
private typedRouter: plugins.typedrequest.TypedRouter;
constructor(typedRouter: plugins.typedrequest.TypedRouter) {
this.typedRouter = typedRouter;
}
/**
* Handle a request - returns Response if handled, null otherwise
*/
public async handle(request: Request): Promise<Response | null> {
const url = new URL(request.url);
const path = url.pathname;
if (path === '/typedrequest' && request.method === 'POST') {
try {
const body = await request.json();
const response = await this.typedRouter.routeAndAddResponse(body);
return new Response(plugins.smartjson.stringify(response), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid request' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
});
}
}
return null;
}
}

2
ts/controllers/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './controller.devtools.js';
export * from './controller.typedrequest.js';

View File

@@ -5,10 +5,10 @@ import * as servertools from './servertools/index.js';
export { servertools };
export * from './classes.typedserver.js';
// Type helpers
export type Request = plugins.express.Request;
export type Response = plugins.express.Response;
// Type helpers - using native Web API Request/Response types
// Native Request and Response are available in Node.js 18+ and all modern browsers
// Legacy Express types are available via servertools for backward compatibility
// lets export utilityservers
import * as utilityservers from './utilityservers/index.js';

View File

@@ -61,11 +61,16 @@ export {
// Create a ready-to-use smartfs instance with Node.js provider
export const fsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
// express
// @push.rocks/smartserve
import * as smartserve from '@push.rocks/smartserve';
export { smartserve };
// Legacy Express dependencies - kept for backward compatibility with deprecated servertools
// These will be removed in the next major version
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import express from 'express';
// @ts-ignore
import expressForceSsl from 'express-force-ssl';
export { bodyParser, cors, express, expressForceSsl };
export { express, bodyParser, cors, expressForceSsl };

View File

@@ -1,12 +1,22 @@
export * from './classes.server.js';
export * from './classes.route.js';
export * from './classes.handler.js';
export * from './classes.handlerstatic.js';
export * from './classes.handlerproxy.js';
export * from './classes.handlertypedrouter.js';
// Core utilities that don't depend on Express
export * from './classes.compressor.js';
import * as serviceworker from './tools.serviceworker.js';
export {
serviceworker,
}
// Legacy Express-based classes - deprecated, will be removed in next major version
// These are kept for backward compatibility but should not be used for new code
// Use SmartServe decorator-based controllers instead
/** @deprecated Use SmartServe directly */
export * from './classes.server.js';
/** @deprecated Use SmartServe @Route decorator */
export * from './classes.route.js';
/** @deprecated Use SmartServe @Get/@Post decorators */
export * from './classes.handler.js';
/** @deprecated Use SmartServe static file serving */
export * from './classes.handlerstatic.js';
/** @deprecated Use SmartServe custom handler */
export * from './classes.handlerproxy.js';
/** @deprecated Use SmartServe TypedRouter integration */
export * from './classes.handlertypedrouter.js';
// Service worker utilities - uses legacy patterns, will be migrated
import * as serviceworker from './tools.serviceworker.js';
export { serviceworker };

View File

@@ -1,10 +1,8 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as interfaces from '../../dist_ts_interfaces/index.js'
import { Handler } from './classes.handler.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import type { TypedServer } from '../classes.typedserver.js';
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
// Lazy-loaded service worker bundle content
let swBundleJs: string | null = null;
@@ -12,64 +10,83 @@ let swBundleJsMap: string | null = null;
const loadServiceWorkerBundle = async (): Promise<void> => {
if (swBundleJs === null) {
swBundleJs = await plugins.fsInstance
swBundleJs = (await plugins.fsInstance
.file(plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js'))
.encoding('utf8')
.read() as string;
.read()) as string;
}
if (swBundleJsMap === null) {
swBundleJsMap = await plugins.fsInstance
swBundleJsMap = (await plugins.fsInstance
.file(plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map'))
.encoding('utf8')
.read() as string;
.read()) as string;
}
};
let swVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
null;
const serviceworkerHandler = new Handler(
'GET',
async (req, res) => {
await loadServiceWorkerBundle();
if (req.path === '/serviceworker.bundle.js') {
res.status(200);
res.set('Content-Type', 'text/javascript');
res.write(swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`);
} else if (req.path === '/serviceworker.bundle.js.map') {
res.status(200);
res.set('Content-Type', 'application/json');
res.write(swBundleJsMap);
}
res.end();
}
);
export const addServiceWorkerRoute = (
typedserverInstance: TypedServer,
swDataFunc: () => interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']
) => {
// lets the version info as unique string;
// Set the version info
swVersionInfo = swDataFunc();
// the basic stuff
typedserverInstance.server.addRoute('/serviceworker/*splat', serviceworkerHandler);
// Service worker bundle handler
typedserverInstance.addRoute('/serviceworker/*splat', 'GET', async (request: Request) => {
await loadServiceWorkerBundle();
const url = new URL(request.url);
const path = url.pathname;
// the typed stuff
const typedrouter = new plugins.typedrequest.TypedRouter();
if (path === '/serviceworker/serviceworker.bundle.js' || path === '/serviceworker.bundle.js') {
return new Response(
swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`,
{
status: 200,
headers: { 'Content-Type': 'text/javascript' },
}
);
} else if (
path === '/serviceworker/serviceworker.bundle.js.map' ||
path === '/serviceworker.bundle.js.map'
) {
return new Response(swBundleJsMap, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
'serviceworker_versionInfo',
async (req) => {
const versionInfoResponse = swDataFunc();
return versionInfoResponse;
}
)
);
return null;
});
typedserverInstance.server.addRoute(
'/sw-typedrequest',
new HandlerTypedRouter(typedrouter)
);
// Typed request handler for service worker
typedserverInstance.addRoute('/sw-typedrequest', 'POST', async (request: Request) => {
try {
const body = await request.json();
// Create a local typed router for service worker requests
const typedrouter = new plugins.typedrequest.TypedRouter();
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
'serviceworker_versionInfo',
async () => {
return swDataFunc();
}
)
);
const response = await typedrouter.routeAndAddResponse(body);
return new Response(plugins.smartjson.stringify(response), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid request' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
});
};

View File

@@ -1,9 +1,7 @@
import { TypedServer } from '../classes.typedserver.js';
import * as servertools from '../servertools/index.js';
import * as plugins from '../plugins.js';
export interface ILoleServiceServerConstructorOptions {
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
serviceName: string;
serviceVersion: string;
serviceDomain: string;
@@ -20,12 +18,12 @@ export class UtilityServiceServer {
}
public async start() {
console.log('starting lole-serviceserver...')
console.log('starting lole-serviceserver...');
this.typedServer = new TypedServer({
cors: true,
domain: this.options.serviceDomain,
forceSsl: false,
port: this.options.port || 3000,
port: this.options.port || 3000,
robots: true,
defaultAnswer: async () => {
const InfoHtml = (await import('../infohtml/index.js')).InfoHtml;
@@ -37,9 +35,9 @@ export class UtilityServiceServer {
},
});
// lets add any custom routes
// Add any custom routes
if (this.options.addCustomRoutes) {
await this.options.addCustomRoutes(this.typedServer.server);
await this.options.addCustomRoutes(this.typedServer);
}
await this.typedServer.start();

View File

@@ -1,11 +1,10 @@
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { type IServerOptions, TypedServer } from '../classes.typedserver.js';
import type { Request, Response } from '../index.js';
import * as plugins from '../plugins.js';
import * as servertools from '../servertools/index.js';
export interface IUtilityWebsiteServerConstructorOptions {
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
appSemVer?: string;
domain: string;
serveDir: string;
@@ -16,7 +15,6 @@ export interface IUtilityWebsiteServerConstructorOptions {
* the utility website server implements a best practice server for websites
* It supports:
* * live reload
* * compression
* * serviceworker
* * pwa manifest
*/
@@ -30,7 +28,7 @@ export class UtilityWebsiteServer {
}
/**
*
* Start the website server
*/
public async start(portArg = 3000) {
this.typedserver = new TypedServer({
@@ -38,8 +36,6 @@ export class UtilityWebsiteServer {
injectReload: true,
watch: true,
serveDir: this.options.serveDir,
enableCompression: true,
preferredCompressionMethod: 'gzip',
domain: this.options.domain,
forceSsl: false,
manifest: {
@@ -58,33 +54,32 @@ export class UtilityWebsiteServer {
sitemap: true,
});
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
{
appHash: 'xxxxxx',
appSemVer: this.options.appSemVer || 'x.x.x',
};
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = {
appHash: 'xxxxxx',
appSemVer: this.options.appSemVer || 'x.x.x',
};
// -> /lsw* - anything regarding serviceworker
servertools.serviceworker.addServiceWorkerRoute(this.typedserver, () => {
return lswData;
});
// lets add ads.txt
this.typedserver.server.addRoute(
'/ads.txt',
new servertools.Handler('GET', async (req, res) => {
res.type('txt/plain');
const adsTxt =
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
res.write(adsTxt);
res.end();
})
);
// 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' },
});
});
this.typedserver.server.addRoute(
// Asset broker manifest handler
this.typedserver.addRoute(
'/assetbroker/manifest/:manifestAsset',
new servertools.Handler('GET', async (req, res) => {
let manifestAssetName = req.params.manifestAsset;
'GET',
async (request: Request) => {
let manifestAssetName = (request as any).params?.manifestAsset;
if (manifestAssetName === 'favicon.png') {
manifestAssetName = `favicon_${this.options.domain
.replace('.', '')
@@ -95,19 +90,19 @@ export class UtilityWebsiteServer {
const smartRequest = plugins.smartrequest.SmartRequest.create();
const response = await smartRequest.url(fullOriginAssetUrl).get();
const arrayBuffer = await response.arrayBuffer();
const dataBuffer: Buffer = Buffer.from(arrayBuffer);
res.type('.png');
res.write(dataBuffer);
res.end();
})
return new Response(arrayBuffer, {
status: 200,
headers: { 'Content-Type': 'image/png' },
});
}
);
// lets add any custom routes
// Add any custom routes
if (this.options.addCustomRoutes) {
await this.options.addCustomRoutes(this.typedserver.server);
await this.options.addCustomRoutes(this.typedserver);
}
// -> /* - serve the files
// Subscribe to serve directory hash changes
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
lswData = {
appHash,
@@ -115,11 +110,11 @@ export class UtilityWebsiteServer {
};
});
// lets setup the typedrouter chain
// Setup the typedrouter chain
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
// lets start everything
console.log('routes are all set. Startin up now!');
// Start everything
console.log('routes are all set. Starting up now!');
await this.typedserver.start();
console.log('typedserver started!');
}
@@ -127,14 +122,4 @@ export class UtilityWebsiteServer {
public async stop() {
await this.typedserver.stop();
}
/**
* allows you to hanlde requests from other server instances without the need to listen for yourself
* note smartexpress allows you start the instance wuith passing >>false<< as second parameter to .start();
* @param req
* @param res
*/
public async handleRequest(req: Request, res: Response) {
await this.typedserver.server.handleReqRes(req, res);
}
}

View File

@@ -1,7 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",