Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 424b742f84 | |||
| c25daba1c1 | |||
| dce2e926e4 | |||
| 27c96949a1 | |||
| c17d6dac35 | |||
| 1c0c88a7cb | |||
| 8557c769fa | |||
| bce84c6838 | |||
| ba08fcdebc | |||
| 588a9d1df6 | |||
| b1d376207a | |||
| 43d8aea4e1 | |||
| 776d6fb95d | |||
| 0f22c91499 | |||
| a0f714a561 | |||
| 9477eac268 | |||
| 8cbae75ae4 | |||
| db05fc91c4 | |||
| 6e647a3556 | |||
| 6da22ab607 | |||
| fb98b3294a |
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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"
|
||||
100
changelog.md
100
changelog.md
@@ -1,5 +1,105 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-02 - 4.1.1 - fix(classes.typedserver)
|
||||
Instantiate and register DevToolsController only when injectReload is enabled; compile ControllerRegistry routes after registration
|
||||
|
||||
- DevToolsController is now created and registered only if options.injectReload is true to avoid unnecessary/invalid registrations when live reload is disabled.
|
||||
- ControllerRegistry.compileRoutes() is invoked after registering controllers to precompile decorated routes for faster route matching.
|
||||
|
||||
## 2025-12-02 - 4.1.0 - feat(TypedServer)
|
||||
Integrate SmartServe controller routing; add built-in routes controller and refactor TypedServer to use controllers and FileServer
|
||||
|
||||
- Add BuiltInRoutesController exposing /robots.txt, /manifest.json, /sitemap, /sitemap-news, /feed and /appversion
|
||||
- Refactor TypedRequestHandler into a SmartServe-decorated TypedRequestController and register it with ControllerRegistry
|
||||
- Refactor TypedServer to use SmartServe: register controller instances, use ControllerRegistry matching, and delegate WebSocket integration to SmartServe
|
||||
- Introduce FileServer-based static serving with HTML reload script injection and improved default root handling
|
||||
- Expand supported HTTP methods to include HEAD and OPTIONS
|
||||
- Remove legacy FeedHelper and consolidate sitemap/feed handling into controllers and helpers
|
||||
- Enhance servertools legacy Express utilities: improved HandlerProxy, HandlerStatic, Compressor with caching and preferred compression support
|
||||
- Service worker subsystem improvements: CacheManager, NetworkManager, UpdateManager and backend enhancements for robust caching, revalidation and client reloads
|
||||
- Web-inject LitElement properties switched from private fields to accessor syntax (typedserver_web.infoscreen)
|
||||
|
||||
## 2025-12-02 - 4.0.0 - BREAKING CHANGE(typedserver)
|
||||
Migrate to new push.rocks packages and async smartfs API; replace smartchok with smartwatch; update deps and service worker handling
|
||||
|
||||
- Major dependency updates: @push.rocks packages upgraded (smartfile -> v13, smartfs added v1.2.0, smartenv v6, smartrequest v5, smartwatch v5, webrequest v4), Express and dev-tooling bumped.
|
||||
- Replace sync smartfile APIs with async SmartFs instance (plugins.fsInstance). All file reads now use async smartfs calls.
|
||||
- smartfile v13 migration: removed sync fs helpers; added plugins.fsInstance (SmartFs + Node provider).
|
||||
- Renamed file watcher usage: Smartchok -> Smartwatch and property names updated (smartwatchInstance).
|
||||
- WebRequest class rename handled: WebRequest -> WebrequestClient (webrequest v4).
|
||||
- Service worker bundle is lazy-loaded (avoid startup sync file reads) and added routes for bundle and source map.
|
||||
- Service worker & SW-related improvements: more robust caching, enhanced cache headers, offline handling, revalidation, and update forcing logic.
|
||||
- createServeDirHash now uses smartfs.directory().recursive().treeHash() and truncates hash to 12 chars; fallback hash logic retained.
|
||||
- HandlerStatic, HandlerProxy and route handling hardened for Express 5 wildcard params (array/various shapes supported).
|
||||
- Various runtime improvements: safer start/stop handling, improved error logging, and non-blocking initialization of optional features (file watcher, TypedSocket).
|
||||
- Updated tooling/dev deps: @git.zone/tsbuild/tsbundle/tstest and @types/node updated.
|
||||
|
||||
## 2025-11-19 - 3.0.80 - fix(dependencies)
|
||||
Bump dependencies and devDependencies to updated versions
|
||||
|
||||
- Upgrade @push.rocks/smartfeed: ^1.0.11 → ^1.4.0
|
||||
- Upgrade @push.rocks/smartfile: ^11.2.5 → ^11.2.7
|
||||
- Upgrade @push.rocks/smartjson: ^5.0.20 → ^5.2.0
|
||||
- Upgrade @push.rocks/smartlog: ^3.1.8 → ^3.1.10
|
||||
- Upgrade @push.rocks/smartsitemap: ^2.0.3 → ^2.0.4
|
||||
- Upgrade @push.rocks/taskbuffer: ^3.1.7 → ^3.4.0
|
||||
- Upgrade @tsclass/tsclass: ^9.2.0 → ^9.3.0
|
||||
- Upgrade @types/express: ^5.0.3 → ^5.0.5
|
||||
- Dev: Upgrade @git.zone/tsbuild: ^2.6.4 → ^3.1.0
|
||||
- Dev: Upgrade @git.zone/tsbundle: ^2.5.1 → ^2.5.2
|
||||
- Dev: Upgrade @git.zone/tsrun: ^1.3.3 → ^2.0.0
|
||||
- Dev: Upgrade @git.zone/tstest: ^2.3.4 → ^2.8.2
|
||||
|
||||
## 2025-09-03 - 3.0.79 - fix(servertools)
|
||||
Normalize Express wildcard parameter notation to /{*splat} across server routes and handlers; add local Claude settings
|
||||
|
||||
- Replaced route pattern '/*splat' with '/{*splat}' in ts/classes.typedserver.ts and ts/servertools/tools.sslredirect.ts
|
||||
- Updated Express options route to use '/{*splat}' in ts/servertools/classes.server.ts
|
||||
- Clarified wildcard handling comments and array-join logic for splat params in ts/servertools/classes.handlerproxy.ts and ts/servertools/classes.handlerstatic.ts
|
||||
- Added .claude/settings.local.json containing local tool permissions for Claude/dev onboarding
|
||||
- No functional API changes — routing pattern normalization and comment/handler improvements only
|
||||
|
||||
## 2025-09-03 - 3.0.78 - fix(servertools)
|
||||
Fix wildcard path extraction for static/proxy handlers, correct serviceworker route, add local settings and test typo fix
|
||||
|
||||
- Make HandlerProxy and HandlerStatic robust to Express 5 wildcard param shapes (handle req.params.splat, numeric params, req.baseUrl and root routes) to correctly compute relative paths
|
||||
- Change serviceworker route registration to use '/serviceworker/*splat' (instead of previous pattern) for consistent wildcard handling
|
||||
- Fix test wording typo in test/test.server.ts ('exposer' -> 'expose')
|
||||
- Add .claude/settings.local.json with local tool permissions and add .serena/.gitignore to ignore /cache
|
||||
|
||||
## 2025-08-17 - 3.0.77 - fix(servertools)
|
||||
Adjust route wildcard patterns and CORS handling; update serviceworker and SSL redirect patterns; bump express dependency; add local Claude settings
|
||||
|
||||
- Normalize route wildcard patterns to use splat tokens (e.g. '/someroute/*' -> '/someroute/*splat', '/*' -> '/*splat') for consistent route matching
|
||||
- Update serviceworker route registration to '/serviceworker{.*}' and adjust related handler
|
||||
- Change SSL redirect route from '*' to '/*splat'
|
||||
- Adjust CORS preflight options path to '/*splat' and update tests to reflect new route patterns
|
||||
- Bump express dependency from ^4.21.2 to ^5.1.0
|
||||
- Add .claude/settings.local.json for local Claude tool permissions
|
||||
|
||||
## 2025-08-16 - 3.0.76 - fix(handlerproxy)
|
||||
Use SmartRequest API and improve proxy/asset response handling; update tests and bump dependencies; add local project configuration files
|
||||
|
||||
- Replace deprecated smartrequest.request usage with SmartRequest fluent API in ts/servertools/classes.handlerproxy.ts and add explicit handling for GET/POST/PUT/DELETE/PATCH methods.
|
||||
- Normalize proxied response body handling by using arrayBuffer()/text() and converting to Buffer to avoid body type inconsistencies.
|
||||
- Switch asset fetching in ts/utilityservers/classes.websiteserver.ts to SmartRequest + arrayBuffer for reliable binary handling.
|
||||
- Update tests (test/test.server.ts) to use SmartRequest.create() and to read response bodies via response.text(), matching the updated request API.
|
||||
- Bump dependencies: @push.rocks/smartrequest -> ^4.2.1 and body-parser -> ^2.2.0.
|
||||
- Add local project configuration files: .claude/settings.local.json and .serena/project.yml.
|
||||
|
||||
## 2025-08-16 - 3.0.75 - fix(deps)
|
||||
Update dependencies, test tooling and test imports; enhance npm test script
|
||||
|
||||
- Bump multiple runtime dependencies to newer patch/minor versions (notable updates: @cloudflare/workers-types, @push.rocks/* packages such as smartchok, smartfile, smartlog, smartpath, smartrx, and others, @tsclass/tsclass, @types/express, lit).
|
||||
- Upgrade dev tooling versions: @git.zone/tsbuild and @git.zone/tsbundle; update @git.zone/tstest to v2.3.4.
|
||||
- Improve npm test script by adding --verbose, --logfile and increased --timeout.
|
||||
- Fix test imports to use @git.zone/tstest/tapbundle in test/test.reload.ts and test/test.server.ts.
|
||||
|
||||
## 2025-04-12 - 3.0.74 - fix(commit-info)
|
||||
chore: update commit metadata (no source code changes)
|
||||
|
||||
- Uncommitted diff shows no changes in source files; the commit updates internal commit info only.
|
||||
|
||||
## 2025-04-11 - 3.0.73 - fix(metadata)
|
||||
Update repository URLs and metadata to reflect the new organization scope
|
||||
|
||||
|
||||
57
package.json
57
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@api.global/typedserver",
|
||||
"version": "3.0.73",
|
||||
"version": "4.1.1",
|
||||
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"./web_serviceworker_client": "./dist_ts_web_serviceworker_client/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run build && tstest test/",
|
||||
"test": "npm run build && tstest test/ --verbose --logfile --timeout 60",
|
||||
"build": "tsbuild tsfolders --web --allowimplicitany && npm run bundle",
|
||||
"bundle": "tsbundle --from ./ts_web_inject/index.ts --to ./dist_ts_web_inject/bundle.js && tsbundle --from ./ts_web_serviceworker/index.ts --to ./dist_ts_web_serviceworker/serviceworker.bundle.js",
|
||||
"interfaces": "tsbuild interfaces --web --allowimplicitany --skiplibcheck",
|
||||
@@ -60,17 +60,17 @@
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.1.10",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedsocket": "^3.0.1",
|
||||
"@cloudflare/workers-types": "^4.20250410.0",
|
||||
"@api.global/typedsocket": "^3.1.1",
|
||||
"@cloudflare/workers-types": "^4.20251202.0",
|
||||
"@design.estate/dees-comms": "^1.0.27",
|
||||
"@push.rocks/lik": "^6.1.0",
|
||||
"@push.rocks/smartchok": "^1.0.34",
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartenv": "^5.0.12",
|
||||
"@push.rocks/smartfeed": "^1.0.11",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.0.7",
|
||||
"@push.rocks/smartenv": "^6.0.0",
|
||||
"@push.rocks/smartfeed": "^1.4.0",
|
||||
"@push.rocks/smartfile": "^13.1.0",
|
||||
"@push.rocks/smartfs": "^1.2.0",
|
||||
"@push.rocks/smartjson": "^5.2.0",
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartlog-destination-devtools": "^1.0.12",
|
||||
"@push.rocks/smartlog-interfaces": "^3.0.2",
|
||||
"@push.rocks/smartmanifest": "^2.0.2",
|
||||
@@ -78,31 +78,32 @@
|
||||
"@push.rocks/smartmime": "^2.0.4",
|
||||
"@push.rocks/smartntml": "^2.0.8",
|
||||
"@push.rocks/smartopen": "^2.0.0",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartrx": "^3.0.7",
|
||||
"@push.rocks/smartsitemap": "^2.0.3",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartserve": "^1.1.0",
|
||||
"@push.rocks/smartsitemap": "^2.0.4",
|
||||
"@push.rocks/smartstream": "^3.2.5",
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@push.rocks/webrequest": "^3.0.37",
|
||||
"@push.rocks/smartwatch": "^5.0.0",
|
||||
"@push.rocks/taskbuffer": "^3.4.0",
|
||||
"@push.rocks/webrequest": "^4.0.1",
|
||||
"@push.rocks/webstore": "^2.0.20",
|
||||
"@tsclass/tsclass": "^8.2.0",
|
||||
"@types/express": "^5.0.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"body-parser": "^2.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"express": "^5.2.1",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^3.2.1"
|
||||
"lit": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsbundle": "^2.2.5",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/tapbundle": "^5.6.3",
|
||||
"@types/node": "^22.14.0"
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsbundle": "^2.6.2",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@types/node": "^24.10.1"
|
||||
},
|
||||
"private": false,
|
||||
"browserslist": [
|
||||
|
||||
8242
pnpm-lock.yaml
generated
8242
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +1,41 @@
|
||||
|
||||
# Project Hints - @api.global/typedserver
|
||||
|
||||
## Recent Changes (December 2025)
|
||||
|
||||
### Dependency Updates
|
||||
- `@push.rocks/smartchok` replaced with `@push.rocks/smartwatch` (renamed package, same API)
|
||||
- `@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
|
||||
- `@push.rocks/smartrequest` upgraded to v5.0.1
|
||||
- `@push.rocks/webrequest` upgraded to v4.0.1 (`WebRequest` renamed to `WebrequestClient`)
|
||||
- Express upgraded to v5.2.1
|
||||
- All `@git.zone/*` dev dependencies updated to latest
|
||||
|
||||
### Code Migration Notes
|
||||
|
||||
#### smartfile v13 Migration
|
||||
- Old: `plugins.smartfile.fs.toStringSync(path)` / `plugins.smartfile.fs.toBufferSync(path)`
|
||||
- New: Use `plugins.fsInstance` (SmartFs instance with Node provider)
|
||||
- String: `await plugins.fsInstance.file(path).encoding('utf8').read() as string`
|
||||
- Buffer: `await plugins.fsInstance.file(path).read() as Buffer`
|
||||
|
||||
#### smartfs treeHash
|
||||
- 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`
|
||||
|
||||
### Architecture
|
||||
- `plugins.fsInstance` is a pre-configured SmartFs instance with SmartFsProviderNode
|
||||
- All file operations should be async using smartfs
|
||||
- Sync file operations have been removed
|
||||
|
||||
### Express 5 Notes
|
||||
- Wildcard routes use `/{*splat}` notation
|
||||
- `req.params.splat` can be an array, use `Array.isArray()` check
|
||||
|
||||
405
readme.md
405
readme.md
@@ -1,249 +1,278 @@
|
||||
# @api.global/typedserver
|
||||
|
||||
A TypeScript-based framework for serving static files with advanced features including live reloading, compression, and type-safe API requests. Part of the @api.global ecosystem, it integrates seamlessly with @api.global/typedrequest for type-safe HTTP requests and @api.global/typedsocket for WebSocket communication.
|
||||
A powerful TypeScript-first web server framework featuring static file serving, live reload, compression, and seamless type-safe API integration. Part of the `@api.global` ecosystem, it provides a modern foundation for building full-stack TypeScript applications with first-class support for typed HTTP requests and WebSocket communication.
|
||||
|
||||
## Features
|
||||
## Issue Reporting and Security
|
||||
|
||||
- **Type-Safe API Ecosystem**:
|
||||
- HTTP Requests via @api.global/typedrequest
|
||||
- WebSocket Support via @api.global/typedsocket
|
||||
- Full TypeScript support across all endpoints
|
||||
- **Service Worker Integration**: Advanced caching and offline capabilities
|
||||
- **Edge Worker Support**: Optimized edge computing capabilities
|
||||
- **Live Reload**: Automatic browser refresh on file changes
|
||||
- **Compression**: Built-in support for response compression
|
||||
- **CORS Management**: Flexible cross-origin resource sharing
|
||||
- **TypeScript First**: Built with and for TypeScript
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Components
|
||||
## ✨ Features
|
||||
|
||||
### Core Server (`ts/`)
|
||||
- Static file serving with Express
|
||||
- Type-safe request handling
|
||||
- Live reload functionality
|
||||
- Compression middleware
|
||||
- 🔒 **Type-Safe API Ecosystem** - Full TypeScript support with `@api.global/typedrequest` and `@api.global/typedsocket`
|
||||
- ⚡ **Live Reload** - Automatic browser refresh on file changes during development
|
||||
- 🗜️ **Compression** - Built-in support for gzip, deflate, and brotli compression
|
||||
- 🌐 **CORS Management** - Flexible cross-origin resource sharing configuration
|
||||
- 🔧 **Service Worker Integration** - Advanced caching and offline capabilities
|
||||
- ☁️ **Edge Worker Support** - Cloudflare Workers compatible edge computing
|
||||
- 📡 **WebSocket Support** - Real-time bidirectional communication via TypedSocket
|
||||
- 🗺️ **Sitemap & Feeds** - Automatic sitemap and RSS feed generation
|
||||
- 🤖 **Robots.txt** - Built-in robots.txt handling
|
||||
|
||||
### Service Worker (`ts_web_serviceworker/`)
|
||||
- `CacheManager`: Advanced caching strategies
|
||||
- `NetworkManager`: Request/response handling
|
||||
- `UpdateManager`: Cache invalidation and updates
|
||||
- `ServiceWorker`: Core service worker implementation
|
||||
|
||||
### Edge Worker (`ts_edgeworker/`)
|
||||
- Edge computing capabilities
|
||||
- Request/response transformation
|
||||
- Edge caching strategies
|
||||
|
||||
### Web Inject (`ts_web_inject/`)
|
||||
- Live reload script injection
|
||||
- Runtime dependency management
|
||||
- Dynamic module loading
|
||||
|
||||
## Installation
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
# Using pnpm (recommended)
|
||||
pnpm add @api.global/typedserver
|
||||
|
||||
# Using npm
|
||||
npm install @api.global/typedserver
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Basic Static File Server
|
||||
|
||||
```typescript
|
||||
import { TypedServer } from '@api.global/typedserver';
|
||||
|
||||
const server = new TypedServer({
|
||||
port: 3000,
|
||||
serveDir: './public',
|
||||
watch: true,
|
||||
compression: true
|
||||
cors: true,
|
||||
watch: true, // Enable file watching
|
||||
injectReload: true, // Inject live reload script
|
||||
});
|
||||
|
||||
server.start();
|
||||
await server.start();
|
||||
console.log('Server running on port 3000');
|
||||
```
|
||||
|
||||
## Type-Safe API Integration
|
||||
### Server with All Options
|
||||
|
||||
### HTTP Requests with TypedRequest
|
||||
```typescript
|
||||
import { TypedRequest } from '@api.global/typedrequest';
|
||||
import { TypedServer } from '@api.global/typedserver';
|
||||
|
||||
// Define your request/response interface
|
||||
interface IUserRequest {
|
||||
const server = new TypedServer({
|
||||
port: 8080,
|
||||
serveDir: './dist',
|
||||
cors: true,
|
||||
watch: true,
|
||||
injectReload: true,
|
||||
enableCompression: true,
|
||||
preferredCompressionMethod: 'brotli',
|
||||
forceSsl: false,
|
||||
sitemap: true,
|
||||
feed: true,
|
||||
robots: true,
|
||||
domain: 'example.com',
|
||||
appVersion: 'v1.0.0',
|
||||
manifest: {
|
||||
name: 'My App',
|
||||
short_name: 'myapp',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#ffffff',
|
||||
theme_color: '#000000',
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
```
|
||||
|
||||
## 🔌 Type-Safe API Integration
|
||||
|
||||
### Adding TypedRequest Handlers
|
||||
|
||||
```typescript
|
||||
import { TypedServer, servertools } from '@api.global/typedserver';
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
|
||||
// Define your typed request interface
|
||||
interface IGetUser {
|
||||
method: 'getUser';
|
||||
request: { userId: string };
|
||||
response: { username: string; email: string; };
|
||||
response: { name: string; email: string };
|
||||
}
|
||||
|
||||
// Create and use a typed request
|
||||
const getUserRequest = new TypedRequest<IUserRequest>('/api/users', 'getUser');
|
||||
const user = await getUserRequest.fire({ userId: '123' });
|
||||
```
|
||||
const server = new TypedServer({ serveDir: './public', cors: true });
|
||||
|
||||
### WebSocket Communication
|
||||
```typescript
|
||||
import { TypedSocket } from '@api.global/typedsocket';
|
||||
// Create a typed router
|
||||
const typedRouter = new typedrequest.TypedRouter();
|
||||
|
||||
// Server setup
|
||||
const typedRouter = new TypedRouter();
|
||||
const server = await TypedSocket.createServer(typedRouter);
|
||||
|
||||
// Client connection
|
||||
const client = await TypedSocket.createClient(typedRouter, 'ws://localhost:3000');
|
||||
|
||||
// Type-safe real-time messaging
|
||||
interface IChatMessage {
|
||||
method: 'sendMessage';
|
||||
request: { text: string };
|
||||
response: { id: string; timestamp: number; };
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Service Worker Setup
|
||||
|
||||
```typescript
|
||||
import { ServiceWorker } from '@api.global/typedserver/web_serviceworker';
|
||||
|
||||
const sw = new ServiceWorker({
|
||||
cacheStrategy: 'network-first',
|
||||
offlineSupport: true
|
||||
});
|
||||
|
||||
sw.register();
|
||||
```
|
||||
|
||||
### Edge Worker Configuration
|
||||
|
||||
```typescript
|
||||
import { EdgeWorker } from '@api.global/typedserver/edgeworker';
|
||||
|
||||
const edge = new EdgeWorker({
|
||||
transforms: ['compress', 'minify'],
|
||||
caching: true
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Server Options
|
||||
```typescript
|
||||
interface IServerOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
serveDir: string;
|
||||
watch?: boolean;
|
||||
compression?: boolean;
|
||||
cors?: boolean | CorsOptions;
|
||||
cache?: CacheOptions;
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Strategies
|
||||
```typescript
|
||||
type CacheStrategy =
|
||||
| 'network-first'
|
||||
| 'cache-first'
|
||||
| 'stale-while-revalidate';
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
See [API Documentation](https://api.global/docs/typedserver) for detailed API reference.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE for details.
|
||||
|
||||
Task Venture Capital GmbH © 2024
|
||||
|
||||
```typescript
|
||||
// Define a request type
|
||||
interface MyCustomRequest {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
// Define a response type
|
||||
interface MyCustomResponse {
|
||||
userName: string;
|
||||
}
|
||||
```
|
||||
|
||||
Next, set up a route to handle requests using these types:
|
||||
|
||||
```typescript
|
||||
import { TypedRouter, TypedHandler } from '@api.global/typedrequest';
|
||||
|
||||
// Instantiate a TypedRouter
|
||||
const typedRouter = new TypedRouter();
|
||||
|
||||
// Register a route with request and response types
|
||||
typedRouter.addTypedHandler<MyCustomRequest, MyCustomResponse>(
|
||||
new TypedHandler('getUser', async (requestData) => {
|
||||
// Implement your logic here. For example, fetch user data from a database.
|
||||
const userData = { userName: 'John Doe' }; // Dummy implementation
|
||||
return { response: userData };
|
||||
// Add a typed handler
|
||||
typedRouter.addTypedHandler<IGetUser>(
|
||||
new typedrequest.TypedHandler('getUser', async (data) => {
|
||||
// Your logic here
|
||||
return { name: 'John Doe', email: 'john@example.com' };
|
||||
})
|
||||
);
|
||||
|
||||
// Bind the typed router to the server
|
||||
typedServer.useTypedRouter(typedRouter);
|
||||
// Attach the router to the server
|
||||
server.server.addRoute('/api', new servertools.HandlerTypedRouter(typedRouter));
|
||||
|
||||
// Now, the route is set up to handle requests with type checking
|
||||
await server.start();
|
||||
```
|
||||
|
||||
This example shows defining types for requests and responses, creating a `TypedRouter`, and adding a route with typed handling. This feature brings the benefits of TypeScript's static type checking to server-side logic, improving the development experience.
|
||||
|
||||
### Enabling SSL/TLS
|
||||
|
||||
To enable SSL/TLS, configure the `TypedServer` with the SSL options, including the paths to your SSL certificate and key files:
|
||||
### WebSocket Communication with TypedSocket
|
||||
|
||||
```typescript
|
||||
const serverOptions = {
|
||||
port: 443,
|
||||
serveDir: 'public',
|
||||
watch: true,
|
||||
injectReload: true,
|
||||
import { TypedServer } from '@api.global/typedserver';
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
const server = new TypedServer({ serveDir: './public', cors: true });
|
||||
const typedRouter = new typedrequest.TypedRouter();
|
||||
|
||||
await server.start();
|
||||
|
||||
// Create WebSocket server attached to the HTTP server
|
||||
const socketServer = await typedsocket.TypedSocket.createServer(
|
||||
typedRouter,
|
||||
server.server
|
||||
);
|
||||
|
||||
// Handle real-time events
|
||||
interface IChatMessage {
|
||||
method: 'sendMessage';
|
||||
request: { text: string; room: string };
|
||||
response: { messageId: string; timestamp: number };
|
||||
}
|
||||
|
||||
typedRouter.addTypedHandler<IChatMessage>(
|
||||
new typedrequest.TypedHandler('sendMessage', async (data) => {
|
||||
return { messageId: crypto.randomUUID(), timestamp: Date.now() };
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## 🛠️ Server Tools
|
||||
|
||||
### Custom Route Handlers
|
||||
|
||||
```typescript
|
||||
import { servertools } from '@api.global/typedserver';
|
||||
|
||||
const server = new servertools.Server({
|
||||
cors: true,
|
||||
privateKey: fs.readFileSync('path/to/ssl/private.key'),
|
||||
publicKey: fs.readFileSync('path/to/ssl/certificate.crt')
|
||||
domain: 'example.com',
|
||||
});
|
||||
|
||||
// Add a custom route with handler
|
||||
server.addRoute('/api/hello', new servertools.Handler('GET', async (req, res) => {
|
||||
res.json({ message: 'Hello, World!' });
|
||||
}));
|
||||
|
||||
// Serve static files from a directory
|
||||
server.addRoute('/{*splat}', new servertools.HandlerStatic('./public', {
|
||||
serveIndexHtmlDefault: true,
|
||||
enableCompression: true,
|
||||
}));
|
||||
|
||||
await server.start(3000);
|
||||
```
|
||||
|
||||
### Proxy Handler
|
||||
|
||||
```typescript
|
||||
import { servertools } from '@api.global/typedserver';
|
||||
|
||||
const server = new servertools.Server({ cors: true });
|
||||
|
||||
// Proxy requests to another server
|
||||
server.addRoute('/proxy/{*splat}', new servertools.HandlerProxy({
|
||||
target: 'https://api.example.com',
|
||||
}));
|
||||
|
||||
await server.start(3000);
|
||||
```
|
||||
|
||||
## ☁️ Edge Worker (Cloudflare Workers)
|
||||
|
||||
```typescript
|
||||
import { EdgeWorker, DomainRouter } from '@api.global/typedserver/edgeworker';
|
||||
|
||||
const router = new DomainRouter();
|
||||
|
||||
router.addDomainInstruction({
|
||||
domainPattern: '*.example.com',
|
||||
originUrl: 'https://origin.example.com',
|
||||
cacheConfig: { maxAge: 3600 },
|
||||
});
|
||||
|
||||
const worker = new EdgeWorker(router);
|
||||
|
||||
// In your Cloudflare Worker entry point
|
||||
export default {
|
||||
fetch: (request: Request, env: any, ctx: any) => worker.handleRequest(request, env, ctx),
|
||||
};
|
||||
|
||||
const typedServer = new TypedServer(serverOptions);
|
||||
startServer().catch(console.error);
|
||||
```
|
||||
|
||||
Replace `'path/to/ssl/private.key'` and `'path/to/ssl/certificate.crt'` with the actual paths to your SSL key and certificate files. This setup ensures that your server communicates over HTTPS, encrypting the data transmitted between the client and the server.
|
||||
## 🔧 Service Worker Client
|
||||
|
||||
### Conclusion
|
||||
```typescript
|
||||
import { ServiceWorkerClient } from '@api.global/typedserver/web_serviceworker_client';
|
||||
|
||||
`@api.global/typedserver` offers a streamlined way to set up a web server with TypeScript, featuring static file serving, live reloading, typed request/response handling, and SSL support. This guide covers the basic usage, but `TypedServer` is highly configurable, catering to various hosting and development needs.
|
||||
const swClient = new ServiceWorkerClient();
|
||||
|
||||
// Register and manage service worker
|
||||
await swClient.register('/serviceworker.bundle.js');
|
||||
|
||||
// Listen for updates
|
||||
swClient.onUpdate(() => {
|
||||
console.log('New version available!');
|
||||
});
|
||||
```
|
||||
|
||||
For a deeper dive into the API and more advanced configurations, refer to the official documentation and type definitions included in the package.
|
||||
## 📋 Configuration Reference
|
||||
|
||||
### IServerOptions
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `serveDir` | `string` | - | Directory to serve static files from |
|
||||
| `port` | `number \| string` | `3000` | Port to listen on |
|
||||
| `cors` | `boolean` | `true` | Enable CORS headers |
|
||||
| `watch` | `boolean` | `false` | Watch files for changes |
|
||||
| `injectReload` | `boolean` | `false` | Inject live reload script into HTML |
|
||||
| `enableCompression` | `boolean` | `false` | Enable response compression |
|
||||
| `preferredCompressionMethod` | `'gzip' \| 'deflate' \| 'brotli'` | - | Preferred compression algorithm |
|
||||
| `forceSsl` | `boolean` | `false` | Redirect HTTP to HTTPS |
|
||||
| `sitemap` | `boolean` | `false` | Generate sitemap at `/sitemap` |
|
||||
| `feed` | `boolean` | `false` | Generate RSS feed at `/feed` |
|
||||
| `robots` | `boolean` | `false` | Serve robots.txt |
|
||||
| `domain` | `string` | - | Domain name for sitemap/feeds |
|
||||
| `appVersion` | `string` | - | Application version string |
|
||||
| `manifest` | `object` | - | Web App Manifest configuration |
|
||||
| `publicKey` | `string` | - | SSL certificate |
|
||||
| `privateKey` | `string` | - | SSL private key |
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
@api.global/typedserver
|
||||
├── /backend - Main server exports (TypedServer, servertools)
|
||||
├── /edgeworker - Cloudflare Workers edge computing
|
||||
├── /web_inject - Live reload script injection
|
||||
├── /web_serviceworker - Service Worker implementation
|
||||
└── /web_serviceworker_client - Service Worker client utilities
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This 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.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**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.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This 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.
|
||||
This 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 or third parties, 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 or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
|
||||
import { TypedServer } from '../ts/index.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
// helper dependencies
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
@@ -48,7 +48,7 @@ tap.test('should create a valid Server', async () => {
|
||||
|
||||
tap.test('should create a valid Route', async () => {
|
||||
testRoute = testServer.addRoute('/someroute');
|
||||
testRoute2 = testServer.addRoute('/someroute/*');
|
||||
testRoute2 = testServer.addRoute('/someroute/*splat');
|
||||
expect(testRoute).toBeInstanceOf(typedserver.servertools.Route);
|
||||
});
|
||||
|
||||
@@ -95,15 +95,18 @@ tap.test('should start the server allright', async () => {
|
||||
|
||||
// see if a demo request holds up
|
||||
tap.test('should issue a request', async (tools) => {
|
||||
const response = await smartrequest.postJson('http://127.0.0.1:3000/someroute', {
|
||||
headers: {
|
||||
const smartRequestInstance = smartrequest.SmartRequest.create();
|
||||
const response = await smartRequestInstance
|
||||
.url('http://127.0.0.1:3000/someroute')
|
||||
.headers({
|
||||
'X-Forwarded-Proto': 'https',
|
||||
},
|
||||
requestBody: {
|
||||
})
|
||||
.json({
|
||||
someprop: 'hi',
|
||||
},
|
||||
});
|
||||
console.log(response.body);
|
||||
})
|
||||
.post();
|
||||
const responseBody = await response.text();
|
||||
console.log(responseBody);
|
||||
});
|
||||
|
||||
tap.test('should get a file from disk', async () => {
|
||||
@@ -119,7 +122,7 @@ tap.test('should answer a preflight request', async () => {
|
||||
console.log(response.headers);
|
||||
});
|
||||
|
||||
tap.test('should exposer a sitemap', async () => {
|
||||
tap.test('should expose a sitemap', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/sitemap');
|
||||
console.log(await response.text());
|
||||
});
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.73',
|
||||
version: '4.1.1',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import * as servertools from './servertools/index.js';
|
||||
import { type TCompressionMethod } from './servertools/classes.compressor.js';
|
||||
import { DevToolsController } from './controllers/controller.devtools.js';
|
||||
import { TypedRequestController } from './controllers/controller.typedrequest.js';
|
||||
import { BuiltInRoutesController } from './controllers/controller.builtin.js';
|
||||
|
||||
export interface IServerOptions {
|
||||
/**
|
||||
@@ -15,16 +16,6 @@ export interface IServerOptions {
|
||||
*/
|
||||
injectReload?: boolean;
|
||||
|
||||
/**
|
||||
* enable compression
|
||||
*/
|
||||
enableCompression?: boolean;
|
||||
|
||||
/**
|
||||
* choose a preferred compression method
|
||||
*/
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
|
||||
/**
|
||||
* watch the serve directory?
|
||||
*/
|
||||
@@ -34,7 +25,6 @@ export interface IServerOptions {
|
||||
|
||||
/**
|
||||
* a default answer given in case there is no other handler.
|
||||
* @returns
|
||||
*/
|
||||
defaultAnswer?: () => Promise<string>;
|
||||
|
||||
@@ -42,13 +32,14 @@ export interface IServerOptions {
|
||||
* will try to reroute traffic to an ssl connection using headers
|
||||
*/
|
||||
forceSsl?: boolean;
|
||||
|
||||
/**
|
||||
* allows serving manifests
|
||||
*/
|
||||
manifest?: plugins.smartmanifest.ISmartManifestConstructorOptions;
|
||||
|
||||
/**
|
||||
* the port to listen on
|
||||
* can be overwritten when actually starting the server
|
||||
*/
|
||||
port?: number | string;
|
||||
publicKey?: string;
|
||||
@@ -57,6 +48,7 @@ export interface IServerOptions {
|
||||
feed?: boolean;
|
||||
robots?: boolean;
|
||||
domain?: string;
|
||||
|
||||
/**
|
||||
* convey information about the app being served
|
||||
*/
|
||||
@@ -66,22 +58,48 @@ export interface IServerOptions {
|
||||
blockWaybackMachine?: boolean;
|
||||
}
|
||||
|
||||
export class TypedServer {
|
||||
// static
|
||||
// nothing here yet
|
||||
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;
|
||||
}
|
||||
|
||||
export class TypedServer {
|
||||
// instance
|
||||
public options: IServerOptions;
|
||||
public server: servertools.Server;
|
||||
public smartchokInstance: plugins.smartchok.Smartchok;
|
||||
public smartServe: plugins.smartserve.SmartServe;
|
||||
public smartwatchInstance: plugins.smartwatch.Smartwatch;
|
||||
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
|
||||
public serveHash: string = '000000';
|
||||
public typedsocket: plugins.typedsocket.TypedSocket;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
// Sitemap helper
|
||||
private sitemapHelper: SitemapHelper;
|
||||
private smartmanifestInstance: plugins.smartmanifest.SmartManifest;
|
||||
|
||||
// Decorated controllers
|
||||
private devToolsController: DevToolsController;
|
||||
private typedRequestController: TypedRequestController;
|
||||
private builtInRoutesController: BuiltInRoutesController;
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
constructor(optionsArg: IServerOptions) {
|
||||
const standardOptions: IServerOptions = {
|
||||
port: 3000,
|
||||
@@ -94,43 +112,64 @@ export class TypedServer {
|
||||
...standardOptions,
|
||||
...optionsArg,
|
||||
};
|
||||
}
|
||||
|
||||
this.server = new servertools.Server(this.options);
|
||||
// add routes to the smartexpress instance
|
||||
this.server.addRoute(
|
||||
'/typedserver/:request',
|
||||
new servertools.Handler('ALL', async (req, res) => {
|
||||
switch (req.params.request) {
|
||||
case 'devtools':
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.status(200);
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.injectBundlePath));
|
||||
res.end();
|
||||
break;
|
||||
case 'reloadcheck':
|
||||
console.log('got request for reloadcheck');
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.status(200);
|
||||
if (this.ended) {
|
||||
res.write('end');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.write(this.lastReload.toString());
|
||||
res.end();
|
||||
break;
|
||||
default:
|
||||
res.status(404);
|
||||
res.write('Unknown request type');
|
||||
res.end();
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* Access sitemap URLs (for adding/replacing)
|
||||
*/
|
||||
public get sitemap() {
|
||||
return this.sitemapHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom route handler
|
||||
* Supports Express-style path patterns like '/path/:param' and '/path/*splat'
|
||||
* @param path - The route path pattern
|
||||
* @param method - HTTP method (GET, POST, PUT, DELETE, PATCH, ALL)
|
||||
* @param handler - Async function that receives Request and returns Response or null
|
||||
*/
|
||||
public addRoute(path: string, method: THttpMethod, handler: IRouteHandler): void {
|
||||
// Convert Express-style path to regex
|
||||
const paramNames: string[] = [];
|
||||
let regexPattern = path
|
||||
// Handle named parameters :param
|
||||
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
|
||||
paramNames.push(paramName);
|
||||
return '([^/]+)';
|
||||
})
|
||||
);
|
||||
this.server.addRoute(
|
||||
'/typedrequest',
|
||||
new servertools.HandlerTypedRouter(this.typedrouter)
|
||||
);
|
||||
// Handle wildcard *splat (matches everything including slashes)
|
||||
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
|
||||
paramNames.push(paramName);
|
||||
return '(.*)';
|
||||
});
|
||||
|
||||
// Ensure exact match
|
||||
regexPattern = `^${regexPattern}$`;
|
||||
|
||||
this.customRoutes.push({
|
||||
pattern: path,
|
||||
regex: new RegExp(regexPattern),
|
||||
paramNames,
|
||||
method,
|
||||
handler,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse route parameters from a path using a registered route
|
||||
*/
|
||||
private parseRouteParams(
|
||||
route: IRegisteredRoute,
|
||||
pathname: string
|
||||
): Record<string, string> | null {
|
||||
const match = pathname.match(route.regex);
|
||||
if (!match) return null;
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
route.paramNames.forEach((name, index) => {
|
||||
params[name] = match[index + 1];
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,77 +183,118 @@ export class TypedServer {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.options.serveDir) {
|
||||
this.server.addRoute(
|
||||
'/*',
|
||||
new servertools.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
let fileString = responseArg.responseContent.toString();
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
fileStringArray[0] = `${fileStringArray[0]}<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: ${this.lastReload},
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
fileString = fileStringArray.join('');
|
||||
console.log('injected typedserver script.');
|
||||
responseArg.responseContent = Buffer.from(fileString);
|
||||
} else if (this.options.injectReload) {
|
||||
console.log('Could not insert typedserver script - no <head> tag found');
|
||||
}
|
||||
}
|
||||
const headers = responseArg.headers;
|
||||
headers.appHash = this.serveHash;
|
||||
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||
headers['Pragma'] = 'no-cache';
|
||||
headers['Expires'] = '0';
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: responseArg.responseContent,
|
||||
travelData: responseArg.travelData,
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
enableCompression: this.options.enableCompression,
|
||||
preferredCompressionMethod: this.options.preferredCompressionMethod,
|
||||
})
|
||||
);
|
||||
const port =
|
||||
typeof this.options.port === 'string'
|
||||
? parseInt(this.options.port, 10)
|
||||
: this.options.port || 3000;
|
||||
|
||||
// Initialize optional helpers
|
||||
if (this.options.sitemap) {
|
||||
this.sitemapHelper = new SitemapHelper(this.options.domain);
|
||||
}
|
||||
|
||||
if (this.options.manifest) {
|
||||
this.smartmanifestInstance = new plugins.smartmanifest.SmartManifest(this.options.manifest);
|
||||
}
|
||||
|
||||
// Initialize file server for static files
|
||||
if (this.options.serveDir) {
|
||||
this.fileServer = new plugins.smartserve.FileServer({
|
||||
root: this.options.serveDir,
|
||||
index: ['index.html'],
|
||||
etag: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize decorated controllers
|
||||
if (this.options.injectReload) {
|
||||
this.devToolsController = new DevToolsController({
|
||||
getLastReload: () => this.lastReload,
|
||||
getEnded: () => this.ended,
|
||||
});
|
||||
}
|
||||
|
||||
this.typedRequestController = new TypedRequestController(this.typedrouter);
|
||||
|
||||
this.builtInRoutesController = new BuiltInRoutesController({
|
||||
domain: this.options.domain,
|
||||
robots: this.options.robots,
|
||||
manifest: this.smartmanifestInstance,
|
||||
sitemap: this.options.sitemap,
|
||||
feed: this.options.feed,
|
||||
appVersion: this.options.appVersion,
|
||||
feedMetadata: this.options.feedMetadata,
|
||||
articleGetterFunction: this.options.articleGetterFunction,
|
||||
blockWaybackMachine: this.options.blockWaybackMachine,
|
||||
getSitemapUrls: () => this.sitemapHelper?.urls || [],
|
||||
});
|
||||
|
||||
// Register controllers with SmartServe's ControllerRegistry
|
||||
if (this.options.injectReload) {
|
||||
plugins.smartserve.ControllerRegistry.registerInstance(this.devToolsController);
|
||||
}
|
||||
plugins.smartserve.ControllerRegistry.registerInstance(this.typedRequestController);
|
||||
plugins.smartserve.ControllerRegistry.registerInstance(this.builtInRoutesController);
|
||||
|
||||
// Compile routes for fast matching
|
||||
plugins.smartserve.ControllerRegistry.compileRoutes();
|
||||
|
||||
// Build SmartServe options
|
||||
const smartServeOptions: plugins.smartserve.ISmartServeOptions = {
|
||||
port,
|
||||
hostname: '0.0.0.0',
|
||||
tls:
|
||||
this.options.privateKey && this.options.publicKey
|
||||
? {
|
||||
key: this.options.privateKey,
|
||||
cert: this.options.publicKey,
|
||||
}
|
||||
: undefined,
|
||||
websocket: {
|
||||
typedRouter: this.typedrouter,
|
||||
onConnectionOpen: (peer) => {
|
||||
peer.tags.add('typedserver_frontend');
|
||||
console.log(`WebSocket connected: ${peer.id}`);
|
||||
},
|
||||
onConnectionClose: (peer) => {
|
||||
console.log(`WebSocket disconnected: ${peer.id}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.smartServe = new plugins.smartserve.SmartServe(smartServeOptions);
|
||||
|
||||
// Set up custom request handler that integrates with ControllerRegistry
|
||||
this.smartServe.setHandler(async (request: Request): Promise<Response> => {
|
||||
return this.handleRequest(request);
|
||||
});
|
||||
|
||||
// Setup file watching
|
||||
if (this.options.watch && this.options.serveDir) {
|
||||
try {
|
||||
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir]);
|
||||
await this.smartchokInstance.start();
|
||||
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
|
||||
this.smartwatchInstance = new plugins.smartwatch.Smartwatch([this.options.serveDir]);
|
||||
await this.smartwatchInstance.start();
|
||||
(await this.smartwatchInstance.getObservableFor('change')).subscribe(async () => {
|
||||
await this.createServeDirHash();
|
||||
this.reload();
|
||||
});
|
||||
await this.createServeDirHash();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize file watching:', error);
|
||||
// Continue without file watching rather than crashing
|
||||
}
|
||||
}
|
||||
|
||||
// lets start the server
|
||||
await this.server.start();
|
||||
// Start the server
|
||||
await this.smartServe.start();
|
||||
console.log(`TypedServer listening on port ${port}`);
|
||||
|
||||
// Setup TypedSocket using SmartServe integration
|
||||
try {
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
|
||||
this.typedrouter,
|
||||
this.server
|
||||
this.typedsocket = plugins.typedsocket.TypedSocket.fromSmartServe(
|
||||
this.smartServe,
|
||||
this.typedrouter
|
||||
);
|
||||
|
||||
// lets setup typedrouter
|
||||
// Setup typedrouter handlers
|
||||
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
|
||||
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
|
||||
return {
|
||||
@@ -224,10 +304,188 @@ export class TypedServer {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize TypedSocket:', error);
|
||||
// Continue without WebSocket support rather than crashing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IRequestContext from a Request
|
||||
*/
|
||||
private async createContext(
|
||||
request: Request,
|
||||
params: Record<string, string>
|
||||
): Promise<plugins.smartserve.IRequestContext> {
|
||||
const url = new URL(request.url);
|
||||
const method = request.method.toUpperCase() as THttpMethod;
|
||||
|
||||
// Parse query params
|
||||
const query: Record<string, string> = {};
|
||||
url.searchParams.forEach((value, key) => {
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
request,
|
||||
body,
|
||||
params,
|
||||
query,
|
||||
headers: request.headers,
|
||||
path: url.pathname,
|
||||
method,
|
||||
url,
|
||||
runtime: 'node' as const,
|
||||
state: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main request handler - routes to appropriate sub-handlers
|
||||
*/
|
||||
private async handleRequest(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
const method = request.method.toUpperCase() as THttpMethod;
|
||||
|
||||
// First, try to match via ControllerRegistry (decorated routes)
|
||||
const match = plugins.smartserve.ControllerRegistry.matchRoute(path, method);
|
||||
if (match) {
|
||||
try {
|
||||
const context = await this.createContext(request, match.params);
|
||||
const result = await match.route.handler(context);
|
||||
|
||||
// Handle Response or convert to Response
|
||||
if (result instanceof Response) {
|
||||
return result;
|
||||
}
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.smartserve.RouteNotFoundError) {
|
||||
// Route explicitly threw "not found", continue to other handlers
|
||||
} else {
|
||||
console.error('Controller error:', error);
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (response) return response;
|
||||
}
|
||||
|
||||
// Try static file serving
|
||||
if (this.fileServer && (method === 'GET' || method === 'HEAD')) {
|
||||
try {
|
||||
const staticResponse = await this.fileServer.serve(request);
|
||||
if (staticResponse) {
|
||||
return staticResponse;
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall through to 404
|
||||
}
|
||||
}
|
||||
|
||||
// Default answer for root
|
||||
if (path === '/' && method === 'GET' && this.options.defaultAnswer) {
|
||||
const html = await this.options.defaultAnswer();
|
||||
return new Response(html, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
}
|
||||
|
||||
// Not found
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTML files with reload script injection
|
||||
*/
|
||||
private async handleHtmlWithInjection(request: Request): Promise<Response | null> {
|
||||
const url = new URL(request.url);
|
||||
const requestPath = url.pathname;
|
||||
|
||||
// Check if this is a request for an HTML file or root
|
||||
if (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.')) {
|
||||
try {
|
||||
let filePath = requestPath === '/' ? 'index.html' : requestPath.slice(1);
|
||||
if (!filePath.endsWith('.html') && !filePath.includes('.')) {
|
||||
filePath = plugins.path.join(filePath, 'index.html');
|
||||
}
|
||||
|
||||
const fullPath = plugins.path.join(this.options.serveDir, filePath);
|
||||
|
||||
// Security check
|
||||
if (!fullPath.startsWith(this.options.serveDir)) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
let fileContent = (await plugins.fsInstance
|
||||
.file(fullPath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Inject reload script
|
||||
if (fileContent.includes('<head>')) {
|
||||
const injection = `<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: ${this.lastReload},
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
fileContent = fileContent.replace('<head>', injection);
|
||||
console.log('injected typedserver script.');
|
||||
}
|
||||
|
||||
return new Response(fileContent, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
appHash: this.serveHash,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Fall through to default handling
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* reloads the page
|
||||
*/
|
||||
@@ -237,14 +495,17 @@ export class TypedServer {
|
||||
console.warn('TypedSocket not initialized, skipping client notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const connections = await this.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
|
||||
const connections = await this.typedsocket.findAllTargetConnectionsByTag(
|
||||
'typedserver_frontend'
|
||||
);
|
||||
for (const connection of connections) {
|
||||
const pushTime = this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
|
||||
'pushLatestServerChangeTime',
|
||||
connection
|
||||
);
|
||||
const pushTime =
|
||||
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
|
||||
'pushLatestServerChangeTime',
|
||||
connection
|
||||
);
|
||||
pushTime.fire({
|
||||
time: this.lastReload,
|
||||
});
|
||||
@@ -259,7 +520,7 @@ export class TypedServer {
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.ended = true;
|
||||
|
||||
|
||||
const stopWithErrorHandling = async (
|
||||
stopFn: () => Promise<unknown>,
|
||||
componentName: string
|
||||
@@ -270,24 +531,24 @@ export class TypedServer {
|
||||
console.error(`Error stopping ${componentName}:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// Stop server
|
||||
if (this.server) {
|
||||
tasks.push(stopWithErrorHandling(() => this.server.stop(), 'server'));
|
||||
|
||||
// Stop SmartServe
|
||||
if (this.smartServe) {
|
||||
tasks.push(stopWithErrorHandling(() => this.smartServe.stop(), 'SmartServe'));
|
||||
}
|
||||
|
||||
// Stop TypedSocket
|
||||
|
||||
// Stop TypedSocket (in SmartServe mode, this is a no-op but good for cleanup)
|
||||
if (this.typedsocket) {
|
||||
tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
|
||||
}
|
||||
|
||||
|
||||
// Stop file watcher
|
||||
if (this.smartchokInstance) {
|
||||
tasks.push(stopWithErrorHandling(() => this.smartchokInstance.stop(), 'file watcher'));
|
||||
if (this.smartwatchInstance) {
|
||||
tasks.push(stopWithErrorHandling(() => this.smartwatchInstance.stop(), 'file watcher'));
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
@@ -296,17 +557,57 @@ export class TypedServer {
|
||||
*/
|
||||
public async createServeDirHash() {
|
||||
try {
|
||||
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
|
||||
this.serveHash = serveDirHash;
|
||||
console.log('Current ServeDir hash: ' + serveDirHash);
|
||||
this.serveDirHashSubject.next(serveDirHash);
|
||||
const serveDirHash = await plugins.fsInstance
|
||||
.directory(this.options.serveDir)
|
||||
.recursive()
|
||||
.treeHash();
|
||||
this.serveHash = serveDirHash.slice(0, 12);
|
||||
console.log('Current ServeDir hash: ' + this.serveHash);
|
||||
this.serveDirHashSubject.next(this.serveHash);
|
||||
} catch (error) {
|
||||
console.error('Failed to create serve directory hash:', error);
|
||||
// Use a timestamp-based hash as fallback
|
||||
const fallbackHash = Date.now().toString(16).slice(-6);
|
||||
this.serveHash = fallbackHash;
|
||||
console.log('Using fallback hash: ' + fallbackHash);
|
||||
this.serveDirHashSubject.next(fallbackHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Classes
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sitemap helper class
|
||||
*/
|
||||
class SitemapHelper {
|
||||
private smartSitemap = new plugins.smartsitemap.SmartSitemap();
|
||||
public urls: plugins.smartsitemap.IUrlInfo[] = [];
|
||||
|
||||
constructor(domain?: string) {
|
||||
if (domain) {
|
||||
this.urls.push({
|
||||
url: `https://${domain}/`,
|
||||
timestamp: Date.now(),
|
||||
frequency: 'daily',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async createSitemap(): Promise<string> {
|
||||
return this.smartSitemap.createSitemapFromUrlInfoArray(this.urls);
|
||||
}
|
||||
|
||||
async createSitemapNews(articles: plugins.tsclass.content.IArticle[]): Promise<string> {
|
||||
return this.smartSitemap.createSitemapNewsFromArticleArray(articles);
|
||||
}
|
||||
|
||||
replaceUrls(urlsArg: plugins.smartsitemap.IUrlInfo[]) {
|
||||
this.urls = urlsArg;
|
||||
}
|
||||
|
||||
addUrls(urlsArg: plugins.smartsitemap.IUrlInfo[]) {
|
||||
this.urls = this.urls.concat(urlsArg);
|
||||
}
|
||||
}
|
||||
|
||||
125
ts/controllers/controller.builtin.ts
Normal file
125
ts/controllers/controller.builtin.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Built-in routes controller for TypedServer
|
||||
* Handles robots.txt, manifest.json, sitemap, feed, appversion
|
||||
*/
|
||||
@plugins.smartserve.Route('')
|
||||
export class BuiltInRoutesController {
|
||||
private options: {
|
||||
domain?: string;
|
||||
robots?: boolean;
|
||||
manifest?: plugins.smartmanifest.SmartManifest;
|
||||
sitemap?: boolean;
|
||||
feed?: boolean;
|
||||
appVersion?: string;
|
||||
feedMetadata?: plugins.smartfeed.IFeedOptions;
|
||||
articleGetterFunction?: () => Promise<plugins.tsclass.content.IArticle[]>;
|
||||
blockWaybackMachine?: boolean;
|
||||
getSitemapUrls: () => plugins.smartsitemap.IUrlInfo[];
|
||||
};
|
||||
|
||||
constructor(options: typeof BuiltInRoutesController.prototype.options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/robots.txt')
|
||||
async getRobots(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
if (!this.options.robots || !this.options.domain) {
|
||||
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
|
||||
}
|
||||
|
||||
const robotsContent = [
|
||||
'User-agent: *',
|
||||
'Allow: /',
|
||||
`Sitemap: https://${this.options.domain}/sitemap`,
|
||||
];
|
||||
|
||||
if (this.options.blockWaybackMachine) {
|
||||
robotsContent.push('', 'User-agent: ia_archiver', 'Disallow: /');
|
||||
}
|
||||
|
||||
return new Response(robotsContent.join('\n'), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/manifest.json')
|
||||
async getManifest(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
if (!this.options.manifest) {
|
||||
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
|
||||
}
|
||||
|
||||
return new Response(this.options.manifest.jsonString(), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/sitemap')
|
||||
async getSitemap(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
if (!this.options.sitemap || !this.options.domain) {
|
||||
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
|
||||
}
|
||||
|
||||
const smartsitemap = new plugins.smartsitemap.SmartSitemap();
|
||||
const urls = this.options.getSitemapUrls();
|
||||
const sitemapXml = await smartsitemap.createSitemapFromUrlInfoArray(urls);
|
||||
|
||||
return new Response(sitemapXml, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/xml' },
|
||||
});
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/sitemap-news')
|
||||
async getSitemapNews(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
if (!this.options.sitemap || !this.options.domain || !this.options.articleGetterFunction) {
|
||||
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
|
||||
}
|
||||
|
||||
const smartsitemap = new plugins.smartsitemap.SmartSitemap();
|
||||
const articles = await this.options.articleGetterFunction();
|
||||
const sitemapNewsXml = await smartsitemap.createSitemapNewsFromArticleArray(articles);
|
||||
|
||||
return new Response(sitemapNewsXml, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/xml' },
|
||||
});
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/feed')
|
||||
async getFeed(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
if (!this.options.feed || !this.options.feedMetadata) {
|
||||
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
|
||||
}
|
||||
|
||||
const smartfeed = new plugins.smartfeed.Smartfeed();
|
||||
const articles = this.options.articleGetterFunction
|
||||
? await this.options.articleGetterFunction()
|
||||
: [];
|
||||
|
||||
const feedXml = await smartfeed.createFeedFromArticleArray(
|
||||
this.options.feedMetadata,
|
||||
articles
|
||||
);
|
||||
|
||||
return new Response(feedXml, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/atom+xml' },
|
||||
});
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/appversion')
|
||||
async getAppVersion(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
if (!this.options.appVersion) {
|
||||
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
|
||||
}
|
||||
|
||||
return new Response(this.options.appVersion, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
}
|
||||
53
ts/controllers/controller.devtools.ts
Normal file
53
ts/controllers/controller.devtools.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
|
||||
/**
|
||||
* DevTools controller for TypedServer
|
||||
* Handles /typedserver/devtools and /typedserver/reloadcheck endpoints
|
||||
*/
|
||||
@plugins.smartserve.Route('/typedserver')
|
||||
export class DevToolsController {
|
||||
private getLastReload: () => number;
|
||||
private getEnded: () => boolean;
|
||||
|
||||
constructor(options: { getLastReload: () => number; getEnded: () => boolean }) {
|
||||
this.getLastReload = options.getLastReload;
|
||||
this.getEnded = options.getEnded;
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/devtools')
|
||||
async getDevtools(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
const devtoolsContent = (await plugins.fsInstance
|
||||
.file(paths.injectBundlePath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
return new Response(devtoolsContent, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@plugins.smartserve.Get('/reloadcheck')
|
||||
async reloadCheck(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
console.log('got request for reloadcheck');
|
||||
|
||||
if (this.getEnded()) {
|
||||
return new Response('end', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(this.getLastReload().toString(), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
34
ts/controllers/controller.typedrequest.ts
Normal file
34
ts/controllers/controller.typedrequest.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* TypedRequest controller for type-safe RPC endpoint
|
||||
*/
|
||||
@plugins.smartserve.Route('/typedrequest')
|
||||
export class TypedRequestController {
|
||||
private typedRouter: plugins.typedrequest.TypedRouter;
|
||||
|
||||
constructor(typedRouter: plugins.typedrequest.TypedRouter) {
|
||||
this.typedRouter = typedRouter;
|
||||
}
|
||||
|
||||
@plugins.smartserve.Post('/')
|
||||
async handleTypedRequest(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
|
||||
try {
|
||||
const response = await this.typedRouter.routeAndAddResponse(ctx.body as plugins.typedrequestInterfaces.ITypedRequest);
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
3
ts/controllers/index.ts
Normal file
3
ts/controllers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './controller.devtools.js';
|
||||
export * from './controller.typedrequest.js';
|
||||
export * from './controller.builtin.js';
|
||||
@@ -5,10 +5,10 @@ import * as servertools from './servertools/index.js';
|
||||
export { servertools };
|
||||
|
||||
export * from './classes.typedserver.js';
|
||||
// Type helpers
|
||||
export type Request = plugins.express.Request;
|
||||
export type Response = plugins.express.Response;
|
||||
|
||||
// Type helpers - using native Web API Request/Response types
|
||||
// Native Request and Response are available in Node.js 18+ and all modern browsers
|
||||
// Legacy Express types are available via servertools for backward compatibility
|
||||
|
||||
// lets export utilityservers
|
||||
import * as utilityservers from './utilityservers/index.js';
|
||||
|
||||
@@ -3,7 +3,6 @@ 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, zlib };
|
||||
@@ -22,10 +21,11 @@ export { typedrequest, typedrequestInterfaces, typedsocket };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartchok from '@push.rocks/smartchok';
|
||||
import * as smartwatch from '@push.rocks/smartwatch';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartfeed from '@push.rocks/smartfeed';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartfs from '@push.rocks/smartfs';
|
||||
import * as smartjson from '@push.rocks/smartjson';
|
||||
import * as smartmanifest from '@push.rocks/smartmanifest';
|
||||
import * as smartmime from '@push.rocks/smartmime';
|
||||
@@ -40,10 +40,11 @@ import * as smarttime from '@push.rocks/smarttime';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartchok,
|
||||
smartwatch,
|
||||
smartdelay,
|
||||
smartfeed,
|
||||
smartfile,
|
||||
smartfs,
|
||||
smartjson,
|
||||
smartmanifest,
|
||||
smartmime,
|
||||
@@ -57,11 +58,19 @@ export {
|
||||
smartrx,
|
||||
};
|
||||
|
||||
// express
|
||||
// Create a ready-to-use smartfs instance with Node.js provider
|
||||
export const fsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
|
||||
|
||||
// @push.rocks/smartserve
|
||||
import * as smartserve from '@push.rocks/smartserve';
|
||||
|
||||
export { smartserve };
|
||||
|
||||
// Legacy Express dependencies - kept for backward compatibility with deprecated servertools
|
||||
// These will be removed in the next major version
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
// @ts-ignore
|
||||
import expressForceSsl from 'express-force-ssl';
|
||||
|
||||
export { bodyParser, cors, express, expressForceSsl };
|
||||
export { express, bodyParser, cors, expressForceSsl };
|
||||
|
||||
@@ -16,21 +16,67 @@ export class HandlerProxy extends Handler {
|
||||
}
|
||||
) {
|
||||
super('ALL', async (req, res) => {
|
||||
const relativeRequestPath = req.path.slice(req.route.path.length - 1);
|
||||
// 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.IExtendedIncomingMessage;
|
||||
let proxiedResponse: plugins.smartrequest.ICoreResponse;
|
||||
try {
|
||||
proxiedResponse = await plugins.smartrequest.request(proxyRequestUrl, {
|
||||
method: req.method,
|
||||
autoJsonParse: false,
|
||||
});
|
||||
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;
|
||||
}
|
||||
for (const header of Object.keys(proxiedResponse.headers)) {
|
||||
res.set(header, proxiedResponse.headers[header] as string);
|
||||
const headers = proxiedResponse.headers;
|
||||
for (const header of Object.keys(headers)) {
|
||||
res.set(header, headers[header] as string);
|
||||
}
|
||||
|
||||
// set additional headers
|
||||
@@ -40,21 +86,20 @@ export class HandlerProxy extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure body exists and convert it to Buffer consistently
|
||||
// Get response body as buffer
|
||||
let responseToSend: Buffer;
|
||||
|
||||
if (proxiedResponse.body !== undefined && proxiedResponse.body !== null) {
|
||||
if (Buffer.isBuffer(proxiedResponse.body)) {
|
||||
responseToSend = proxiedResponse.body;
|
||||
} else if (typeof proxiedResponse.body === 'string') {
|
||||
responseToSend = Buffer.from(proxiedResponse.body);
|
||||
} else {
|
||||
// Handle other types (like objects) by JSON stringifying them
|
||||
responseToSend = Buffer.from(JSON.stringify(proxiedResponse.body));
|
||||
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('');
|
||||
}
|
||||
} else {
|
||||
// Provide a default empty buffer if body is undefined/null
|
||||
responseToSend = Buffer.from('');
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.responseModifier) {
|
||||
|
||||
@@ -36,7 +36,31 @@ export class HandlerStatic extends Handler {
|
||||
}
|
||||
|
||||
// lets compute some paths
|
||||
let filePath: string = requestPath.slice(req.route.path.length - 1); // lets slice of the root
|
||||
// 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';
|
||||
@@ -68,7 +92,7 @@ export class HandlerStatic extends Handler {
|
||||
// lets actually care about serving, if security checks pass
|
||||
let fileBuffer: Buffer;
|
||||
try {
|
||||
fileBuffer = plugins.smartfile.fs.toBufferSync(joinedPath);
|
||||
fileBuffer = await plugins.fsInstance.file(joinedPath).read() as Buffer;
|
||||
usedPath = joinedPath;
|
||||
} catch (err) {
|
||||
// try serving index.html instead
|
||||
@@ -77,7 +101,7 @@ export class HandlerStatic extends Handler {
|
||||
console.log(`serving default path ${defaultPath} instead of ${joinedPath}`);
|
||||
try {
|
||||
parsedPath = plugins.path.parse(defaultPath);
|
||||
fileBuffer = plugins.smartfile.fs.toBufferSync(defaultPath);
|
||||
fileBuffer = await plugins.fsInstance.file(defaultPath).read() as Buffer;
|
||||
usedPath = defaultPath;
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
|
||||
@@ -132,7 +132,7 @@ export class Server {
|
||||
});
|
||||
|
||||
this.expressAppInstance.use(cors);
|
||||
this.expressAppInstance.options('/*', cors);
|
||||
this.expressAppInstance.options('/{*splat}', cors);
|
||||
}
|
||||
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
export * from './classes.server.js';
|
||||
export * from './classes.route.js';
|
||||
export * from './classes.handler.js';
|
||||
export * from './classes.handlerstatic.js';
|
||||
export * from './classes.handlerproxy.js';
|
||||
export * from './classes.handlertypedrouter.js';
|
||||
// Core utilities that don't depend on Express
|
||||
export * from './classes.compressor.js';
|
||||
import * as serviceworker from './tools.serviceworker.js';
|
||||
|
||||
export {
|
||||
serviceworker,
|
||||
}
|
||||
// Legacy Express-based classes - deprecated, will be removed in next major version
|
||||
// These are kept for backward compatibility but should not be used for new code
|
||||
// Use SmartServe decorator-based controllers instead
|
||||
/** @deprecated Use SmartServe directly */
|
||||
export * from './classes.server.js';
|
||||
/** @deprecated Use SmartServe @Route decorator */
|
||||
export * from './classes.route.js';
|
||||
/** @deprecated Use SmartServe @Get/@Post decorators */
|
||||
export * from './classes.handler.js';
|
||||
/** @deprecated Use SmartServe static file serving */
|
||||
export * from './classes.handlerstatic.js';
|
||||
/** @deprecated Use SmartServe custom handler */
|
||||
export * from './classes.handlerproxy.js';
|
||||
/** @deprecated Use SmartServe TypedRouter integration */
|
||||
export * from './classes.handlertypedrouter.js';
|
||||
|
||||
// Service worker utilities - uses legacy patterns, will be migrated
|
||||
import * as serviceworker from './tools.serviceworker.js';
|
||||
export { serviceworker };
|
||||
|
||||
@@ -1,60 +1,92 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js'
|
||||
import { Handler } from './classes.handler.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import type { TypedServer } from '../classes.typedserver.js';
|
||||
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
|
||||
|
||||
const swBundleJs: string = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js')
|
||||
);
|
||||
const swBundleJsMap: string = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map')
|
||||
);
|
||||
// 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;
|
||||
const serviceworkerHandler = new Handler(
|
||||
'GET',
|
||||
async (req, res) => {
|
||||
if (req.path === '/serviceworker.bundle.js') {
|
||||
res.status(200);
|
||||
res.set('Content-Type', 'text/javascript');
|
||||
res.write(swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`);
|
||||
} else if (req.path === '/serviceworker.bundle.js.map') {
|
||||
res.status(200);
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.write(swBundleJsMap);
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
|
||||
export const addServiceWorkerRoute = (
|
||||
typedserverInstance: TypedServer,
|
||||
swDataFunc: () => interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']
|
||||
) => {
|
||||
// lets the version info as unique string;
|
||||
// Set the version info
|
||||
swVersionInfo = swDataFunc();
|
||||
|
||||
// the basic stuff
|
||||
typedserverInstance.server.addRoute('/serviceworker.*', serviceworkerHandler);
|
||||
// Service worker bundle handler
|
||||
typedserverInstance.addRoute('/serviceworker/*splat', 'GET', async (request: Request) => {
|
||||
await loadServiceWorkerBundle();
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
|
||||
// the typed stuff
|
||||
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
if (path === '/serviceworker/serviceworker.bundle.js' || path === '/serviceworker.bundle.js') {
|
||||
return new Response(
|
||||
swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`,
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript' },
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
path === '/serviceworker/serviceworker.bundle.js.map' ||
|
||||
path === '/serviceworker.bundle.js.map'
|
||||
) {
|
||||
return new Response(swBundleJsMap, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
|
||||
'serviceworker_versionInfo',
|
||||
async (req) => {
|
||||
const versionInfoResponse = swDataFunc();
|
||||
return versionInfoResponse;
|
||||
}
|
||||
)
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
typedserverInstance.server.addRoute(
|
||||
'/sw-typedrequest',
|
||||
new HandlerTypedRouter(typedrouter)
|
||||
);
|
||||
// Typed request handler for service worker
|
||||
typedserverInstance.addRoute('/sw-typedrequest', 'POST', async (request: Request) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Create a local typed router for service worker requests
|
||||
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
|
||||
'serviceworker_versionInfo',
|
||||
async () => {
|
||||
return swDataFunc();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const response = await typedrouter.routeAndAddResponse(body);
|
||||
return new Response(plugins.smartjson.stringify(response), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid request' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export const redirectFrom80To443 = async () => {
|
||||
});
|
||||
|
||||
smartexpressInstance.addRoute(
|
||||
'*',
|
||||
'/{*splat}',
|
||||
new Handler('ALL', async (req, res) => {
|
||||
res.redirect('https://' + req.headers.host + req.url);
|
||||
})
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { TypedServer } from '../classes.typedserver.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export interface ILoleServiceServerConstructorOptions {
|
||||
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
|
||||
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
|
||||
serviceName: string;
|
||||
serviceVersion: string;
|
||||
serviceDomain: string;
|
||||
@@ -20,12 +18,12 @@ export class UtilityServiceServer {
|
||||
}
|
||||
|
||||
public async start() {
|
||||
console.log('starting lole-serviceserver...')
|
||||
console.log('starting lole-serviceserver...');
|
||||
this.typedServer = new TypedServer({
|
||||
cors: true,
|
||||
domain: this.options.serviceDomain,
|
||||
forceSsl: false,
|
||||
port: this.options.port || 3000,
|
||||
port: this.options.port || 3000,
|
||||
robots: true,
|
||||
defaultAnswer: async () => {
|
||||
const InfoHtml = (await import('../infohtml/index.js')).InfoHtml;
|
||||
@@ -37,9 +35,9 @@ export class UtilityServiceServer {
|
||||
},
|
||||
});
|
||||
|
||||
// lets add any custom routes
|
||||
// Add any custom routes
|
||||
if (this.options.addCustomRoutes) {
|
||||
await this.options.addCustomRoutes(this.typedServer.server);
|
||||
await this.options.addCustomRoutes(this.typedServer);
|
||||
}
|
||||
|
||||
await this.typedServer.start();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { type IServerOptions, TypedServer } from '../classes.typedserver.js';
|
||||
import type { Request, Response } from '../index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
|
||||
export interface IUtilityWebsiteServerConstructorOptions {
|
||||
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
|
||||
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
|
||||
appSemVer?: string;
|
||||
domain: string;
|
||||
serveDir: string;
|
||||
@@ -16,7 +15,6 @@ export interface IUtilityWebsiteServerConstructorOptions {
|
||||
* the utility website server implements a best practice server for websites
|
||||
* It supports:
|
||||
* * live reload
|
||||
* * compression
|
||||
* * serviceworker
|
||||
* * pwa manifest
|
||||
*/
|
||||
@@ -30,7 +28,7 @@ export class UtilityWebsiteServer {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start the website server
|
||||
*/
|
||||
public async start(portArg = 3000) {
|
||||
this.typedserver = new TypedServer({
|
||||
@@ -38,8 +36,6 @@ export class UtilityWebsiteServer {
|
||||
injectReload: true,
|
||||
watch: true,
|
||||
serveDir: this.options.serveDir,
|
||||
enableCompression: true,
|
||||
preferredCompressionMethod: 'gzip',
|
||||
domain: this.options.domain,
|
||||
forceSsl: false,
|
||||
manifest: {
|
||||
@@ -58,33 +54,32 @@ export class UtilityWebsiteServer {
|
||||
sitemap: true,
|
||||
});
|
||||
|
||||
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
|
||||
{
|
||||
appHash: 'xxxxxx',
|
||||
appSemVer: this.options.appSemVer || 'x.x.x',
|
||||
};
|
||||
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = {
|
||||
appHash: 'xxxxxx',
|
||||
appSemVer: this.options.appSemVer || 'x.x.x',
|
||||
};
|
||||
|
||||
// -> /lsw* - anything regarding serviceworker
|
||||
servertools.serviceworker.addServiceWorkerRoute(this.typedserver, () => {
|
||||
return lswData;
|
||||
});
|
||||
|
||||
// lets add ads.txt
|
||||
this.typedserver.server.addRoute(
|
||||
'/ads.txt',
|
||||
new servertools.Handler('GET', async (req, res) => {
|
||||
res.type('txt/plain');
|
||||
const adsTxt =
|
||||
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
|
||||
res.write(adsTxt);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
// ads.txt handler
|
||||
this.typedserver.addRoute('/ads.txt', 'GET', async () => {
|
||||
const adsTxt =
|
||||
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
|
||||
return new Response(adsTxt, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
});
|
||||
|
||||
this.typedserver.server.addRoute(
|
||||
// Asset broker manifest handler
|
||||
this.typedserver.addRoute(
|
||||
'/assetbroker/manifest/:manifestAsset',
|
||||
new servertools.Handler('GET', async (req, res) => {
|
||||
let manifestAssetName = req.params.manifestAsset;
|
||||
'GET',
|
||||
async (request: Request) => {
|
||||
let manifestAssetName = (request as any).params?.manifestAsset;
|
||||
if (manifestAssetName === 'favicon.png') {
|
||||
manifestAssetName = `favicon_${this.options.domain
|
||||
.replace('.', '')
|
||||
@@ -92,19 +87,22 @@ export class UtilityWebsiteServer {
|
||||
}
|
||||
const fullOriginAssetUrl = `https://assetbroker.lossless.one/brandfiles/00general/${manifestAssetName}`;
|
||||
console.log(`Getting ${manifestAssetName} from ${fullOriginAssetUrl}`);
|
||||
const dataBuffer: Buffer = (await plugins.smartrequest.getBinary(fullOriginAssetUrl)).body;
|
||||
res.type('.png');
|
||||
res.write(dataBuffer);
|
||||
res.end();
|
||||
})
|
||||
const smartRequest = plugins.smartrequest.SmartRequest.create();
|
||||
const response = await smartRequest.url(fullOriginAssetUrl).get();
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return new Response(arrayBuffer, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// lets add any custom routes
|
||||
// Add any custom routes
|
||||
if (this.options.addCustomRoutes) {
|
||||
await this.options.addCustomRoutes(this.typedserver.server);
|
||||
await this.options.addCustomRoutes(this.typedserver);
|
||||
}
|
||||
|
||||
// -> /* - serve the files
|
||||
// Subscribe to serve directory hash changes
|
||||
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
|
||||
lswData = {
|
||||
appHash,
|
||||
@@ -112,11 +110,11 @@ export class UtilityWebsiteServer {
|
||||
};
|
||||
});
|
||||
|
||||
// lets setup the typedrouter chain
|
||||
// Setup the typedrouter chain
|
||||
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
// lets start everything
|
||||
console.log('routes are all set. Startin up now!');
|
||||
// Start everything
|
||||
console.log('routes are all set. Starting up now!');
|
||||
await this.typedserver.start();
|
||||
console.log('typedserver started!');
|
||||
}
|
||||
@@ -124,14 +122,4 @@ export class UtilityWebsiteServer {
|
||||
public async stop() {
|
||||
await this.typedserver.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* allows you to hanlde requests from other server instances without the need to listen for yourself
|
||||
* note smartexpress allows you start the instance wuith passing >>false<< as second parameter to .start();
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async handleRequest(req: Request, res: Response) {
|
||||
await this.typedserver.server.handleReqRes(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ export class TypedserverInfoscreen extends LitElement {
|
||||
//INSTANCE
|
||||
|
||||
@property()
|
||||
private text = 'Hello';
|
||||
accessor text = 'Hello';
|
||||
|
||||
@property()
|
||||
private success = false;
|
||||
accessor success = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
|
||||
@@ -4,7 +4,7 @@ import { logger } from './logging.js';
|
||||
|
||||
export class NetworkManager {
|
||||
public serviceWorkerRef: ServiceWorker;
|
||||
public webRequest: plugins.webrequest.WebRequest;
|
||||
public webRequest: plugins.webrequest.WebrequestClient;
|
||||
private isOffline: boolean = false;
|
||||
private lastOnlineCheck: number = 0;
|
||||
private readonly ONLINE_CHECK_INTERVAL = 30000; // 30 seconds
|
||||
@@ -13,7 +13,7 @@ export class NetworkManager {
|
||||
|
||||
constructor(serviceWorkerRefArg: ServiceWorker) {
|
||||
this.serviceWorkerRef = serviceWorkerRefArg;
|
||||
this.webRequest = new plugins.webrequest.WebRequest();
|
||||
this.webRequest = new plugins.webrequest.WebrequestClient();
|
||||
|
||||
// Listen for connection changes
|
||||
this.getConnection()?.addEventListener('change', () => {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
|
||||
Reference in New Issue
Block a user