Compare commits

..

10 Commits

Author SHA1 Message Date
48995e6dfd v8.0.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-20 08:11:04 +00:00
64f8f400c2 BREAKING CHANGE(typedserver): migrate route handlers to use IRequestContext and lazy body parsers 2025-12-20 08:11:04 +00:00
d5800f58b4 v7.11.1
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 17:00:16 +00:00
49949b6776 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 2025-12-08 17:00:16 +00:00
623e40c5b7 v7.11.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 12:19:01 +00:00
94532c3c68 feat(typedserver): Add configurable response compression (Brotli + Gzip) with defaults enabled and documentation 2025-12-08 12:19:01 +00:00
e8e4f81747 v7.10.2
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-05 21:30:36 +00:00
f8b4c355d5 fix(docs): Update README with routing examples and utility server config; bump @cloudflare/workers-types and @push.rocks/smartserve versions 2025-12-05 21:30:36 +00:00
980ccfe949 v7.10.1
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-05 15:46:33 +00:00
4a76c8f738 fix(typedserver): Use smartserve ControllerRegistry for custom routes and remove custom route parsing 2025-12-05 15:46:33 +00:00
14 changed files with 513 additions and 221 deletions

View File

@@ -1,5 +1,54 @@
# 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
- package.json: @design.estate/dees-catalog updated from ^2.0.3 to ^3.1.1 (includes new icons, components and DeesIcon unified icon property; legacy iconFA deprecated)
- package.json: @push.rocks/smartwatch updated from ^5.0.0 to ^6.0.0 (cross-runtime support, native fs.watch, API compatibility maintained: new Smartwatch class methods and events documented)
- readme.hints.md: added migration notes for smartwatch v6 and dees-catalog v3, plus other dependency update summaries
## 2025-12-08 - 7.11.0 - feat(typedserver)
Add configurable response compression (Brotli + Gzip) with defaults enabled and documentation
- Expose a new compression option on IServerOptions (plugins.smartserve.ICompressionConfig | boolean).
- Pass the compression setting through to SmartServe (smartServeOptions.compression = this.options.compression).
- Add compression option to UtilityWebsiteServer and forward it when creating SmartServe options.
- Update README: new Compression section with global config examples, per-route decorator usage, and options reference.
- Add a small readme.todo.md with service worker wake/reload TODO notes.
## 2025-12-05 - 7.10.2 - fix(docs)
Update README with routing examples and utility server config; bump @cloudflare/workers-types and @push.rocks/smartserve versions
- Bumped dependency @cloudflare/workers-types to ^4.20251205.0
- Bumped dependency @push.rocks/smartserve to ^1.3.0
- Expanded README: added decorator-based routing examples (Route/Get/Post) using smartserve
- Added programmatic routing examples (addRoute) and SPA/wildcard route samples
- Enhanced UtilityWebsiteServer and UtilityServiceServer docs: default port, ads.txt, feedMetadata, addCustomRoutes example and other config options
- Clarified security headers descriptions and configuration reference
- Updated Quick Start console message to show running port ("Server running on port 3000!")
- Documented EdgeWorker/domain routing caching example and noted service worker version update behavior
- Adjusted TypedSocket example tag to use 'allClients' in README
## 2025-12-05 - 7.10.1 - fix(typedserver)
Use smartserve ControllerRegistry for custom routes and remove custom route parsing
- addRoute now delegates to plugins.smartserve.ControllerRegistry instead of building its own regex-based matcher
- Backwards compatibility: incoming smartserve IRequestContext is converted to a Request and ctx.params is attached to request.params before invoking the handler
- Removed internal IRegisteredRoute, customRoutes storage, and parseRouteParams helper
- Request handling now uses ControllerRegistry.matchRoute and registered controllers are compiled via ControllerRegistry.compileRoutes()
## 2025-12-05 - 7.10.0 - feat(website-server)
Add configurable ads.txt support to website server

View File

