Compare commits

...

8 Commits

Author SHA1 Message Date
2f064c7ea8 v7.8.18
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-05 10:00:40 +00:00
76b5cb5142 fix(readme): Update README to reflect new features and updated examples (SPA/PWA/Edge/ServiceWorker) and clarify API usage 2025-12-05 10:00:40 +00:00
790b468188 7.8.17
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-05 09:26:29 +00:00
24d6d6d2e7 fix(typedserver): Update WebSocket peer tag and add error handling for pushTime 2025-12-05 09:26:27 +00:00
a86fd6c1f3 7.8.16
Some checks failed
Default (tags) / security (push) Failing after 43s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-05 00:18:07 +00:00
d04179ccbe feat(sw-dash): Add right-click context menu for request cards
- Add dees-catalog dependency for DeesContextmenu component
- Right-click on message card shows context menu with options:
  - Copy Full Message (request + response with all data)
  - Copy Request Payload
  - Copy Response Payload
  - Copy Correlation ID
  - Copy Method Name
  - Filter by Method
  - Show Payload (opens modal)
2025-12-05 00:18:07 +00:00
d6eacf5fcc 7.8.15
Some checks failed
Default (tags) / security (push) Failing after 42s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-05 00:14:55 +00:00
0f974701d4 feat(sw-dash): Click method tile to filter by that method 2025-12-05 00:14:55 +00:00
8 changed files with 1070 additions and 111 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## 2025-12-05 - 7.8.18 - fix(readme)
Update README to reflect new features and updated examples (SPA/PWA/Edge/ServiceWorker) and clarify API usage
- Rewrite project introduction and features list to highlight Service Worker, Edge Workers, SPA support, and PWA readiness
- Replace and expand example sections: Basic Server, Full Configuration, TypedRequest handlers, WebSocket usage, Edge Worker entrypoint, and Service Worker client usage
- Update configuration reference: remove legacy compression flags, add spaFallback, defaultAnswer, feedMetadata, and blockWaybackMachine options
- Document package exports and add examples for utility servers (WebsiteServer, ServiceServer)
- Clarify TypedRequest/TypedSocket usage by showing server.typedrouter and service worker client initializer (getServiceworkerClient)
## 2025-12-04 - 7.8.11 - fix(web_inject)
Improve logging in web injection (TypedRequest) and update dees-comms dependency

View File

