BREAKING CHANGE(typedserver): migrate route handlers to use IRequestContext and lazy body parsers
This commit is contained in:
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-20 - 8.0.0 - BREAKING CHANGE(typedserver)
|
||||
migrate route handlers to use IRequestContext and lazy body parsers
|
||||
|
||||
- Route handlers now receive plugins.smartserve.IRequestContext instead of Request (breaking API change). addRoute no longer wraps handlers to convert context → Request.
|
||||
- createContext() is now synchronous and provides lazy body accessors: ctx.json(), ctx.text(), ctx.arrayBuffer(), ctx.formData(); ctx.body property was removed.
|
||||
- DevToolsController constructor now accepts optional options and supplies no-op defaults so controllers can be auto-instantiated without args.
|
||||
- TypedRequest controller now reads the request via await ctx.json() and forwards typed requests accordingly.
|
||||
- Utility website server handlers and other internal callsites updated to use ctx.params and the new context API.
|
||||
- Tests updated to the new TypedServer API, improved assertions, changed test port and reduced delays, and switched tap runner export to default.
|
||||
- Bumped dependency @push.rocks/smartserve to ^2.0.1 to match API changes.
|
||||
- npmextra.json reorganized git.zone/tsdoc entries and added release registries and @ship.zone/szci metadata.
|
||||
|
||||
## 2025-12-08 - 7.11.1 - fix(dependencies)
|
||||
Upgrade dependencies: bump @design.estate/dees-catalog to v3.1.1 and @push.rocks/smartwatch to v6.0.0; update migration notes in readme.hints.md
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"npmci": {
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
@@ -27,9 +24,17 @@
|
||||
"robots.txt",
|
||||
"compression (gzip, deflate, brotli)"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"tsdoc": {
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
},
|
||||
"@ship.zone/szci": {}
|
||||
}
|
||||
@@ -83,7 +83,7 @@
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartserve": "^1.3.0",
|
||||
"@push.rocks/smartserve": "^2.0.1",
|
||||
"@push.rocks/smartsitemap": "^2.0.4",
|
||||
"@push.rocks/smartstream": "^3.2.5",
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
|
||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@@ -16,7 +16,7 @@ importers:
|
||||
version: 3.0.19
|
||||
'@api.global/typedsocket':
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(@push.rocks/smartserve@1.3.0)
|
||||
version: 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20251205.0
|
||||
version: 4.20251205.0
|
||||
@@ -84,8 +84,8 @@ importers:
|
||||
specifier: ^3.0.10
|
||||
version: 3.0.10
|
||||
'@push.rocks/smartserve':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@push.rocks/smartsitemap':
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
@@ -125,7 +125,7 @@ importers:
|
||||
version: 2.0.0
|
||||
'@git.zone/tstest':
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3(@aws-sdk/credential-providers@3.787.0)(@push.rocks/smartserve@1.3.0)(socks@2.8.7)(typescript@5.9.3)
|
||||
version: 3.1.3(@aws-sdk/credential-providers@3.787.0)(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
@@ -543,6 +543,9 @@ packages:
|
||||
'@borewit/text-codec@0.1.1':
|
||||
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
|
||||
|
||||
'@cfworker/json-schema@4.1.1':
|
||||
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
|
||||
|
||||
'@cloudflare/workers-types@4.20251205.0':
|
||||
resolution: {integrity: sha512-7pup7fYkuQW5XD8RUS/vkxF9SXlrGyCXuZ4ro3uVQvca/GTeSa+8bZ8T4wbq1Aea5lmLIGSlKbhl2msME7bRBA==}
|
||||
|
||||
@@ -1297,8 +1300,8 @@ packages:
|
||||
'@push.rocks/smarts3@3.0.3':
|
||||
resolution: {integrity: sha512-Y9nXMwurthJ9Z7yi0RwjhPFUC58aY8Mhia8kFo6Xj1tBM4LE8Oxg/ydejF7otHqQGr3QyqV5C4YrDEG17rUuzg==}
|
||||
|
||||
'@push.rocks/smartserve@1.3.0':
|
||||
resolution: {integrity: sha512-4ZR9uKVWXVAPzU5wtCQ1mA9jNmOlUl3oGr50EceLT6803UwbNcst7Ek/BhzSaZ0qb2pz0jO5T/V+icgvZ1/5ww==}
|
||||
'@push.rocks/smartserve@2.0.1':
|
||||
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
|
||||
|
||||
'@push.rocks/smartshell@3.3.0':
|
||||
resolution: {integrity: sha512-m0w618H6YBs+vXGz1CgS4nPi5CUAnqRtckcS9/koGwfcIx1IpjqmiP47BoCTbdgcv0IPUxQVBG1IXTHPuZ8Z5g==}
|
||||
@@ -4298,11 +4301,11 @@ snapshots:
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
'@push.rocks/webstream': 1.0.10
|
||||
|
||||
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@1.3.0)':
|
||||
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@2.0.1)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.5
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@1.3.0)
|
||||
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20251205.0
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.2.2
|
||||
@@ -4346,7 +4349,7 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@1.3.0)':
|
||||
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.5
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
@@ -4357,7 +4360,7 @@ snapshots:
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
optionalDependencies:
|
||||
'@push.rocks/smartserve': 1.3.0
|
||||
'@push.rocks/smartserve': 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- bufferutil
|
||||
@@ -4366,7 +4369,7 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@1.3.0)':
|
||||
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@2.0.1)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.5
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
@@ -4375,7 +4378,7 @@ snapshots:
|
||||
'@push.rocks/smartjson': 5.2.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartserve': 1.3.0
|
||||
'@push.rocks/smartserve': 2.0.1
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
|
||||
@@ -5639,6 +5642,8 @@ snapshots:
|
||||
|
||||
'@borewit/text-codec@0.1.1': {}
|
||||
|
||||
'@cfworker/json-schema@4.1.1': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20251205.0': {}
|
||||
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
@@ -6026,9 +6031,9 @@ snapshots:
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
tsx: 4.20.6
|
||||
|
||||
'@git.zone/tstest@3.1.3(@aws-sdk/credential-providers@3.787.0)(@push.rocks/smartserve@1.3.0)(socks@2.8.7)(typescript@5.9.3)':
|
||||
'@git.zone/tstest@3.1.3(@aws-sdk/credential-providers@3.787.0)(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.3.0)
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@git.zone/tsbundle': 2.6.3
|
||||
'@git.zone/tsrun': 2.0.0
|
||||
'@push.rocks/consolecolor': 2.0.3
|
||||
@@ -6874,9 +6879,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@push.rocks/smartserve@1.3.0':
|
||||
'@push.rocks/smartserve@2.0.1':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.5
|
||||
'@cfworker/json-schema': 4.1.1
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartenv': 6.0.0
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
@@ -6907,7 +6913,7 @@ snapshots:
|
||||
'@push.rocks/smartsocket@2.1.0':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.3.0)
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@push.rocks/isohash': 2.0.1
|
||||
'@push.rocks/isounique': 1.0.5
|
||||
'@push.rocks/lik': 6.2.2
|
||||
|
||||
@@ -119,7 +119,7 @@ class UserController {
|
||||
|
||||
@smartserve.Post('/')
|
||||
async createUser(ctx: smartserve.IRequestContext): Promise<Response> {
|
||||
const userData = ctx.body;
|
||||
const userData = await ctx.json();
|
||||
const newUser = await createUserInDb(userData);
|
||||
return new Response(JSON.stringify(newUser), {
|
||||
status: 201,
|
||||
|
||||
@@ -7,7 +7,7 @@ let testTypedServer: TypedServer;
|
||||
tap.test('should create a valid instance of TypedServer', async () => {
|
||||
testTypedServer = new TypedServer({
|
||||
injectReload: true,
|
||||
port: 3000,
|
||||
port: 3001,
|
||||
serveDir: smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
watch: true,
|
||||
cors: true,
|
||||
@@ -17,15 +17,15 @@ tap.test('should create a valid instance of TypedServer', async () => {
|
||||
|
||||
tap.test('should start to serve files', async (tools) => {
|
||||
await testTypedServer.start();
|
||||
await tools.delayFor(5000);
|
||||
await tools.delayFor(1000);
|
||||
await testTypedServer.reload();
|
||||
await tools.delayFor(5000);
|
||||
await tools.delayFor(1000);
|
||||
await testTypedServer.reload();
|
||||
});
|
||||
|
||||
tap.test('should stop to serve files ', async (tools) => {
|
||||
await tools.delayFor(5000);
|
||||
tap.test('should stop to serve files', async (tools) => {
|
||||
await tools.delayFor(1000);
|
||||
await testTypedServer.stop();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
export default tap.start();
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
// helper dependencies
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
|
||||
import * as typedserver from '../ts/index.js';
|
||||
|
||||
let testServer: typedserver.servertools.Server;
|
||||
let testRoute: typedserver.servertools.Route;
|
||||
let testRoute2: typedserver.servertools.Route;
|
||||
let testHandler: typedserver.servertools.Handler;
|
||||
let testServer: typedserver.TypedServer;
|
||||
|
||||
// =================
|
||||
// Test class Server
|
||||
// Test TypedServer
|
||||
// =================
|
||||
|
||||
tap.test('should create a valid Server', async () => {
|
||||
testServer = new typedserver.servertools.Server({
|
||||
tap.test('should create a valid TypedServer', async () => {
|
||||
testServer = new typedserver.TypedServer({
|
||||
cors: true,
|
||||
domain: 'testing.git.zone',
|
||||
forceSsl: false,
|
||||
port: 3000,
|
||||
appVersion: 'v3.2.1',
|
||||
manifest: {
|
||||
name: 'Test App',
|
||||
@@ -38,101 +31,137 @@ tap.test('should create a valid Server', async () => {
|
||||
feed: true,
|
||||
sitemap: true,
|
||||
robots: true,
|
||||
serveDir: smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
});
|
||||
expect(testServer).toBeInstanceOf(typedserver.servertools.Server);
|
||||
expect(testServer).toBeInstanceOf(typedserver.TypedServer);
|
||||
});
|
||||
|
||||
// ================
|
||||
// Test class Route
|
||||
// Test addRoute
|
||||
// ================
|
||||
|
||||
tap.test('should create a valid Route', async () => {
|
||||
testRoute = testServer.addRoute('/someroute');
|
||||
testRoute2 = testServer.addRoute('/someroute/*splat');
|
||||
expect(testRoute).toBeInstanceOf(typedserver.servertools.Route);
|
||||
});
|
||||
|
||||
// ==================
|
||||
// Test class Handler
|
||||
// ==================
|
||||
|
||||
tap.test('should produce a valid handler', async () => {
|
||||
testHandler = new typedserver.servertools.Handler('POST', (request, response) => {
|
||||
tap.test('should add a POST route', async () => {
|
||||
testServer.addRoute('/someroute', 'POST', async (ctx) => {
|
||||
const body = await ctx.json();
|
||||
console.log('request body is:');
|
||||
console.log(request.body);
|
||||
response.send('hi');
|
||||
console.log(body);
|
||||
return new Response(JSON.stringify({ message: 'hi', received: body }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
});
|
||||
expect(testHandler).toBeInstanceOf(typedserver.servertools.Handler);
|
||||
});
|
||||
|
||||
tap.test('should add handler to route', async () => {
|
||||
testRoute.addHandler(testHandler);
|
||||
});
|
||||
|
||||
tap.test('should create a valid StaticHandler', async () => {
|
||||
testRoute2.addHandler(
|
||||
new typedserver.servertools.HandlerStatic(
|
||||
smartpath.get.dirnameFromImportMetaUrl(import.meta.url)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
tap.test('should add typedrequest and typedsocket', async () => {
|
||||
const typedrequest = await import('@api.global/typedrequest');
|
||||
|
||||
const typedrouter = new typedrequest.TypedRouter();
|
||||
testServer.addTypedRequest(typedrouter);
|
||||
testServer.addTypedSocket(typedrouter);
|
||||
tap.test('should add a GET route with params', async () => {
|
||||
testServer.addRoute('/users/:id', 'GET', async (ctx) => {
|
||||
const userId = ctx.params.id;
|
||||
return new Response(JSON.stringify({ userId }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =====================
|
||||
// start the server and test the configuration
|
||||
// Test typedrouter integration
|
||||
// =====================
|
||||
|
||||
tap.test('should start the server allright', async () => {
|
||||
await testServer.start(3000);
|
||||
tap.test('should have a typedrouter', async () => {
|
||||
expect(testServer.typedrouter).toBeDefined();
|
||||
});
|
||||
|
||||
// see if a demo request holds up
|
||||
tap.test('should issue a request', async (tools) => {
|
||||
// =====================
|
||||
// Start the server and test
|
||||
// =====================
|
||||
|
||||
tap.test('should start the server', async () => {
|
||||
await testServer.start();
|
||||
});
|
||||
|
||||
// Test POST route
|
||||
tap.test('should handle a POST request', async () => {
|
||||
const smartRequestInstance = smartrequest.SmartRequest.create();
|
||||
const response = await smartRequestInstance
|
||||
.url('http://127.0.0.1:3000/someroute')
|
||||
.headers({
|
||||
'X-Forwarded-Proto': 'https',
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
.json({
|
||||
someprop: 'hi',
|
||||
someprop: 'hello world',
|
||||
})
|
||||
.post();
|
||||
const responseBody = await response.text();
|
||||
console.log(responseBody);
|
||||
const responseBody = await response.json();
|
||||
console.log('POST response:', responseBody);
|
||||
expect(responseBody.message).toEqual('hi');
|
||||
expect(responseBody.received.someprop).toEqual('hello world');
|
||||
});
|
||||
|
||||
tap.test('should get a file from disk', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/someroute/testresponse.js');
|
||||
console.log(response.status);
|
||||
console.log(response.headers);
|
||||
// Test GET route with params
|
||||
tap.test('should handle a GET request with params', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/users/123');
|
||||
const body = await response.json();
|
||||
console.log('GET response:', body);
|
||||
expect(body.userId).toEqual('123');
|
||||
});
|
||||
|
||||
// Test static file serving
|
||||
tap.test('should serve a static file', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/test.server.ts');
|
||||
console.log('Static file status:', response.status);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
// Test CORS preflight
|
||||
tap.test('should answer a preflight request', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/some/randompath/', {
|
||||
method: 'OPTIONS',
|
||||
});
|
||||
console.log(response.headers);
|
||||
console.log('Preflight headers:', Object.fromEntries(response.headers.entries()));
|
||||
// CORS should return appropriate headers
|
||||
expect(response.headers.get('access-control-allow-origin')).toBeDefined();
|
||||
});
|
||||
|
||||
// Test sitemap endpoint
|
||||
tap.test('should expose a sitemap', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/sitemap');
|
||||
console.log(await response.text());
|
||||
const text = await response.text();
|
||||
console.log('Sitemap:', text);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
// Test robots.txt endpoint
|
||||
tap.test('should expose robots.txt', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/robots.txt');
|
||||
const text = await response.text();
|
||||
console.log('Robots.txt:', text);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
// Test manifest endpoint
|
||||
tap.test('should expose manifest.json', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/manifest.json');
|
||||
const json = await response.json();
|
||||
console.log('Manifest:', json);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(json.name).toEqual('Test App');
|
||||
});
|
||||
|
||||
// Test appversion endpoint
|
||||
tap.test('should expose appversion', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/appversion');
|
||||
const text = await response.text();
|
||||
console.log('App version:', text);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(text).toEqual('v3.2.1');
|
||||
});
|
||||
|
||||
// ========
|
||||
// clean up
|
||||
// Clean up
|
||||
// ========
|
||||
|
||||
tap.test('should stop the server', async () => {
|
||||
await testServer.stop();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
export default tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '7.11.1',
|
||||
version: '8.0.0',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export interface IServerOptions {
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
|
||||
|
||||
export interface IRouteHandler {
|
||||
(request: Request): Promise<Response | null>;
|
||||
(ctx: plugins.smartserve.IRequestContext): Promise<Response | null>;
|
||||
}
|
||||
|
||||
export class TypedServer {
|
||||
@@ -202,19 +202,11 @@ export class TypedServer {
|
||||
* 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
|
||||
* @param handler - Async function that receives IRequestContext and returns Response or null
|
||||
*/
|
||||
public addRoute(path: string, method: THttpMethod, handler: IRouteHandler): void {
|
||||
// Delegate to smartserve's ControllerRegistry
|
||||
plugins.smartserve.ControllerRegistry.addRoute(path, method, async (ctx: plugins.smartserve.IRequestContext) => {
|
||||
// Convert context to Request for backwards compatibility
|
||||
const request = new Request(ctx.url.toString(), {
|
||||
method: ctx.method,
|
||||
headers: ctx.headers,
|
||||
});
|
||||
(request as any).params = ctx.params;
|
||||
return handler(request);
|
||||
});
|
||||
plugins.smartserve.ControllerRegistry.addRoute(path, method, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,6 +266,9 @@ export class TypedServer {
|
||||
});
|
||||
|
||||
// Register controllers with SmartServe's ControllerRegistry
|
||||
// Note: @Route decorators auto-register classes at import time.
|
||||
// Controllers with constructor args (like DevToolsController) use default no-op
|
||||
// constructors to handle auto-instantiation gracefully.
|
||||
if (this.options.injectReload) {
|
||||
plugins.smartserve.ControllerRegistry.registerInstance(this.devToolsController);
|
||||
}
|
||||
@@ -389,10 +384,10 @@ export class TypedServer {
|
||||
/**
|
||||
* Create an IRequestContext from a Request
|
||||
*/
|
||||
private async createContext(
|
||||
private createContext(
|
||||
request: Request,
|
||||
params: Record<string, string>
|
||||
): Promise<plugins.smartserve.IRequestContext> {
|
||||
): plugins.smartserve.IRequestContext {
|
||||
const url = new URL(request.url);
|
||||
const method = request.method.toUpperCase() as THttpMethod;
|
||||
|
||||
@@ -402,20 +397,14 @@ export class TypedServer {
|
||||
query[key] = value;
|
||||
});
|
||||
|
||||
// Parse body
|
||||
let body: unknown = undefined;
|
||||
const contentType = request.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
try {
|
||||
body = await request.clone().json();
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
}
|
||||
// Cached body parsers (lazy evaluation)
|
||||
let jsonCache: unknown;
|
||||
let textCache: string;
|
||||
let arrayBufferCache: ArrayBuffer;
|
||||
let formDataCache: FormData;
|
||||
|
||||
return {
|
||||
request,
|
||||
body,
|
||||
params,
|
||||
query,
|
||||
headers: request.headers,
|
||||
@@ -424,6 +413,30 @@ export class TypedServer {
|
||||
url,
|
||||
runtime: 'node' as const,
|
||||
state: {},
|
||||
async json<T = unknown>(): Promise<T> {
|
||||
if (jsonCache === undefined) {
|
||||
jsonCache = await request.clone().json();
|
||||
}
|
||||
return jsonCache as T;
|
||||
},
|
||||
async text(): Promise<string> {
|
||||
if (textCache === undefined) {
|
||||
textCache = await request.clone().text();
|
||||
}
|
||||
return textCache;
|
||||
},
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
if (arrayBufferCache === undefined) {
|
||||
arrayBufferCache = await request.clone().arrayBuffer();
|
||||
}
|
||||
return arrayBufferCache;
|
||||
},
|
||||
async formData(): Promise<FormData> {
|
||||
if (formDataCache === undefined) {
|
||||
formDataCache = await request.clone().formData();
|
||||
}
|
||||
return formDataCache;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -577,7 +590,7 @@ export class TypedServer {
|
||||
}
|
||||
|
||||
// Process the request and wrap response with all configured headers
|
||||
const response = await this.handleRequestInternal(request, url, path, method);
|
||||
const response = await this.handleRequestInternal(request, path, method);
|
||||
return this.applyResponseHeaders(response);
|
||||
}
|
||||
|
||||
@@ -586,7 +599,6 @@ export class TypedServer {
|
||||
*/
|
||||
private async handleRequestInternal(
|
||||
request: Request,
|
||||
url: URL,
|
||||
path: string,
|
||||
method: THttpMethod
|
||||
): Promise<Response> {
|
||||
@@ -594,7 +606,7 @@ export class TypedServer {
|
||||
const match = plugins.smartserve.ControllerRegistry.matchRoute(path, method);
|
||||
if (match) {
|
||||
try {
|
||||
const context = await this.createContext(request, match.params);
|
||||
const context = this.createContext(request, match.params);
|
||||
const result = await match.route.handler(context);
|
||||
|
||||
// Handle Response or convert to Response
|
||||
|
||||
@@ -10,9 +10,10 @@ export class DevToolsController {
|
||||
private getLastReload: () => number;
|
||||
private getEnded: () => boolean;
|
||||
|
||||
constructor(options: { getLastReload: () => number; getEnded: () => boolean }) {
|
||||
this.getLastReload = options.getLastReload;
|
||||
this.getEnded = options.getEnded;
|
||||
constructor(options?: { getLastReload: () => number; getEnded: () => boolean }) {
|
||||
// Default no-op functions for when controller is auto-instantiated without options
|
||||
this.getLastReload = options?.getLastReload ?? (() => 0);
|
||||
this.getEnded = options?.getEnded ?? (() => false);
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/devtools')
|
||||
|
||||
@@ -14,7 +14,8 @@ export class TypedRequestController {
|
||||
@plugins.smartserve.Post('')
|
||||
async handleTypedRequest(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
try {
|
||||
const response = await this.typedRouter.routeAndAddResponse(ctx.body as plugins.typedrequestInterfaces.ITypedRequest);
|
||||
const body = await ctx.json() as plugins.typedrequestInterfaces.ITypedRequest;
|
||||
const response = await this.typedRouter.routeAndAddResponse(body);
|
||||
|
||||
return new Response(plugins.smartjson.stringify(response), {
|
||||
status: 200,
|
||||
|
||||
@@ -116,8 +116,8 @@ export class UtilityWebsiteServer {
|
||||
this.typedserver.addRoute(
|
||||
'/assetbroker/manifest/:manifestAsset',
|
||||
'GET',
|
||||
async (request: Request) => {
|
||||
let manifestAssetName = (request as any).params?.manifestAsset;
|
||||
async (ctx) => {
|
||||
let manifestAssetName = ctx.params?.manifestAsset;
|
||||
if (manifestAssetName === 'favicon.png') {
|
||||
manifestAssetName = `favicon_${this.options.domain
|
||||
.replace('.', '')
|
||||
|
||||
Reference in New Issue
Block a user