@@ -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": {}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@api.global/typedserver",
"version": "7.10.0",
"version": "8.0.0",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {
@@ -61,8 +61,8 @@
"@api.global/typedrequest": "^3.2.5",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^4.1.0",
"@cloudflare/workers-types": "^4.20251202.0",
"@design.estate/dees-catalog": "^2.0.3",
"@cloudflare/workers-types": "^4.20251205.0",
"@design.estate/dees-catalog": "^3.1.1",
"@design.estate/dees-comms": "^1.0.30",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.5",
@@ -83,11 +83,11 @@
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartserve": "^1.1.2",
"@push.rocks/smartserve": "^2.0.1",
"@push.rocks/smartsitemap": "^2.0.4",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartwatch": "^5.0.0",
"@push.rocks/smartwatch": "^6.0.0",
"@push.rocks/taskbuffer": "^3.5.0",
"@push.rocks/webrequest": "^4.0.1",
"@push.rocks/webstore": "^2.0.20",

70
pnpm-lock.yaml generated
View File

@@ -16,13 +16,13 @@ importers:
version: 3.0.19
'@api.global/typedsocket':
specifier: ^4.1.0
version: 4.1.0(@push.rocks/smartserve@1.1.2)
version: 4.1.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types':
specifier: ^4.20251202.0
version: 4.20251202.0
specifier: ^4.20251205.0
version: 4.20251205.0
'@design.estate/dees-catalog':
specifier: ^2.0.3
version: 2.0.3(@tiptap/pm@2.27.1)
specifier: ^3.1.1
version: 3.1.1(@tiptap/pm@2.27.1)
'@design.estate/dees-comms':
specifier: ^1.0.30
version: 1.0.30
@@ -84,8 +84,8 @@ importers:
specifier: ^3.0.10
version: 3.0.10
'@push.rocks/smartserve':
specifier: ^1.1.2
version: 1.1.2
specifier: ^2.0.1
version: 2.0.1
'@push.rocks/smartsitemap':
specifier: ^2.0.4
version: 2.0.4
@@ -96,8 +96,8 @@ importers:
specifier: ^4.1.1
version: 4.1.1
'@push.rocks/smartwatch':
specifier: ^5.0.0
version: 5.0.0
specifier: ^6.0.0
version: 6.0.0
'@push.rocks/taskbuffer':
specifier: ^3.5.0
version: 3.5.0
@@ -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.1.2)(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,14 +543,17 @@ packages:
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@cloudflare/workers-types@4.20251202.0':
resolution: {integrity: sha512-Q7m1Ivu2fbKalOPm00KLpu6GfRaq4TlrPknqugvZgp/gDH96OYKINO4x7jvCIBvCz/aK9vVoOj8tlbSQBervVA==}
'@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==}
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@2.0.3':
resolution: {integrity: sha512-+LoCZd2sHlp+8WRTJkDqBg9Ui1tlLcg+K3mYGkqVn+P2JLUCWUM8+LsYxgWEpgGGA8C6n1I7d+y2a63RjXwZ+Q==}
'@design.estate/dees-catalog@3.1.1':
resolution: {integrity: sha512-w7GMX5L3uF+w1q0oCAA1489HabgwBubbdt48/3FTOhklOZkXZF0Dfbs/hu4LDTjv0IkNPFxWY+4AXUErD+wh2Q==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -1297,8 +1300,8 @@ packages:
'@push.rocks/smarts3@3.0.3':
resolution: {integrity: sha512-Y9nXMwurthJ9Z7yi0RwjhPFUC58aY8Mhia8kFo6Xj1tBM4LE8Oxg/ydejF7otHqQGr3QyqV5C4YrDEG17rUuzg==}
'@push.rocks/smartserve@1.1.2':
resolution: {integrity: sha512-NkJNgdDt/rfsd9AMheCxtFd5X+ubzffvxOxjb0Aw1A5JR3xmiWeRifqEV1oN7mMTGL9jyQVvIME6Yrdxr244dA==}
'@push.rocks/smartserve@2.0.1':
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
'@push.rocks/smartshell@3.3.0':
resolution: {integrity: sha512-m0w618H6YBs+vXGz1CgS4nPi5CUAnqRtckcS9/koGwfcIx1IpjqmiP47BoCTbdgcv0IPUxQVBG1IXTHPuZ8Z5g==}
@@ -1339,8 +1342,8 @@ packages:
'@push.rocks/smartversion@3.0.5':
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
'@push.rocks/smartwatch@5.0.0':
resolution: {integrity: sha512-uuWUlTo0l5LWOWoOuTMG7zzxpUNKBcyqoB+zyQ24NHTtSYNcaUJtaQzTO2gxMXr5sqiZDkohlThS0KvsBc3g7w==}
'@push.rocks/smartwatch@6.0.0':
resolution: {integrity: sha512-RVUpIP7rg+C3WFilFQ1W00oO93wA+ht9+Of2795rjFNc68skpSuQ93/1npGBliEHfFFLak2eO9vXBdngQM1N2A==}
engines: {node: '>=20.0.0'}
'@push.rocks/smartxml@2.0.0':
@@ -4298,12 +4301,12 @@ snapshots:
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstream': 1.0.10
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@1.1.2)':
'@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.1.2)
'@cloudflare/workers-types': 4.20251202.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
'@push.rocks/smartchok': 1.1.1
@@ -4346,7 +4349,7 @@ snapshots:
- utf-8-validate
- vue
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@1.1.2)':
'@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.1.2
'@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.1.2)':
'@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.1.2
'@push.rocks/smartserve': 2.0.1
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
@@ -5639,13 +5642,15 @@ snapshots:
'@borewit/text-codec@0.1.1': {}
'@cloudflare/workers-types@4.20251202.0': {}
'@cfworker/json-schema@4.1.1': {}
'@cloudflare/workers-types@4.20251205.0': {}
'@configvault.io/interfaces@1.0.17':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@2.0.3(@tiptap/pm@2.27.1)':
'@design.estate/dees-catalog@3.1.1(@tiptap/pm@2.27.1)':
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
@@ -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.1.2)(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.1.2)
'@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.1.2':
'@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.1.2)
'@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
@@ -7002,7 +7008,7 @@ snapshots:
'@types/semver': 7.7.1
semver: 7.7.3
'@push.rocks/smartwatch@5.0.0':
'@push.rocks/smartwatch@6.0.0':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0

