feat(typedserver): Remove legacy Express-based servertools, drop express deps, and refactor TypedServer to SmartServe + typedrouter with CORS support
This commit is contained in:
1
.serena/.gitignore
vendored
1
.serena/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/cache
|
||||
@@ -1,68 +0,0 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "typedserver"
|
||||
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-04 - 7.6.0 - feat(typedserver)
|
||||
Remove legacy Express-based servertools, drop express deps, and refactor TypedServer to SmartServe + typedrouter with CORS support
|
||||
|
||||
- Remove legacy ts/servertools module and many Express-based helpers (classes.server, handler, handlerstatic, handlerproxy, compressor, sitemap, feed, tools.*). The servertools compatibility layer is no longer available.
|
||||
- Drop express-related dependencies from package.json (@types/express, express, body-parser, cors, express-force-ssl).
|
||||
- Refactor core API: ts/index.ts no longer exports servertools and ts/plugins.ts no longer re-exports Express middleware — consumers must migrate to SmartServe/typedrequest/typedsocket primitives.
|
||||
- TypedServer rewritten: integrates with @push.rocks/smartserve ControllerRegistry, adds custom route parsing, CORS header helper and OPTIONS preflight handling, improved static file handling with optional reload injection, file watching, typedrouter and typedsocket integration.
|
||||
- UtilityWebsiteServer now registers the serviceworker versionInfo handler on typedrouter instead of using the removed servertools.serviceworker helper.
|
||||
- This is a breaking change — public APIs and dependency surface changed; bump major version.
|
||||
|
||||
## 2025-12-04 - 7.5.0 - feat(serviceworker)
|
||||
Add real-time service worker push updates and DeesComms integration (metrics, events, resource caching)
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"HTTP server",
|
||||
"SSL",
|
||||
"cors",
|
||||
"express middleware",
|
||||
"proxy",
|
||||
"sitemap",
|
||||
"feeds",
|
||||
@@ -92,11 +91,6 @@
|
||||
"@push.rocks/webrequest": "^4.0.1",
|
||||
"@push.rocks/webstore": "^2.0.20",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"body-parser": "^2.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.2.1",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -107,21 +107,6 @@ importers:
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.3.0
|
||||
version: 9.3.0
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
body-parser:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
express-force-ssl:
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.2
|
||||
lit:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '7.5.0',
|
||||
version: '7.6.0',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
||||
@@ -381,6 +381,25 @@ export class TypedServer {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add CORS headers to a response
|
||||
*/
|
||||
private addCorsHeaders(response: Response): Response {
|
||||
if (!this.options.cors) return response;
|
||||
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('Access-Control-Allow-Origin', '*');
|
||||
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
|
||||
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
||||
headers.set('Access-Control-Max-Age', '86400');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main request handler - routes to appropriate sub-handlers
|
||||
*/
|
||||
@@ -389,6 +408,25 @@ export class TypedServer {
|
||||
const path = url.pathname;
|
||||
const method = request.method.toUpperCase() as THttpMethod;
|
||||
|
||||
// Handle OPTIONS preflight for CORS
|
||||
if (method === 'OPTIONS' && this.options.cors) {
|
||||
return this.addCorsHeaders(new Response(null, { status: 204 }));
|
||||
}
|
||||
|
||||
// Process the request and wrap response with CORS headers
|
||||
const response = await this.handleRequestInternal(request, url, path, method);
|
||||
return this.addCorsHeaders(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal request handler - routes to appropriate sub-handlers
|
||||
*/
|
||||
private async handleRequestInternal(
|
||||
request: Request,
|
||||
url: URL,
|
||||
path: string,
|
||||
method: THttpMethod
|
||||
): Promise<Response> {
|
||||
// First, try to match via ControllerRegistry (decorated routes)
|
||||
const match = plugins.smartserve.ControllerRegistry.matchRoute(path, method);
|
||||
if (match) {
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import * as servertools from './servertools/index.js';
|
||||
|
||||
export { servertools };
|
||||
|
||||
export * from './classes.typedserver.js';
|
||||
|
||||
// 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';
|
||||
export { utilityservers };
|
||||
|
||||
@@ -65,12 +65,3 @@ export const fsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode())
|
||||
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 expressForceSsl from 'express-force-ssl';
|
||||
|
||||
export { express, bodyParser, cors, expressForceSsl };
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export type TCompressionMethod = 'gzip' | 'deflate' | 'br' | 'none';
|
||||
export interface ICompressionResult {
|
||||
result: Buffer;
|
||||
compressionMethod: TCompressionMethod;
|
||||
}
|
||||
|
||||
export class Compressor {
|
||||
private _cache: Map<string, Buffer>;
|
||||
private MAX_CACHE_SIZE: number = 100 * 1024 * 1024; // 100 MB
|
||||
|
||||
constructor() {
|
||||
this._cache = new Map<string, Buffer>();
|
||||
}
|
||||
|
||||
private _addToCache(key: string, value: Buffer) {
|
||||
this._cache.set(key, value);
|
||||
this._manageCacheSize();
|
||||
}
|
||||
|
||||
private _manageCacheSize() {
|
||||
let currentSize = Array.from(this._cache.values()).reduce((acc, buffer) => acc + buffer.length, 0);
|
||||
|
||||
while (currentSize > this.MAX_CACHE_SIZE) {
|
||||
const firstKey = this._cache.keys().next().value;
|
||||
const firstValue = this._cache.get(firstKey)!;
|
||||
currentSize -= firstValue.length;
|
||||
this._cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
public async compressContent(
|
||||
content: Buffer,
|
||||
method: 'gzip' | 'deflate' | 'br' | 'none'
|
||||
): Promise<Buffer> {
|
||||
const cacheKey = content.toString('base64') + method;
|
||||
const cachedResult = this._cache.get(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (err: Error | null, result: Buffer) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
this._addToCache(cacheKey, result);
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
switch (method) {
|
||||
case 'gzip':
|
||||
plugins.zlib.gzip(content, {
|
||||
level: 1,
|
||||
},callback,);
|
||||
break;
|
||||
case 'br':
|
||||
plugins.zlib.brotliCompress(content, {}, callback);
|
||||
break;
|
||||
case 'deflate':
|
||||
plugins.zlib.deflate(content, callback);
|
||||
break;
|
||||
default:
|
||||
this._addToCache(cacheKey, content);
|
||||
resolve(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public determineCompression(acceptEncoding: string | string[], preferredCompressionMethodsArg: TCompressionMethod[] = []) {
|
||||
// Ensure acceptEncoding is a single string
|
||||
const encodingString = Array.isArray(acceptEncoding)
|
||||
? acceptEncoding.join(', ')
|
||||
: acceptEncoding;
|
||||
|
||||
let compressionMethod: TCompressionMethod = 'none';
|
||||
|
||||
// Prioritize preferred compression methods if provided
|
||||
for (const preferredMethod of preferredCompressionMethodsArg) {
|
||||
if (new RegExp(`\\b${preferredMethod}\\b`).test(encodingString)) {
|
||||
return preferredMethod;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default prioritization if no preferred method matches
|
||||
if (/\bbr\b/.test(encodingString)) {
|
||||
compressionMethod = 'br';
|
||||
} else if (/\bgzip\b/.test(encodingString)) {
|
||||
compressionMethod = 'gzip';
|
||||
} else if (/\bdeflate\b/.test(encodingString)) {
|
||||
compressionMethod = 'deflate';
|
||||
}
|
||||
|
||||
return compressionMethod;
|
||||
}
|
||||
|
||||
public async maybeCompress(requestHeaders: plugins.http.IncomingHttpHeaders, content: Buffer, preferredCompressionMethodsArg?: TCompressionMethod[]): Promise<ICompressionResult> {
|
||||
const acceptEncoding = requestHeaders['accept-encoding'];
|
||||
const compressionMethod = this.determineCompression(acceptEncoding, preferredCompressionMethodsArg);
|
||||
const result = await this.compressContent(content, compressionMethod);
|
||||
return {
|
||||
result,
|
||||
compressionMethod,
|
||||
};
|
||||
}
|
||||
|
||||
public createCompressionStream(method: 'gzip' | 'deflate' | 'br' | 'none') {
|
||||
let compressionStream: any;
|
||||
switch (method) {
|
||||
case 'gzip':
|
||||
compressionStream = plugins.zlib.createGzip();
|
||||
return compressionStream;
|
||||
case 'br':
|
||||
compressionStream = plugins.zlib.createBrotliCompress({
|
||||
chunkSize: 16 * 1024,
|
||||
params: {
|
||||
|
||||
},
|
||||
});
|
||||
return compressionStream;
|
||||
case 'deflate':
|
||||
compressionStream = plugins.zlib.createDeflate();
|
||||
return compressionStream;
|
||||
default:
|
||||
compressionStream = plugins.smartstream.createPassThrough();
|
||||
return compressionStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export class Feed {
|
||||
public smartexpressRef: Server;
|
||||
public smartfeedInstance = new plugins.smartfeed.Smartfeed();
|
||||
|
||||
public feedHandler = new Handler('GET', async (req, res) => {
|
||||
if (!this.smartexpressRef.options.feedMetadata) {
|
||||
res.status(500);
|
||||
res.write('feed metadata is missing');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (!this.smartexpressRef.options.articleGetterFunction) {
|
||||
res.status(500);
|
||||
res.write('no article getter function defined.');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const xmlString = await this.smartfeedInstance.createFeedFromArticleArray(
|
||||
this.smartexpressRef.options.feedMetadata,
|
||||
await this.smartexpressRef.options.articleGetterFunction()
|
||||
);
|
||||
res.type('.xml');
|
||||
res.write(xmlString);
|
||||
res.end();
|
||||
});
|
||||
|
||||
constructor(smartexpressRefArg: Server) {
|
||||
this.smartexpressRef = smartexpressRefArg;
|
||||
this.smartexpressRef.addRouteBefore('/feed', this.feedHandler);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { type Request, type Response } from 'express';
|
||||
|
||||
export interface IHandlerFunction {
|
||||
(requestArg: Request, responseArg: Response): void;
|
||||
}
|
||||
|
||||
export type THttpMethods = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
|
||||
export class Handler {
|
||||
httpMethod: THttpMethods;
|
||||
handlerFunction: IHandlerFunction;
|
||||
constructor(httpMethodArg: THttpMethods, handlerArg: IHandlerFunction) {
|
||||
this.httpMethod = httpMethodArg;
|
||||
this.handlerFunction = handlerArg;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerProxy extends Handler {
|
||||
/**
|
||||
* The constuctor of HandlerProxy
|
||||
* @param remoteMountPointArg
|
||||
*/
|
||||
constructor(
|
||||
remoteMountPointArg: string,
|
||||
optionsArg?: {
|
||||
responseModifier?: interfaces.TResponseModifier;
|
||||
headers?: { [key: string]: string };
|
||||
}
|
||||
) {
|
||||
super('ALL', async (req, res) => {
|
||||
// Extract the path using Express 5's params or fallback methods
|
||||
let relativeRequestPath: string;
|
||||
if (req.params && req.params.splat !== undefined) {
|
||||
// Express 5 wildcard route (/*splat or /{*splat})
|
||||
// Handle array values - join them if array, otherwise use as-is
|
||||
relativeRequestPath = Array.isArray(req.params.splat) ? req.params.splat.join('/') : String(req.params.splat || '');
|
||||
} else if (req.params && req.params[0] !== undefined) {
|
||||
// Numbered parameter fallback
|
||||
relativeRequestPath = Array.isArray(req.params[0]) ? req.params[0].join('/') : String(req.params[0] || '');
|
||||
} else if (req.baseUrl) {
|
||||
// If there's a baseUrl, remove it from the path
|
||||
relativeRequestPath = req.path.slice(req.baseUrl.length);
|
||||
} else if (req.route && req.route.path === '/') {
|
||||
// Root route - use full path minus leading slash
|
||||
relativeRequestPath = req.path.slice(1);
|
||||
} else {
|
||||
// Fallback to the original slicing logic for compatibility
|
||||
relativeRequestPath = req.path.slice(req.route.path.length - 1);
|
||||
}
|
||||
|
||||
// Ensure relativeRequestPath is a string and has no leading slash
|
||||
relativeRequestPath = String(relativeRequestPath || '');
|
||||
if (relativeRequestPath.startsWith('/')) {
|
||||
relativeRequestPath = relativeRequestPath.slice(1);
|
||||
}
|
||||
const proxyRequestUrl = remoteMountPointArg + relativeRequestPath;
|
||||
console.log(`proxy ${req.path} to ${proxyRequestUrl}`);
|
||||
let proxiedResponse: plugins.smartrequest.ICoreResponse;
|
||||
try {
|
||||
const smartRequest = plugins.smartrequest.SmartRequest.create()
|
||||
.url(proxyRequestUrl);
|
||||
|
||||
// Execute request based on method
|
||||
switch (req.method.toUpperCase()) {
|
||||
case 'GET':
|
||||
proxiedResponse = await smartRequest.get();
|
||||
break;
|
||||
case 'POST':
|
||||
proxiedResponse = await smartRequest.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
proxiedResponse = await smartRequest.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
proxiedResponse = await smartRequest.delete();
|
||||
break;
|
||||
case 'PATCH':
|
||||
proxiedResponse = await smartRequest.patch();
|
||||
break;
|
||||
default:
|
||||
// For other methods, default to GET
|
||||
proxiedResponse = await smartRequest.get();
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
res.end('failed to fullfill request');
|
||||
return;
|
||||
}
|
||||
const headers = proxiedResponse.headers;
|
||||
for (const header of Object.keys(headers)) {
|
||||
res.set(header, headers[header] as string);
|
||||
}
|
||||
|
||||
// set additional headers
|
||||
if (optionsArg && optionsArg.headers) {
|
||||
for (const key of Object.keys(optionsArg.headers)) {
|
||||
res.set(key, optionsArg.headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get response body as buffer
|
||||
let responseToSend: Buffer;
|
||||
try {
|
||||
const arrayBuffer = await proxiedResponse.arrayBuffer();
|
||||
responseToSend = Buffer.from(arrayBuffer);
|
||||
} catch {
|
||||
// If we can't get arrayBuffer, try text
|
||||
try {
|
||||
const text = await proxiedResponse.text();
|
||||
responseToSend = Buffer.from(text);
|
||||
} catch {
|
||||
// Provide a default empty buffer if body cannot be read
|
||||
responseToSend = Buffer.from('');
|
||||
}
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.responseModifier) {
|
||||
const modifiedResponse = await optionsArg.responseModifier({
|
||||
headers: res.getHeaders(),
|
||||
path: req.path,
|
||||
responseContent: responseToSend,
|
||||
});
|
||||
|
||||
// headers
|
||||
for (const key of Object.keys(res.getHeaders())) {
|
||||
if (!modifiedResponse.headers[key]) {
|
||||
res.removeHeader(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(modifiedResponse.headers)) {
|
||||
res.setHeader(key, modifiedResponse.headers[key]);
|
||||
}
|
||||
|
||||
// responseContent
|
||||
responseToSend = modifiedResponse.responseContent;
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.write(responseToSend);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Compressor, type TCompressionMethod, type ICompressionResult } from './classes.compressor.js';
|
||||
|
||||
export class HandlerStatic extends Handler {
|
||||
public compressor = new Compressor();
|
||||
constructor(
|
||||
pathArg: string,
|
||||
optionsArg?: {
|
||||
requestModifier?: interfaces.TRequestModifier;
|
||||
responseModifier?: interfaces.TResponseModifier;
|
||||
headers?: { [key: string]: string };
|
||||
serveIndexHtmlDefault?: boolean;
|
||||
enableCompression?: boolean;
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
}
|
||||
) {
|
||||
super('GET', async (req, res) => {
|
||||
let requestPath = req.path;
|
||||
let requestHeaders = req.headers;
|
||||
let requestBody = req.body;
|
||||
let travelData: unknown;
|
||||
if (optionsArg && optionsArg.requestModifier) {
|
||||
const modifiedRequest = await optionsArg.requestModifier({
|
||||
headers: requestHeaders,
|
||||
path: requestPath,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
requestHeaders = modifiedRequest.headers;
|
||||
requestPath = modifiedRequest.path;
|
||||
requestBody = modifiedRequest.body;
|
||||
travelData = modifiedRequest.travelData;
|
||||
}
|
||||
|
||||
// lets compute some paths
|
||||
// Extract the path using Express 5's params or fallback methods
|
||||
let filePath: string;
|
||||
if (req.params && req.params.splat !== undefined) {
|
||||
// Express 5 wildcard route (/*splat or /{*splat})
|
||||
// Handle array values - join them if array, otherwise use as-is
|
||||
filePath = Array.isArray(req.params.splat) ? req.params.splat.join('/') : String(req.params.splat || '');
|
||||
} else if (req.params && req.params[0] !== undefined) {
|
||||
// Numbered parameter fallback
|
||||
filePath = Array.isArray(req.params[0]) ? req.params[0].join('/') : String(req.params[0] || '');
|
||||
} else if (req.baseUrl) {
|
||||
// If there's a baseUrl, remove it from the path
|
||||
filePath = requestPath.slice(req.baseUrl.length);
|
||||
} else if (req.route && req.route.path === '/') {
|
||||
// Root route - use full path minus leading slash
|
||||
filePath = requestPath.slice(1);
|
||||
} else {
|
||||
// Fallback to the original slicing logic for compatibility
|
||||
filePath = requestPath.slice(req.route.path.length - 1);
|
||||
}
|
||||
|
||||
// Ensure filePath is a string and has no leading slash
|
||||
filePath = String(filePath || '');
|
||||
if (filePath.startsWith('/')) {
|
||||
filePath = filePath.slice(1);
|
||||
}
|
||||
if (requestPath === '') {
|
||||
console.log('replaced root with index.html');
|
||||
filePath = 'index.html';
|
||||
}
|
||||
console.log(filePath);
|
||||
const joinedPath = plugins.path.join(pathArg, filePath);
|
||||
const defaultPath = plugins.path.join(pathArg, 'index.html');
|
||||
let parsedPath = plugins.path.parse(joinedPath);
|
||||
let usedPath: string;
|
||||
|
||||
// important security checks
|
||||
if (
|
||||
requestPath.includes('..') || // don't allow going up the filePath
|
||||
requestPath.includes('~') || // don't allow referencing of home directory
|
||||
!joinedPath.startsWith(pathArg) // make sure the joined path is within the directory
|
||||
) {
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// set additional headers
|
||||
if (optionsArg && optionsArg.headers) {
|
||||
for (const key of Object.keys(optionsArg.headers)) {
|
||||
res.set(key, optionsArg.headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// lets actually care about serving, if security checks pass
|
||||
let fileBuffer: Buffer;
|
||||
try {
|
||||
fileBuffer = await plugins.fsInstance.file(joinedPath).read() as Buffer;
|
||||
usedPath = joinedPath;
|
||||
} catch (err) {
|
||||
// try serving index.html instead
|
||||
console.log(`could not resolve ${joinedPath}`);
|
||||
if (optionsArg && optionsArg.serveIndexHtmlDefault) {
|
||||
console.log(`serving default path ${defaultPath} instead of ${joinedPath}`);
|
||||
try {
|
||||
parsedPath = plugins.path.parse(defaultPath);
|
||||
fileBuffer = await plugins.fsInstance.file(defaultPath).read() as Buffer;
|
||||
usedPath = defaultPath;
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
res.end('File not found!');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.writeHead(500);
|
||||
res.end('File not found!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.type(parsedPath.ext);
|
||||
|
||||
const headers = res.getHeaders();
|
||||
|
||||
// lets modify the response at last
|
||||
if (optionsArg && optionsArg.responseModifier) {
|
||||
const modifiedResponse = await optionsArg.responseModifier({
|
||||
headers: res.getHeaders(),
|
||||
path: usedPath,
|
||||
responseContent: fileBuffer,
|
||||
travelData,
|
||||
});
|
||||
|
||||
// headers
|
||||
for (const key of Object.keys(res.getHeaders())) {
|
||||
if (!modifiedResponse.headers[key]) {
|
||||
res.removeHeader(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(modifiedResponse.headers)) {
|
||||
res.setHeader(key, modifiedResponse.headers[key]);
|
||||
}
|
||||
|
||||
// responseContent
|
||||
fileBuffer = modifiedResponse.responseContent;
|
||||
}
|
||||
|
||||
// lets finally deal with compression
|
||||
let compressionResult: ICompressionResult;
|
||||
|
||||
if (optionsArg && optionsArg.enableCompression) {
|
||||
compressionResult = await this.compressor.maybeCompress(requestHeaders, fileBuffer, [optionsArg.preferredCompressionMethod]);
|
||||
} else {
|
||||
compressionResult = {
|
||||
compressionMethod: 'none',
|
||||
result: fileBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
if (compressionResult?.compressionMethod) {
|
||||
res.header('Content-Encoding', compressionResult.compressionMethod);
|
||||
res.write(compressionResult.result);
|
||||
} else {
|
||||
res.write(fileBuffer);
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerTypedRouter extends Handler {
|
||||
/**
|
||||
* The constuctor of HandlerProxy
|
||||
* @param remoteMountPointArg
|
||||
*/
|
||||
constructor(typedrouter: plugins.typedrequest.TypedRouter) {
|
||||
super('POST', async (req, res) => {
|
||||
const response = await typedrouter.routeAndAddResponse(req.body);
|
||||
res.type('json');
|
||||
res.write(plugins.smartjson.stringify(response));
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Server } from './classes.server.js';
|
||||
|
||||
import { type IRoute as IExpressRoute } from 'express';
|
||||
|
||||
export class Route {
|
||||
public routeString: string;
|
||||
|
||||
/**
|
||||
* an object map of handlers
|
||||
* Why multiple? Because GET, POST, PUT, DELETE, etc. can all have different handlers
|
||||
*/
|
||||
public handlerObjectMap = new plugins.lik.ObjectMap<Handler>();
|
||||
|
||||
public expressMiddlewareObjectMap = new plugins.lik.ObjectMap<any>();
|
||||
public expressRoute: IExpressRoute; // will be set to server route on server start
|
||||
constructor(ServerArg: Server, routeStringArg: string) {
|
||||
this.routeString = routeStringArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* add a handler to do something with requests
|
||||
* @param handlerArg
|
||||
*/
|
||||
public addHandler(handlerArg: Handler) {
|
||||
this.handlerObjectMap.add(handlerArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* add a express middleware
|
||||
* @param middlewareArg
|
||||
*/
|
||||
public addExpressMiddleWare(middlewareArg: plugins.express.Application) {
|
||||
this.expressMiddlewareObjectMap.add(middlewareArg);
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import { Route } from './classes.route.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
|
||||
|
||||
// export types
|
||||
import { setupRobots } from './tools.robots.js';
|
||||
import { setupManifest } from './tools.manifest.js';
|
||||
import { Sitemap } from './classes.sitemap.js';
|
||||
import { Feed } from './classes.feed.js';
|
||||
import { type IServerOptions } from '../classes.typedserver.js';
|
||||
export type TServerStatus = 'initiated' | 'running' | 'stopped';
|
||||
|
||||
/**
|
||||
* can be used to spawn a server to answer http/https calls
|
||||
* for constructor options see [[IServerOptions]]
|
||||
*/
|
||||
export class Server {
|
||||
public httpServer: plugins.http.Server | plugins.https.Server;
|
||||
public expressAppInstance: plugins.express.Application;
|
||||
public routeObjectMap = new Array<Route>();
|
||||
public options: IServerOptions;
|
||||
public serverStatus: TServerStatus = 'initiated';
|
||||
|
||||
public feed: Feed;
|
||||
public sitemap: Sitemap;
|
||||
|
||||
public executeAfterStartFunctions: (() => Promise<void>)[] = [];
|
||||
|
||||
// do stuff when server is ready
|
||||
private startedDeferred = plugins.smartpromise.defer();
|
||||
// tslint:disable-next-line:member-ordering
|
||||
public startedPromise = this.startedDeferred.promise;
|
||||
|
||||
private socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
||||
|
||||
constructor(optionsArg: IServerOptions) {
|
||||
this.options = {
|
||||
...optionsArg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* allows updating of server options
|
||||
* @param optionsArg
|
||||
*/
|
||||
public updateServerOptions(optionsArg: IServerOptions) {
|
||||
Object.assign(this.options, optionsArg);
|
||||
}
|
||||
|
||||
public addTypedRequest(typedrouter: plugins.typedrequest.TypedRouter) {
|
||||
this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is deprecated. Use TypedServer with SmartServe integration instead.
|
||||
* TypedSocket v4 no longer supports attaching to an existing Express server.
|
||||
*/
|
||||
public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void {
|
||||
console.warn(
|
||||
'[DEPRECATED] servertools.Server.addTypedSocket() is deprecated and has no effect. ' +
|
||||
'Use TypedServer with SmartServe integration for WebSocket support.'
|
||||
);
|
||||
// TypedSocket v4 creates its own server, which would conflict with Express.
|
||||
// This method is now a no-op for backward compatibility.
|
||||
}
|
||||
|
||||
public addRoute(routeStringArg: string, handlerArg?: Handler) {
|
||||
const route = new Route(this, routeStringArg);
|
||||
if (handlerArg) {
|
||||
route.addHandler(handlerArg);
|
||||
}
|
||||
this.routeObjectMap.push(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
public addRouteBefore(routeStringArg: string, handlerArg?: Handler) {
|
||||
const route = new Route(this, routeStringArg);
|
||||
if (handlerArg) {
|
||||
route.addHandler(handlerArg);
|
||||
}
|
||||
this.routeObjectMap.unshift(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the server and sets up the routes
|
||||
* @param portArg
|
||||
* @param doListen
|
||||
*/
|
||||
public async start(portArg: number | string = this.options.port, doListen = true) {
|
||||
const done = plugins.smartpromise.defer();
|
||||
|
||||
if (typeof portArg === 'string') {
|
||||
portArg = parseInt(portArg);
|
||||
}
|
||||
|
||||
this.expressAppInstance = plugins.express();
|
||||
if (!this.httpServer && (!this.options.privateKey || !this.options.publicKey)) {
|
||||
console.log('Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy');
|
||||
this.httpServer = plugins.http.createServer(this.expressAppInstance);
|
||||
} else if (!this.httpServer) {
|
||||
console.log('Got SSL certificate. Using it for the http server');
|
||||
this.httpServer = plugins.https.createServer(
|
||||
{
|
||||
key: this.options.privateKey,
|
||||
cert: this.options.publicKey,
|
||||
},
|
||||
this.expressAppInstance
|
||||
);
|
||||
} else {
|
||||
console.log('Using externally supplied http server');
|
||||
}
|
||||
this.httpServer.keepAliveTimeout = 600 * 1000;
|
||||
this.httpServer.headersTimeout = 20 * 1000;
|
||||
|
||||
// general request handlling
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// forceSsl
|
||||
if (this.options.forceSsl) {
|
||||
this.expressAppInstance.set('forceSSLOptions', {
|
||||
enable301Redirects: true,
|
||||
trustXFPHeader: true,
|
||||
sslRequiredMessage: 'SSL Required.',
|
||||
});
|
||||
this.expressAppInstance.use(plugins.expressForceSsl);
|
||||
}
|
||||
|
||||
// cors
|
||||
if (this.options.cors) {
|
||||
const cors = plugins.cors({
|
||||
allowedHeaders: '*',
|
||||
methods: '*',
|
||||
origin: '*',
|
||||
});
|
||||
|
||||
this.expressAppInstance.use(cors);
|
||||
this.expressAppInstance.options('/{*splat}', cors);
|
||||
}
|
||||
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('SERVEZONE_ROUTE', 'LOSSLESS_ORIGIN_CONTAINER');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Expires', new Date(Date.now()).toUTCString());
|
||||
next();
|
||||
});
|
||||
|
||||
// body parsing
|
||||
this.expressAppInstance.use(async (req, res, next) => {
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
let data = '';
|
||||
req.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
req.body = plugins.smartjson.parse(data);
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(400).send('Invalid JSON');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
this.expressAppInstance.use(plugins.bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
||||
|
||||
// robots
|
||||
if (this.options.robots && this.options.domain) {
|
||||
await setupRobots(this, this.options.domain);
|
||||
}
|
||||
|
||||
// manifest.json
|
||||
if (this.options.manifest) {
|
||||
await setupManifest(this.expressAppInstance, this.options.manifest);
|
||||
}
|
||||
|
||||
// sitemaps
|
||||
if (this.options.sitemap) {
|
||||
this.sitemap = new Sitemap(this);
|
||||
}
|
||||
|
||||
if (this.options.feed) {
|
||||
// feed
|
||||
this.feed = new Feed(this);
|
||||
}
|
||||
|
||||
// appVersion
|
||||
if (this.options.appVersion) {
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
res.set('appversion', this.options.appVersion);
|
||||
next();
|
||||
});
|
||||
this.addRoute(
|
||||
'/appversion',
|
||||
new Handler('GET', async (req, res) => {
|
||||
res.write(this.options.appVersion);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// set up routes in for express
|
||||
await this.routeObjectMap.forEach(async (routeArg) => {
|
||||
console.log(
|
||||
`"${routeArg.routeString}" maps to ${routeArg.handlerObjectMap.getArray().length} handlers`
|
||||
);
|
||||
const expressRoute = this.expressAppInstance.route(routeArg.routeString);
|
||||
routeArg.handlerObjectMap.forEach(async (handler) => {
|
||||
console.log(` -> ${handler.httpMethod}`);
|
||||
switch (handler.httpMethod) {
|
||||
case 'GET':
|
||||
expressRoute.get(handler.handlerFunction);
|
||||
return;
|
||||
case 'POST':
|
||||
expressRoute.post(handler.handlerFunction);
|
||||
return;
|
||||
case 'PUT':
|
||||
expressRoute.put(handler.handlerFunction);
|
||||
return;
|
||||
case 'ALL':
|
||||
expressRoute.all(handler.handlerFunction);
|
||||
return;
|
||||
case 'DELETE':
|
||||
expressRoute.delete(handler.handlerFunction);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (this.options.defaultAnswer) {
|
||||
this.expressAppInstance.get('/', async (request, response) => {
|
||||
response.send(await this.options.defaultAnswer());
|
||||
});
|
||||
}
|
||||
|
||||
this.httpServer.on('connection', (connection: plugins.net.Socket) => {
|
||||
this.socketMap.add(connection);
|
||||
console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`);
|
||||
|
||||
const closeListener = () => {
|
||||
console.log('connection closed');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const errorListener = () => {
|
||||
console.log('connection errored');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const endListener = () => {
|
||||
console.log('connection ended');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const timeoutListener = () => {
|
||||
console.log('connection timed out');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
connection.addListener('close', closeListener);
|
||||
connection.addListener('error', errorListener);
|
||||
connection.addListener('end', endListener);
|
||||
connection.addListener('timeout', timeoutListener);
|
||||
|
||||
const cleanupConnection = async () => {
|
||||
connection.removeListener('close', closeListener);
|
||||
connection.removeListener('error', errorListener);
|
||||
connection.removeListener('end', endListener);
|
||||
connection.removeListener('timeout', timeoutListener);
|
||||
|
||||
if (this.socketMap.checkForObject(connection)) {
|
||||
this.socketMap.remove(connection);
|
||||
console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
|
||||
await plugins.smartdelay.delayFor(0);
|
||||
if (connection.destroyed === false) {
|
||||
connection.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// finally listen on a port
|
||||
if (doListen) {
|
||||
this.httpServer.listen(portArg, '0.0.0.0', () => {
|
||||
console.log(`now listening on ${portArg}!`);
|
||||
this.startedDeferred.resolve();
|
||||
this.serverStatus = 'running';
|
||||
done.resolve();
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
'The server does not listen on a network stack and instead expects to get handed requests by other mechanics'
|
||||
);
|
||||
}
|
||||
await done.promise;
|
||||
for (const executeAfterStartFunction of this.executeAfterStartFunctions) {
|
||||
await executeAfterStartFunction();
|
||||
}
|
||||
}
|
||||
|
||||
public getHttpServer() {
|
||||
return this.httpServer;
|
||||
}
|
||||
|
||||
public getExpressAppInstance() {
|
||||
return this.expressAppInstance;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
if (this.httpServer) {
|
||||
this.httpServer.close(async () => {
|
||||
this.serverStatus = 'stopped';
|
||||
done.resolve();
|
||||
});
|
||||
await this.socketMap.forEach(async (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
} else {
|
||||
throw new Error('There is no Server to be stopped. Have you started it?');
|
||||
}
|
||||
return await done.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* allows handling requests and responses that come from other
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async handleReqRes(req: plugins.express.Request, res: plugins.express.Response) {
|
||||
this.expressAppInstance(req, res);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { type IUrlInfo } from '@push.rocks/smartsitemap';
|
||||
|
||||
export class Sitemap {
|
||||
public smartexpressRef: Server;
|
||||
public smartSitemap = new plugins.smartsitemap.SmartSitemap();
|
||||
public urls: plugins.smartsitemap.IUrlInfo[] = [];
|
||||
|
||||
/**
|
||||
* handles the normal sitemap request
|
||||
*/
|
||||
public sitemapHandler = new Handler('GET', async (req, res) => {
|
||||
const sitemapXmlString = await this.smartSitemap.createSitemapFromUrlInfoArray(this.urls);
|
||||
res.type('.xml');
|
||||
res.write(sitemapXmlString);
|
||||
res.end();
|
||||
});
|
||||
|
||||
/**
|
||||
* handles the sitemap-news request
|
||||
*/
|
||||
public sitemapNewsHandler = new Handler('GET', async (req, res) => {
|
||||
if (!this.smartexpressRef.options.articleGetterFunction) {
|
||||
res.status(500);
|
||||
res.write('no article getter function defined.');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const sitemapNewsXml = await this.smartSitemap.createSitemapNewsFromArticleArray(
|
||||
await this.smartexpressRef.options.articleGetterFunction()
|
||||
);
|
||||
res.type('.xml');
|
||||
res.write(sitemapNewsXml);
|
||||
res.end();
|
||||
});
|
||||
|
||||
constructor(smartexpressRefArg: Server) {
|
||||
this.smartexpressRef = smartexpressRefArg;
|
||||
this.smartexpressRef.addRouteBefore('/sitemap', this.sitemapHandler);
|
||||
this.smartexpressRef.addRouteBefore('/sitemap-news', this.sitemapNewsHandler);
|
||||
|
||||
// lets set the default url
|
||||
if (this.smartexpressRef.options.domain) {
|
||||
this.urls.push({
|
||||
url: `https://${this.smartexpressRef.options.domain}/`,
|
||||
timestamp: Date.now(),
|
||||
frequency: 'daily',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* replaces the current urlsArray
|
||||
* @param urlsArg
|
||||
*/
|
||||
public replaceUrls(urlsArg: IUrlInfo[]) {
|
||||
this.urls = urlsArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds urls to the current set of urls
|
||||
*/
|
||||
public addUrls(urlsArg: IUrlInfo[]) {
|
||||
this.urls = this.urls.concat(urlsArg);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Core utilities that don't depend on Express
|
||||
export * from './classes.compressor.js';
|
||||
|
||||
// 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 };
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export const setupManifest = async (
|
||||
expressInstanceArg: plugins.express.Application,
|
||||
manifestArg: plugins.smartmanifest.ISmartManifestConstructorOptions
|
||||
) => {
|
||||
const smartmanifestInstance = new plugins.smartmanifest.SmartManifest(manifestArg);
|
||||
expressInstanceArg.get('/manifest.json', async (req, res) => {
|
||||
res.status(200);
|
||||
res.type('json');
|
||||
res.write(smartmanifestInstance.jsonString());
|
||||
res.end();
|
||||
});
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
export const setupRobots = async (smartexpressRefArg: Server, domainArg: string) => {
|
||||
smartexpressRefArg.addRouteBefore(
|
||||
'/robots.txt',
|
||||
new Handler('GET', async (req, res) => {
|
||||
res.type('text/plain');
|
||||
res.send(`
|
||||
User-agent: Googlebot-News
|
||||
Disallow: /account
|
||||
Disallow: /login
|
||||
|
||||
User-agent: *
|
||||
Disallow: /account
|
||||
Disallow: /login
|
||||
|
||||
${
|
||||
smartexpressRefArg.options.blockWaybackMachine
|
||||
? `
|
||||
User-Agent: ia_archiver
|
||||
Disallow: /
|
||||
`
|
||||
: ``
|
||||
}
|
||||
|
||||
Sitemap: https://${domainArg}/sitemap
|
||||
Sitemap: https://${domainArg}/sitemap-news
|
||||
`);
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -1,134 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import type { TypedServer } from '../classes.typedserver.js';
|
||||
|
||||
// Lazy-loaded service worker bundle content
|
||||
let swBundleJs: string | null = null;
|
||||
let swBundleJsMap: string | null = null;
|
||||
|
||||
const loadServiceWorkerBundle = async (): Promise<void> => {
|
||||
if (swBundleJs === null) {
|
||||
swBundleJs = (await plugins.fsInstance
|
||||
.file(plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js'))
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
}
|
||||
if (swBundleJsMap === null) {
|
||||
swBundleJsMap = (await plugins.fsInstance
|
||||
.file(plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map'))
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
}
|
||||
};
|
||||
|
||||
let swVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
|
||||
null;
|
||||
|
||||
export const addServiceWorkerRoute = (
|
||||
typedserverInstance: TypedServer,
|
||||
swDataFunc: () => interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']
|
||||
) => {
|
||||
// Set the version info
|
||||
swVersionInfo = swDataFunc();
|
||||
|
||||
// Handler function for serviceworker bundle requests
|
||||
const handleServiceWorkerRequest = async (request: Request): Promise<Response> => {
|
||||
await loadServiceWorkerBundle();
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
|
||||
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' },
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Service worker bundle handler - nested path
|
||||
typedserverInstance.addRoute('/serviceworker/*splat', 'GET', handleServiceWorkerRequest);
|
||||
|
||||
// Service worker bundle handler - root level (for /serviceworker.bundle.js)
|
||||
typedserverInstance.addRoute('/serviceworker.bundle.js', 'GET', handleServiceWorkerRequest);
|
||||
typedserverInstance.addRoute('/serviceworker.bundle.js.map', 'GET', handleServiceWorkerRequest);
|
||||
|
||||
// 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();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Speedtest handler for measuring connection speed (time-based chunked approach)
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Speedtest>(
|
||||
'serviceworker_speedtest',
|
||||
async (reqArg) => {
|
||||
const chunkSizeKB = reqArg.chunkSizeKB || 64;
|
||||
const sizeBytes = chunkSizeKB * 1024;
|
||||
let payload: string | undefined;
|
||||
let bytesTransferred = 0;
|
||||
|
||||
switch (reqArg.type) {
|
||||
case 'download_chunk':
|
||||
// Generate chunk payload for download test
|
||||
payload = 'x'.repeat(sizeBytes);
|
||||
bytesTransferred = sizeBytes;
|
||||
break;
|
||||
case 'upload_chunk':
|
||||
// For upload, measure bytes received from client
|
||||
bytesTransferred = reqArg.payload?.length || 0;
|
||||
break;
|
||||
case 'latency':
|
||||
// Simple ping - no payload needed
|
||||
bytesTransferred = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
bytesTransferred,
|
||||
timestamp: Date.now(),
|
||||
payload, // Only for download_chunk tests
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
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' },
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
export const redirectFrom80To443 = async () => {
|
||||
const smartexpressInstance = new Server({
|
||||
cors: true,
|
||||
forceSsl: true,
|
||||
port: 80,
|
||||
});
|
||||
|
||||
smartexpressInstance.addRoute(
|
||||
'/{*splat}',
|
||||
new Handler('ALL', async (req, res) => {
|
||||
res.redirect('https://' + req.headers.host + req.url);
|
||||
})
|
||||
);
|
||||
|
||||
await smartexpressInstance.start();
|
||||
|
||||
return smartexpressInstance;
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { type IServerOptions, TypedServer } from '../classes.typedserver.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
|
||||
export interface IUtilityWebsiteServerConstructorOptions {
|
||||
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
|
||||
@@ -59,10 +58,12 @@ export class UtilityWebsiteServer {
|
||||
appSemVer: this.options.appSemVer || 'x.x.x',
|
||||
};
|
||||
|
||||
// -> /lsw* - anything regarding serviceworker
|
||||
servertools.serviceworker.addServiceWorkerRoute(this.typedserver, () => {
|
||||
return lswData;
|
||||
});
|
||||
// -> 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
|
||||
this.typedserver.addRoute('/ads.txt', 'GET', async () => {
|
||||
|
||||
Reference in New Issue
Block a user