Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
21938e5f20 | |||
99427f5835 | |||
552a15bb2f | |||
b0efc48b96 | |||
8c3aad69a0 | |||
fb2692b50e | |||
65c868aefe | |||
11df25f028 | |||
efb4229f58 | |||
61dcc6badc | |||
585da9bc79 | |||
60a2efaecb | |||
2c8f550830 | |||
c9688159e5 | |||
a710473d33 | |||
61c62672fc | |||
1a7150e1f8 | |||
f35360adba | |||
9774567dc0 | |||
529c5feeb1 | |||
d2cac36a6e | |||
2cdef55f13 | |||
05444b757b | |||
1ef5c0da06 | |||
00b4108803 | |||
7e75cccbcb |
@ -105,6 +105,6 @@ jobs:
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @gitzone/tsdoc
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
||||
|
39
package.json
39
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@api.global/typedserver",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.16",
|
||||
"description": "easy serving of static files",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@ -37,15 +37,15 @@
|
||||
],
|
||||
"homepage": "https://github.com/pushrocks/easyserve",
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.0.1",
|
||||
"@api.global/typedrequest": "^3.0.4",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.1",
|
||||
"@api.global/typedsocket": "^3.0.0",
|
||||
"@push.rocks/lik": "^6.0.3",
|
||||
"@push.rocks/lik": "^6.0.12",
|
||||
"@push.rocks/smartchok": "^1.0.23",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartenv": "^5.0.5",
|
||||
"@push.rocks/smartenv": "^5.0.12",
|
||||
"@push.rocks/smartfeed": "^1.0.11",
|
||||
"@push.rocks/smartfile": "^10.0.28",
|
||||
"@push.rocks/smartfile": "^11.0.4",
|
||||
"@push.rocks/smartlog": "^3.0.3",
|
||||
"@push.rocks/smartlog-destination-devtools": "^1.0.10",
|
||||
"@push.rocks/smartmanifest": "^2.0.2",
|
||||
@ -53,26 +53,27 @@
|
||||
"@push.rocks/smartopen": "^2.0.0",
|
||||
"@push.rocks/smartpath": "^5.0.11",
|
||||
"@push.rocks/smartpromise": "^4.0.2",
|
||||
"@push.rocks/smartrequest": "^2.0.18",
|
||||
"@push.rocks/smartrx": "^3.0.6",
|
||||
"@push.rocks/smartsitemap": "^2.0.2",
|
||||
"@push.rocks/smarttime": "^4.0.4",
|
||||
"@push.rocks/webstore": "^2.0.9",
|
||||
"@tsclass/tsclass": "^4.0.42",
|
||||
"@types/express": "^4.17.17",
|
||||
"@push.rocks/smartrequest": "^2.0.21",
|
||||
"@push.rocks/smartrx": "^3.0.7",
|
||||
"@push.rocks/smartsitemap": "^2.0.3",
|
||||
"@push.rocks/smartstream": "^3.0.30",
|
||||
"@push.rocks/smarttime": "^4.0.6",
|
||||
"@push.rocks/webstore": "^2.0.13",
|
||||
"@tsclass/tsclass": "^4.0.46",
|
||||
"@types/express": "^4.17.21",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^2.8.0"
|
||||
"lit": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.66",
|
||||
"@gitzone/tsbundle": "^2.0.8",
|
||||
"@gitzone/tsrun": "^1.2.44",
|
||||
"@gitzone/tstest": "^1.0.77",
|
||||
"@push.rocks/tapbundle": "^5.0.12",
|
||||
"@types/node": "^20.4.8"
|
||||
"@git.zone/tsbuild": "^2.1.66",
|
||||
"@git.zone/tsbundle": "^2.0.8",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.84",
|
||||
"@push.rocks/tapbundle": "^5.0.15",
|
||||
"@types/node": "^20.10.7"
|
||||
},
|
||||
"private": false,
|
||||
"browserslist": [
|
||||
|
3028
pnpm-lock.yaml
generated
3028
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -78,7 +78,7 @@ tap.test('should create a valid StaticHandler', async () => {
|
||||
});
|
||||
|
||||
tap.test('should add typedrequest and typedsocket', async () => {
|
||||
const typedrequest = await import('@apiglobal/typedrequest');
|
||||
const typedrequest = await import('@api.global/typedrequest');
|
||||
|
||||
const typedrouter = new typedrequest.TypedRouter();
|
||||
testServer.addTypedRequest(typedrouter);
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.3',
|
||||
version: '3.0.16',
|
||||
description: 'easy serving of static files'
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
export type TResponseModifier = <T>(responseArg: {
|
||||
headers: { [header: string]: number | string | string[] | undefined };
|
||||
path: string;
|
||||
responseContent: string;
|
||||
responseContent: Buffer;
|
||||
travelData?: T;
|
||||
}) => Promise<{
|
||||
headers: { [header: string]: number | string | string[] | undefined };
|
||||
path: string;
|
||||
responseContent: string;
|
||||
responseContent: Buffer;
|
||||
travelData?: T;
|
||||
}>;
|
||||
|
127
ts/servertools/classes.compressor.ts
Normal file
127
ts/servertools/classes.compressor.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as plugins from '../typedserver.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();
|
||||
case 'br':
|
||||
compressionStream = plugins.zlib.createBrotliCompress({
|
||||
chunkSize: 16 * 1024,
|
||||
params: {
|
||||
|
||||
},
|
||||
});
|
||||
case 'deflate':
|
||||
compressionStream = plugins.zlib.createDeflate();
|
||||
default:
|
||||
compressionStream = plugins.smartstream.createPassThrough();
|
||||
}
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ export class HandlerProxy extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
let responseToSend: string = proxiedResponse.body;
|
||||
let responseToSend: Buffer = proxiedResponse.body;
|
||||
if (typeof responseToSend !== 'string') {
|
||||
console.log(proxyRequestUrl);
|
||||
console.log(responseToSend);
|
||||
|
@ -2,8 +2,10 @@ import * as plugins from '../typedserver.plugins.js';
|
||||
import * as interfaces from '../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?: {
|
||||
@ -11,6 +13,8 @@ export class HandlerStatic extends Handler {
|
||||
responseModifier?: interfaces.TResponseModifier;
|
||||
headers?: { [key: string]: string };
|
||||
serveIndexHtmlDefault?: boolean;
|
||||
enableCompression?: boolean;
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
}
|
||||
) {
|
||||
super('GET', async (req, res) => {
|
||||
@ -62,11 +66,9 @@ export class HandlerStatic extends Handler {
|
||||
}
|
||||
|
||||
// lets actually care about serving, if security checks pass
|
||||
let fileString: string;
|
||||
let fileEncoding: 'binary' | 'utf8';
|
||||
let fileBuffer: Buffer;
|
||||
try {
|
||||
fileString = plugins.smartfile.fs.toStringSync(joinedPath);
|
||||
fileEncoding = plugins.smartmime.getEncoding(joinedPath);
|
||||
fileBuffer = plugins.smartfile.fs.toBufferSync(joinedPath);
|
||||
usedPath = joinedPath;
|
||||
} catch (err) {
|
||||
// try serving index.html instead
|
||||
@ -75,8 +77,7 @@ export class HandlerStatic extends Handler {
|
||||
console.log(`serving default path ${defaultPath} instead of ${joinedPath}`);
|
||||
try {
|
||||
parsedPath = plugins.path.parse(defaultPath);
|
||||
fileString = plugins.smartfile.fs.toStringSync(defaultPath);
|
||||
fileEncoding = plugins.smartmime.getEncoding(defaultPath);
|
||||
fileBuffer = plugins.smartfile.fs.toBufferSync(defaultPath);
|
||||
usedPath = defaultPath;
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
@ -99,7 +100,7 @@ export class HandlerStatic extends Handler {
|
||||
const modifiedResponse = await optionsArg.responseModifier({
|
||||
headers: res.getHeaders(),
|
||||
path: usedPath,
|
||||
responseContent: fileString,
|
||||
responseContent: fileBuffer,
|
||||
travelData,
|
||||
});
|
||||
|
||||
@ -115,11 +116,21 @@ export class HandlerStatic extends Handler {
|
||||
}
|
||||
|
||||
// responseContent
|
||||
fileString = modifiedResponse.responseContent;
|
||||
fileBuffer = modifiedResponse.responseContent;
|
||||
}
|
||||
|
||||
// lets finally deal with compression
|
||||
let compressionResult: ICompressionResult;
|
||||
|
||||
if (optionsArg && optionsArg.enableCompression) {
|
||||
compressionResult = await this.compressor.maybeCompress(requestHeaders, fileBuffer);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.write(Buffer.from(fileString, fileEncoding));
|
||||
if (compressionResult?.compressionMethod) {
|
||||
res.header('Content-Encoding', compressionResult.compressionMethod);
|
||||
}
|
||||
res.write(compressionResult.result);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
@ -132,7 +132,8 @@ export class Server {
|
||||
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.setHeader('Cross-Origin-Embedder-Policy', 'unsafe-none');
|
||||
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');
|
||||
|
@ -2,6 +2,7 @@ import * as plugins from './typedserver.plugins.js';
|
||||
import * as paths from './typedserver.paths.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as servertools from './servertools/index.js';
|
||||
import { type TCompressionMethod } from './servertools/classes.compressor.js';
|
||||
|
||||
export interface IServerOptions {
|
||||
/**
|
||||
@ -14,6 +15,16 @@ export interface IServerOptions {
|
||||
*/
|
||||
injectReload?: boolean;
|
||||
|
||||
/**
|
||||
* enable compression
|
||||
*/
|
||||
enableCompression?: boolean;
|
||||
|
||||
/**
|
||||
* choose a preferred compression method
|
||||
*/
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
|
||||
/**
|
||||
* watch the serve directory?
|
||||
*/
|
||||
@ -124,7 +135,7 @@ export class TypedServer {
|
||||
'/*',
|
||||
new servertools.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
let fileString = responseArg.responseContent;
|
||||
let fileString = responseArg.responseContent.toString();
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
@ -153,10 +164,12 @@ export class TypedServer {
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: fileString,
|
||||
responseContent: Buffer.from(fileString),
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
enableCompression: this.options.enableCompression,
|
||||
preferredCompressionMethod: this.options.preferredCompressionMethod,
|
||||
})
|
||||
);
|
||||
} else if (this.options.injectReload) {
|
||||
|
@ -3,8 +3,10 @@ import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as stream from 'stream';
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
export { http, https, net, path };
|
||||
export { http, https, net, path, zlib };
|
||||
|
||||
// @tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
@ -32,6 +34,7 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartrx from '@push.rocks/smartrx';
|
||||
import * as smartsitemap from '@push.rocks/smartsitemap';
|
||||
import * as smartstream from '@push.rocks/smartstream';
|
||||
import * as smarttime from '@push.rocks/smarttime';
|
||||
|
||||
export {
|
||||
@ -47,6 +50,7 @@ export {
|
||||
smartpromise,
|
||||
smartrequest,
|
||||
smartsitemap,
|
||||
smartstream,
|
||||
smarttime,
|
||||
smartrx,
|
||||
};
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.3',
|
||||
version: '3.0.16',
|
||||
description: 'easy serving of static files'
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user