View File

@@ -3,7 +3,9 @@
## Recent Changes (December 2025)
### Dependency Updates
- `@push.rocks/smartchok` replaced with `@push.rocks/smartwatch` (renamed package, same API)
- `@push.rocks/smartwatch` upgraded to v6.0.0 (cross-runtime, native fs.watch)
- `@design.estate/dees-catalog` upgraded to v3.1.1 (new icons, components)
- `@push.rocks/smartchok` replaced with `@push.rocks/smartwatch` (renamed package)
- `@push.rocks/smartfile` upgraded from v11 to v13 (major API change - `fs` module removed)
- `@push.rocks/smartfs` added for filesystem operations (v1.2.0+)
- `@push.rocks/smartenv` upgraded to v6.0.0
@@ -14,6 +16,26 @@
### Code Migration Notes
#### smartwatch v6.0.0
- Cross-runtime support: Node.js 20+, Deno, Bun
- Uses native `fs.watch({ recursive: true })` for performance
- Minimal dependencies (no chokidar, no FSEvents bindings)
- API unchanged: `new Smartwatch([patterns])`, `.start()`, `.stop()`, `.getObservableFor(event)`
- Events: `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `error`, `ready`
- Dynamic watching: `.add(patterns)`, `.remove(pattern)`
- Status property: `'idle' | 'starting' | 'watching'`
#### dees-catalog v3.0.0+ Migration
- **DeesIcon**: New unified `icon` property with library prefixes:
- FontAwesome: `icon="fa:check"` (prefix `fa:`)
- Lucide: `icon="lucide:menu"` (prefix `lucide:`)
- Legacy `iconFA` property deprecated but still supported
- **DeesToast**: New convenience methods and positioning:
- `DeesToast.info()`, `.success()`, `.warning()`, `.error()`
- Position options: `top-right`, `top-left`, `bottom-right`, `bottom-left`, `top-center`, `bottom-center`
- New components: DeesInputTags, DeesInputDatepicker, DeesStatsGrid, DeesPagination, DeesAppuiBase
- DeesAppuiAppbar: Hierarchical menus with keyboard navigation
#### smartfile v13 Migration
- Old: `plugins.smartfile.fs.toStringSync(path)` / `plugins.smartfile.fs.toBufferSync(path)`
- New: Use `plugins.fsInstance` (SmartFs instance with Node provider)
@@ -24,10 +46,6 @@
- Old: `plugins.smartfile.fs.fileTreeToHash(dir, pattern)`
- New: `await plugins.fsInstance.directory(dir).recursive().treeHash()`
#### smartwatch (formerly smartchok)
- Class renamed: `Smartchok``Smartwatch`
- API remains the same: `new Smartwatch([paths])`, `.start()`, `.stop()`, `.getObservableFor(event)`
#### webrequest v4
- Class renamed: `WebRequest``WebrequestClient`

238
readme.md
View File

@@ -1,6 +1,6 @@
# @api.global/typedserver
A TypeScript-first web server framework for building modern full-stack applications. Features static file serving, live reload, type-safe API integration, service worker support, and edge computing capabilities. Part of the `@api.global` ecosystem.
A powerful TypeScript-first web server framework for building modern full-stack applications. Features static file serving, live reload, type-safe API integration, decorator-based routing, service worker support, and edge computing capabilities. Part of the `@api.global` ecosystem.
## Issue Reporting and Security
@@ -9,13 +9,14 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## ✨ Features
- 🔒 **Type-Safe API** - Full TypeScript support with `@api.global/typedrequest` and `@api.global/typedsocket`
- 🛡️ **Security Headers** - Built-in CSP, HSTS, X-Frame-Options, and more
- 🎯 **Decorator Routing** - Clean, expressive routing with `@Route`, `@Get`, `@Post` decorators via smartserve
- 🛡️ **Security Headers** - Built-in CSP, HSTS, X-Frame-Options, and comprehensive security configuration
-**Live Reload** - Automatic browser refresh on file changes during development
- 🛠️ **Service Worker** - Advanced caching, offline support, and background sync
- ☁️ **Edge Workers** - Cloudflare Workers compatible edge computing with domain routing
- 📡 **WebSocket** - Real-time bidirectional communication via TypedSocket
- 🗺️ **SEO Tools** - Built-in sitemap, RSS feed, and robots.txt generation
- 🎯 **SPA Support** - Single-page application fallback routing (default in UtilityWebsiteServer)
- 🎯 **SPA Support** - Single-page application fallback routing
- 📱 **PWA Ready** - Web App Manifest generation for progressive web apps
## 📦 Installation
@@ -43,7 +44,7 @@ const server = new TypedServer({
});
await server.start();
console.log('Server running!');
console.log('Server running on port 3000!');
```
### Full Configuration
@@ -86,6 +87,85 @@ const server = new TypedServer({
await server.start();
```
## 🛣️ Routing
TypedServer uses a unified routing system powered by `@push.rocks/smartserve`. You can add routes using decorators or the programmatic API.
### Decorator-Based Routing
Create clean, expressive controllers using decorators:
```typescript
import * as smartserve from '@push.rocks/smartserve';
@smartserve.Route('/api/users')
class UserController {
@smartserve.Get('/')
async listUsers(ctx: smartserve.IRequestContext): Promise<Response> {
const users = await getUsersFromDb();
return new Response(JSON.stringify(users), {
headers: { 'Content-Type': 'application/json' },
});
}
@smartserve.Get('/:id')
async getUser(ctx: smartserve.IRequestContext): Promise<Response> {
const userId = ctx.params.id;
const user = await getUserById(userId);
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
}
@smartserve.Post('/')
async createUser(ctx: smartserve.IRequestContext): Promise<Response> {
const userData = await ctx.json();
const newUser = await createUserInDb(userData);
return new Response(JSON.stringify(newUser), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
}
}
// Register the controller
smartserve.ControllerRegistry.registerInstance(new UserController());
```
### Programmatic Routes with `addRoute()`
Add routes dynamically using the `addRoute()` API:
```typescript
import { TypedServer } from '@api.global/typedserver';
const server = new TypedServer({ serveDir: './public', cors: true });
// Simple route
server.addRoute('/api/health', 'GET', async (request) => {
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
});
// Route with parameters (Express-style :param syntax)
server.addRoute('/api/items/:id', 'GET', async (request) => {
const itemId = (request as any).params.id;
return new Response(JSON.stringify({ id: itemId }), {
headers: { 'Content-Type': 'application/json' },
});
});
// Wildcard routes
server.addRoute('/files/*path', 'GET', async (request) => {
const filePath = (request as any).params.path;
// Handle file serving logic
return new Response(`Requested: ${filePath}`);
});
await server.start();
```
## 🔌 Type-Safe API Integration
### Adding TypedRequest Handlers
@@ -139,20 +219,22 @@ server.typedrouter.addTypedHandler<IChatMessage>(
await server.start();
// Push messages to connected clients
const connections = await server.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
const connections = await server.typedsocket.findAllTargetConnectionsByTag('allClients');
for (const conn of connections) {
// Push to specific clients
// Push to specific clients via TypedSocket
}
```
## ☁️ Edge Worker (Cloudflare Workers)
Deploy your application to the edge with Cloudflare Workers:
```typescript
import { EdgeWorker, DomainRouter } from '@api.global/typedserver/edgeworker';
const worker = new EdgeWorker();
// Configure domain routing
// Configure domain routing with caching
worker.domainRouter.addDomainInstruction({
domainPattern: '*.example.com',
originUrl: 'https://origin.example.com',
@@ -160,10 +242,11 @@ worker.domainRouter.addDomainInstruction({
cacheConfig: { maxAge: 3600 },
});
// Pass-through to origin for API routes
worker.domainRouter.addDomainInstruction({
domainPattern: 'api.example.com',
originUrl: 'https://api-origin.example.com',
type: 'origin', // Pass through to origin
type: 'origin',
});
// Cloudflare Worker entry point
@@ -188,6 +271,7 @@ const swClient = await getServiceworkerClient({
// - Cache invalidation from server
// - Offline support
// - Background sync
// - Version updates
```
## 🛡️ Security Headers
@@ -250,13 +334,82 @@ await server.start();
| `Strict-Transport-Security` | `hstsMaxAge`, `hstsIncludeSubDomains`, `hstsPreload` | Forces HTTPS connections |
| `X-Frame-Options` | `xFrameOptions` | Prevents clickjacking attacks |
| `X-Content-Type-Options` | `xContentTypeOptions` | Prevents MIME-sniffing |
| `X-XSS-Protection` | `xXssProtection` | Legacy XSS filter (still useful) |
| `X-XSS-Protection` | `xXssProtection` | Legacy XSS filter |
| `Referrer-Policy` | `referrerPolicy` | Controls referrer information |
| `Permissions-Policy` | `permissionsPolicy` | Controls browser features |
| `Cross-Origin-Opener-Policy` | `crossOriginOpenerPolicy` | Isolates browsing context |
| `Cross-Origin-Embedder-Policy` | `crossOriginEmbedderPolicy` | Controls cross-origin embedding |
| `Cross-Origin-Resource-Policy` | `crossOriginResourcePolicy` | Controls cross-origin resource sharing |
## 🗜️ Compression
TypedServer supports automatic response compression using Brotli and Gzip. Compression is powered by smartserve and enabled by default.
### Global Configuration
```typescript
import { TypedServer } from '@api.global/typedserver';
const server = new TypedServer({
serveDir: './dist',
cors: true,
// Enable with defaults (brotli + gzip, threshold: 1024 bytes)
compression: true,
// Or disable completely
compression: false,
// Or configure in detail
compression: {
enabled: true,
algorithms: ['br', 'gzip'], // Preferred order
threshold: 1024, // Min size to compress (bytes)
level: 4, // Compression level (1-11 for brotli, 1-9 for gzip)
exclude: ['/api/stream/*'], // Skip these paths
},
});
```
### Per-Route Control with Decorators
Use `@Compress` and `@NoCompress` decorators for fine-grained control:
```typescript
import * as smartserve from '@push.rocks/smartserve';
@smartserve.Route('/api')
class ApiController {
// Force maximum compression for this endpoint
@smartserve.Get('/large-data')
@smartserve.Compress({ level: 11 })
async getLargeData(ctx: smartserve.IRequestContext): Promise<Response> {
return new Response(JSON.stringify(largeDataset));
}
// Disable compression for streaming endpoint
@smartserve.Get('/events')
@smartserve.NoCompress()
async streamEvents(ctx: smartserve.IRequestContext): Promise<Response> {
// Server-Sent Events shouldn't be compressed
return new Response(eventStream, {
headers: { 'Content-Type': 'text/event-stream' },
});
}
}
```
### Compression Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enabled` | `boolean` | `true` | Enable/disable compression |
| `algorithms` | `string[]` | `['br', 'gzip']` | Preferred algorithms in order |
| `threshold` | `number` | `1024` | Minimum response size (bytes) to compress |
| `level` | `number` | `4` | Compression level (1-11 for brotli, 1-9 for gzip) |
| `compressibleTypes` | `string[]` | auto | MIME types to compress |
| `exclude` | `string[]` | `[]` | Path patterns to skip |
## 📋 Configuration Reference
### IServerOptions
@@ -281,7 +434,8 @@ await server.start();
| `defaultAnswer` | `function` | - | Custom default response handler |
| `feedMetadata` | `object` | - | RSS feed metadata options |
| `blockWaybackMachine` | `boolean` | `false` | Block Wayback Machine archiving |
| `securityHeaders` | `ISecurityHeaders` | - | Security headers configuration (CSP, HSTS, etc.) |
| `securityHeaders` | `ISecurityHeaders` | - | Security headers configuration |
| `compression` | `ICompressionConfig \| boolean` | `true` | Response compression configuration |
## 🏗️ Package Exports
@@ -297,7 +451,7 @@ await server.start();
## 🔄 Utility Servers
Pre-configured server templates with best practices built-in:
Pre-configured server templates with best practices built-in.
### UtilityWebsiteServer
@@ -310,10 +464,10 @@ const websiteServer = new utilityservers.UtilityWebsiteServer({
serveDir: './dist',
domain: 'example.com',
// SPA fallback enabled by default (serves index.html for client routes)
// SPA fallback enabled by default
spaFallback: true, // default: true
// Optional security headers
// Security headers
securityHeaders: {
csp: {
defaultSrc: ["'self'"],
@@ -324,18 +478,41 @@ const websiteServer = new utilityservers.UtilityWebsiteServer({
xContentTypeOptions: true,
},
// Compression (enabled by default)
compression: true, // or { level: 6, threshold: 512 }
// Other options
cors: true, // default: true
forceSsl: false, // default: false
appSemVer: '1.0.0',
port: 3000, // default: 3000
// Optional ads.txt entries (only served if configured)
adsTxt: [
'google.com, pub-1234567890, DIRECT, f08c47fec0942fa0',
],
// RSS feed metadata
feedMetadata: {
title: 'My Blog',
description: 'A cool blog',
link: 'https://example.com',
},
// Add custom routes
addCustomRoutes: async (typedserver) => {
typedserver.addRoute('/api/custom', 'GET', async () => {
return new Response('Custom route!');
});
},
});
await websiteServer.start(); // Default port 3000
await websiteServer.start();
```
### UtilityServiceServer
Optimized for API services:
Optimized for API services with auto-generated info page:
```typescript
import { utilityservers } from '@api.global/typedserver';
@@ -345,11 +522,42 @@ const serviceServer = new utilityservers.UtilityServiceServer({
serviceVersion: '1.0.0',
serviceDomain: 'api.example.com',
port: 8080,
// Add custom routes
addCustomRoutes: async (typedserver) => {
typedserver.addRoute('/api/status', 'GET', async () => {
return new Response(JSON.stringify({ status: 'healthy' }), {
headers: { 'Content-Type': 'application/json' },
});
});
},
});
await serviceServer.start();
```
## 🧩 Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ TypedServer │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ SmartServe │ │ TypedRouter │ │ TypedSocket │ │
│ │ (Routing) │ │ (RPC) │ │ (WebSocket) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Request Handler Pipeline │ │
│ │ 1. Controller Registry (Decorated Routes) │ │
│ │ 2. TypedRequest/TypedSocket handlers │ │
│ │ 3. Static File Serving │ │
│ │ 4. SPA Fallback │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.

5
readme.todo.md Normal file
View File

@@ -0,0 +1,5 @@
- Wake up the service worker before sending stuff.
Handle reload properly. Make sure service worker is up.
Pill handling of service worker status.

View File

@@ -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();

View File

@@ -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();

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '7.10.0',
version: '8.0.0',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@@ -137,20 +137,18 @@ export interface IServerOptions {
* Security headers configuration (CSP, HSTS, X-Frame-Options, etc.)
*/
securityHeaders?: ISecurityHeaders;
/**
* Response compression configuration
* Set to true for defaults (brotli + gzip), false to disable, or provide detailed config
*/
compression?: plugins.smartserve.ICompressionConfig | boolean;
}
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
export interface IRouteHandler {
(request: Request): Promise<Response | null>;
}
export interface IRegisteredRoute {
pattern: string;
regex: RegExp;
paramNames: string[];
method: THttpMethod;
handler: IRouteHandler;
(ctx: plugins.smartserve.IRequestContext): Promise<Response | null>;
}
export class TypedServer {
@@ -175,9 +173,6 @@ export class TypedServer {
// File server for static files
private fileServer: plugins.smartserve.FileServer;
// Custom route handlers (for addRoute API)
private customRoutes: IRegisteredRoute[] = [];
public lastReload: number = Date.now();
public ended = false;
@@ -207,50 +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 {
// Convert Express-style path to regex
const paramNames: string[] = [];
let regexPattern = path
// Handle named parameters :param
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
})
// Handle wildcard *splat (matches everything including slashes)
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '(.*)';
});
// Ensure exact match
regexPattern = `^${regexPattern}$`;
this.customRoutes.push({
pattern: path,
regex: new RegExp(regexPattern),
paramNames,
method,
handler,
});
}
/**
* Parse route parameters from a path using a registered route
*/
private parseRouteParams(
route: IRegisteredRoute,
pathname: string
): Record<string, string> | null {
const match = pathname.match(route.regex);
if (!match) return null;
const params: Record<string, string> = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
return params;
// Delegate to smartserve's ControllerRegistry
plugins.smartserve.ControllerRegistry.addRoute(path, method, handler);
}
/**
@@ -310,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);
}
@@ -323,6 +282,7 @@ export class TypedServer {
const smartServeOptions: plugins.smartserve.ISmartServeOptions = {
port,
hostname: '0.0.0.0',
compression: this.options.compression,
tls:
this.options.privateKey && this.options.publicKey
? {
@@ -424,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;
@@ -437,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,
@@ -459,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;
},
};
}
@@ -612,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);
}
@@ -621,7 +599,6 @@ export class TypedServer {
*/
private async handleRequestInternal(
request: Request,
url: URL,
path: string,
method: THttpMethod
): Promise<Response> {
@@ -629,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
@@ -650,18 +627,6 @@ export class TypedServer {
}
}
// Custom routes (registered via addRoute)
for (const route of this.customRoutes) {
if (route.method === 'ALL' || route.method === method) {
const params = this.parseRouteParams(route, path);
if (params !== null) {
(request as any).params = params;
const response = await route.handler(request);
if (response) return response;
}
}
}
// HTML injection for reload (if enabled)
if (this.options.injectReload && this.options.serveDir) {
const response = await this.handleHtmlWithInjection(request);

View File

@@ -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')

View File

@@ -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,

View File

@@ -25,6 +25,8 @@ export interface IUtilityWebsiteServerConstructorOptions {
port?: number;
/** ads.txt entries (only served if configured) */
adsTxt?: string[];
/** Response compression configuration (default: enabled with brotli + gzip) */
compression?: plugins.smartserve.ICompressionConfig | boolean;
}
/**
@@ -67,6 +69,9 @@ export class UtilityWebsiteServer {
forceSsl: this.options.forceSsl ?? false,
securityHeaders: this.options.securityHeaders,
// Compression
compression: this.options.compression,
// PWA manifest
manifest: {
name: this.options.domain,
@@ -111,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('.', '')