@@ -1,6 +1,6 @@
{
"name": "@api.global/typedserver",
"version": "7.8.14",
"version": "7.8.18",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {
@@ -62,6 +62,7 @@
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^4.1.0",
"@cloudflare/workers-types": "^4.20251202.0",
"@design.estate/dees-catalog": "^2.0.3",
"@design.estate/dees-comms": "^1.0.30",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.5",

848
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

195
readme.md
View File

@@ -1,6 +1,6 @@
# @api.global/typedserver
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.
A TypeScript-first web server framework for building modern full-stack applications. Features static file serving, live reload, type-safe API integration, service worker support, and edge computing capabilities. Part of the `@api.global` ecosystem.
## Issue Reporting and Security
@@ -8,15 +8,14 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## ✨ Features
- 🔒 **Type-Safe API Ecosystem** - Full TypeScript support with `@api.global/typedrequest` and `@api.global/typedsocket`
- 🔒 **Type-Safe API** - 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** - Advanced caching, offline support, and background sync
- ☁️ **Edge Workers** - Cloudflare Workers compatible edge computing with domain routing
- 📡 **WebSocket** - Real-time bidirectional communication via TypedSocket
- 🗺 **SEO Tools** - Built-in sitemap, RSS feed, and robots.txt generation
- 🎯 **SPA Support** - Single-page application fallback routing
- 📱 **PWA Ready** - Web App Manifest generation for progressive web apps
## 📦 Installation
@@ -30,7 +29,7 @@ npm install @api.global/typedserver
## 🚀 Quick Start
### Basic Static File Server
### Basic Server
```typescript
import { TypedServer } from '@api.global/typedserver';
@@ -43,10 +42,10 @@ const server = new TypedServer({
});
await server.start();
console.log('Server running on port 3000');
console.log('Server running!');
```
### Server with All Options
### Full Configuration
```typescript
import { TypedServer } from '@api.global/typedserver';
@@ -55,15 +54,23 @@ const server = new TypedServer({
port: 8080,
serveDir: './dist',
cors: true,
// Development
watch: true,
injectReload: true,
enableCompression: true,
preferredCompressionMethod: 'brotli',
forceSsl: false,
// Production
forceSsl: true,
spaFallback: true, // Serve index.html for client-side routes
// SEO
sitemap: true,
feed: true,
robots: true,
domain: 'example.com',
blockWaybackMachine: false,
// PWA
appVersion: 'v1.0.0',
manifest: {
name: 'My App',
@@ -83,11 +90,11 @@ await server.start();
### Adding TypedRequest Handlers
```typescript
import { TypedServer, servertools } from '@api.global/typedserver';
import { TypedServer } from '@api.global/typedserver';
import * as typedrequest from '@api.global/typedrequest';
// Define your typed request interface
interface IGetUser {
interface IGetUser extends typedrequest.implementsTR<IGetUser> {
method: 'getUser';
request: { userId: string };
response: { name: string; email: string };
@@ -95,94 +102,46 @@ interface IGetUser {
const server = new TypedServer({ serveDir: './public', cors: true });
// Create a typed router
const typedRouter = new typedrequest.TypedRouter();
// Add a typed handler
typedRouter.addTypedHandler<IGetUser>(
// Add a typed handler directly to the server's router
server.typedrouter.addTypedHandler<IGetUser>(
new typedrequest.TypedHandler('getUser', async (data) => {
// Your logic here
return { name: 'John Doe', email: 'john@example.com' };
})
);
// Attach the router to the server
server.server.addRoute('/api', new servertools.HandlerTypedRouter(typedRouter));
await server.start();
```
### WebSocket Communication with TypedSocket
### Real-Time WebSocket Communication
TypedServer automatically sets up TypedSocket for real-time communication:
```typescript
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 {
interface IChatMessage extends typedrequest.implementsTR<IChatMessage> {
method: 'sendMessage';
request: { text: string; room: string };
response: { messageId: string; timestamp: number };
}
typedRouter.addTypedHandler<IChatMessage>(
const server = new TypedServer({ serveDir: './public', cors: true });
// Handle real-time messages
server.typedrouter.addTypedHandler<IChatMessage>(
new typedrequest.TypedHandler('sendMessage', async (data) => {
return { messageId: crypto.randomUUID(), timestamp: Date.now() };
})
);
```
## 🛠️ Server Tools
await server.start();
### Custom Route Handlers
```typescript
import { servertools } from '@api.global/typedserver';
const server = new servertools.Server({
cors: true,
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);
// Push messages to connected clients
const connections = await server.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
for (const conn of connections) {
// Push to specific clients
}
```
## ☁️ Edge Worker (Cloudflare Workers)
@@ -190,36 +149,44 @@ await server.start(3000);
```typescript
import { EdgeWorker, DomainRouter } from '@api.global/typedserver/edgeworker';
const router = new DomainRouter();
const worker = new EdgeWorker();
router.addDomainInstruction({
// Configure domain routing
worker.domainRouter.addDomainInstruction({
domainPattern: '*.example.com',
originUrl: 'https://origin.example.com',
type: 'cache',
cacheConfig: { maxAge: 3600 },
});
const worker = new EdgeWorker(router);
worker.domainRouter.addDomainInstruction({
domainPattern: 'api.example.com',
originUrl: 'https://api-origin.example.com',
type: 'origin', // Pass through to origin
});
// In your Cloudflare Worker entry point
// Cloudflare Worker entry point
export default {
fetch: (request: Request, env: any, ctx: any) => worker.handleRequest(request, env, ctx),
fetch: worker.fetchFunction.bind(worker),
};
```
## 🔧 Service Worker Client
Manage service workers in your frontend application:
```typescript
import { ServiceWorkerClient } from '@api.global/typedserver/web_serviceworker_client';
import { getServiceworkerClient } from '@api.global/typedserver/web_serviceworker_client';
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!');
// Initialize and register service worker
const swClient = await getServiceworkerClient({
pollInterval: 30000, // Poll for updates every 30s
});
// The service worker handles:
// - Cache invalidation from server
// - Offline support
// - Background sync
```
## 📋 Configuration Reference
@@ -233,9 +200,8 @@ swClient.onUpdate(() => {
| `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 |
| `spaFallback` | `boolean` | `false` | Serve index.html for non-file routes |
| `sitemap` | `boolean` | `false` | Generate sitemap at `/sitemap` |
| `feed` | `boolean` | `false` | Generate RSS feed at `/feed` |
| `robots` | `boolean` | `false` | Serve robots.txt |
@@ -244,16 +210,39 @@ swClient.onUpdate(() => {
| `manifest` | `object` | - | Web App Manifest configuration |
| `publicKey` | `string` | - | SSL certificate |
| `privateKey` | `string` | - | SSL private key |
| `defaultAnswer` | `function` | - | Custom default response handler |
| `feedMetadata` | `object` | - | RSS feed metadata options |
| `blockWaybackMachine` | `boolean` | `false` | Block Wayback Machine archiving |
## 🏗️ Architecture
## 🏗️ Package Exports
```
@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
├── . - Main server (TypedServer)
├── /backend - Alias for main server
├── /edgeworker - Cloudflare Workers edge computing
├── /web_inject - Live reload script injection
── /web_serviceworker - Service Worker implementation
└── /web_serviceworker_client - Service Worker client utilities
```
## 🔄 Utility Servers
Pre-configured server templates for common use cases:
```typescript
import { utilityservers } from '@api.global/typedserver';
// WebsiteServer - optimized for static websites
const websiteServer = new utilityservers.WebsiteServer({
serveDir: './dist',
domain: 'example.com',
});
// ServiceServer - optimized for API services
const serviceServer = new utilityservers.ServiceServer({
cors: true,
});
```
## License and Legal Information

View File

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

View File

@@ -258,7 +258,7 @@ export class TypedServer {
websocket: {
typedRouter: this.typedrouter,
onConnectionOpen: (peer) => {
peer.tags.add('typedserver_frontend');
peer.tags.add('allClients');
console.log(`WebSocket connected: ${peer.id}`);
},
onConnectionClose: (peer) => {
@@ -644,6 +644,8 @@ export class TypedServer {
);
pushTime.fire({
time: this.lastReload,
}).catch(err => {
console.warn('Failed to push latest server change time to client:', err);
});
}
} catch (error) {

View File

@@ -6,6 +6,9 @@ import { customElement, property, state } from 'lit/decorators.js';
// DeesComms for push communication
import * as deesComms from '@design.estate/dees-comms';
// Dees-catalog for UI components
import { DeesContextmenu } from '@design.estate/dees-catalog';
export {
LitElement,
html,
@@ -14,6 +17,7 @@ export {
property,
state,
deesComms,
DeesContextmenu,
};
export type { CSSResult, TemplateResult };

View File

@@ -1,4 +1,4 @@
import { LitElement, html, css, property, state, customElement } from './plugins.js';
import { LitElement, html, css, property, state, customElement, DeesContextmenu } from './plugins.js';
import type { CSSResult, TemplateResult } from './plugins.js';
import { sharedStyles, panelStyles, tableStyles, buttonStyles } from './sw-dash-styles.js';
@@ -237,6 +237,17 @@ export class SwDashRequests extends LitElement {
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
padding: var(--space-2);
cursor: pointer;
transition: background 0.15s ease;
}
.method-stat-card:hover {
background: var(--bg-secondary);
}
.method-stat-card.active {
background: rgba(99, 102, 241, 0.15);
border: 1px solid var(--accent-primary);
}
.method-stat-name {
@@ -504,6 +515,11 @@ export class SwDashRequests extends LitElement {
// Local filtering - no HTTP request
}
private setMethodFilter(method: string): void {
// Toggle: clicking the same method clears the filter
this.methodFilter = this.methodFilter === method ? '' : method;
}
private handleSearch(e: Event): void {
this.searchText = (e.target as HTMLInputElement).value.toLowerCase();
}
@@ -546,6 +562,90 @@ export class SwDashRequests extends LitElement {
this.modalOpen = true;
}
private handleContextMenu(event: MouseEvent, group: IGroupedRequest): void {
// Build full message object for copying
const fullMessage = {
correlationId: group.correlationId,
method: group.method,
timestamp: group.timestamp,
durationMs: group.durationMs,
request: group.request ? {
direction: group.request.direction,
phase: group.request.phase,
timestamp: group.request.timestamp,
payload: group.request.payload,
} : null,
response: group.response ? {
direction: group.response.direction,
phase: group.response.phase,
timestamp: group.response.timestamp,
durationMs: group.response.durationMs,
payload: group.response.payload,
error: group.response.error,
} : null,
};
DeesContextmenu.openContextMenuWithOptions(event, [
{
name: 'Copy Full Message',
iconName: 'copy',
action: async () => {
await navigator.clipboard.writeText(JSON.stringify(fullMessage, null, 2));
},
},
{
name: 'Copy Request Payload',
iconName: 'upload',
disabled: !group.request,
action: async () => {
if (group.request) {
await navigator.clipboard.writeText(JSON.stringify(group.request.payload, null, 2));
}
},
},
{
name: 'Copy Response Payload',
iconName: 'download',
disabled: !group.response,
action: async () => {
if (group.response) {
await navigator.clipboard.writeText(JSON.stringify(group.response.payload, null, 2));
}
},
},
{ divider: true },
{
name: 'Copy Correlation ID',
iconName: 'hash',
action: async () => {
await navigator.clipboard.writeText(group.correlationId);
},
},
{
name: 'Copy Method Name',
iconName: 'tag',
action: async () => {
await navigator.clipboard.writeText(group.method);
},
},
{ divider: true },
{
name: 'Filter by Method',
iconName: 'filter',
action: async () => {
this.setMethodFilter(group.method);
},
},
{
name: 'Show Payload',
iconName: 'eye',
action: async () => {
this.openPayloadModal(group);
},
},
]);
}
private closeModal(): void {
this.modalOpen = false;
this.selectedGroup = null;
@@ -781,7 +881,10 @@ export class SwDashRequests extends LitElement {
<div class="method-stats-title">Methods</div>
<div class="method-stats-grid">
${Object.entries(this.stats.methodCounts).slice(0, 8).map(([method, data]) => html`
<div class="method-stat-card">
<div
class="method-stat-card ${this.methodFilter === method ? 'active' : ''}"
@click="${() => this.setMethodFilter(method)}"
>
<div class="method-stat-name" title="${method}">${method}</div>
<div class="method-stat-details">
<span>${data.requests} req</span>
@@ -813,9 +916,9 @@ export class SwDashRequests extends LitElement {
</select>
<span class="filter-label">Method:</span>
<select class="filter-select" @change="${this.handleMethodFilterChange}">
<select class="filter-select" .value="${this.methodFilter}" @change="${this.handleMethodFilterChange}">
<option value="">All Methods</option>
${this.methods.map(m => html`<option value="${m}">${m}</option>`)}
${this.methods.map(m => html`<option value="${m}" ?selected="${this.methodFilter === m}">${m}</option>`)}
</select>
<input
@@ -838,7 +941,10 @@ export class SwDashRequests extends LitElement {
` : html`
<div class="requests-list">
${groupedLogs.map(group => html`
<div class="request-card ${group.hasError ? 'has-error' : ''}">
<div
class="request-card ${group.hasError ? 'has-error' : ''}"
@contextmenu="${(e: MouseEvent) => this.handleContextMenu(e, group)}"
>
<div class="request-header">
<div>
<div class="request-badges">