diff --git a/changelog.md b/changelog.md index 999a0db..4b084ca 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-12-04 - 7.7.0 - feat(typedserver) +Add SPA fallback support to TypedServer + +- Introduce new IServerOptions.spaFallback boolean to enable SPA routing fallback. +- When enabled, GET requests for paths without a file extension will serve serveDir/index.html. +- Preserves existing HTML injection behavior: injectReload still injects devtools script and typedserver metadata into when enabled. +- Responses from SPA fallback include Cache-Control: no-cache and appHash header for cache-busting; falls through to 404 on errors. +- Non-file routes that contain a dot (.) are not considered for SPA fallback to avoid interfering with asset requests. + ## 2025-12-04 - 7.6.0 - feat(typedserver) Remove legacy Express-based servertools, drop express deps, and refactor TypedServer to SmartServe + typedrouter with CORS support diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index a1bd978..5b922dc 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedserver', - version: '7.6.0', + version: '7.7.0', description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.' } diff --git a/ts/classes.typedserver.ts b/ts/classes.typedserver.ts index 5ace0bd..bace93b 100644 --- a/ts/classes.typedserver.ts +++ b/ts/classes.typedserver.ts @@ -56,6 +56,12 @@ export interface IServerOptions { feedMetadata?: plugins.smartfeed.IFeedOptions; articleGetterFunction?: () => Promise; blockWaybackMachine?: boolean; + + /** + * SPA fallback - serve index.html for non-file routes (e.g., /login, /dashboard) + * Useful for single-page applications with client-side routing + */ + spaFallback?: boolean; } export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL'; @@ -491,6 +497,41 @@ export class TypedServer { }); } + // SPA fallback - serve index.html for non-file routes + if (this.options.spaFallback && this.options.serveDir && method === 'GET' && !path.includes('.')) { + try { + const indexPath = plugins.path.join(this.options.serveDir, 'index.html'); + let html = await plugins.fsInstance.file(indexPath).encoding('utf8').read() as string; + + // Inject reload script if enabled + if (this.options.injectReload && html.includes('')) { + const injection = ` + + + + + `; + html = html.replace('', injection); + } + + return new Response(html, { + status: 200, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + appHash: this.serveHash, + }, + }); + } catch { + // Fall through to 404 + } + } + // Not found return new Response('Not Found', { status: 404 }); } diff --git a/ts_web_serviceworker/classes.serviceworker.ts b/ts_web_serviceworker/classes.serviceworker.ts index 294e10c..d330ffb 100644 --- a/ts_web_serviceworker/classes.serviceworker.ts +++ b/ts_web_serviceworker/classes.serviceworker.ts @@ -110,6 +110,13 @@ export class ServiceWorker { done.reject(error); } }); + + // Handle wake-up scenario: if already activated, connect immediately + // (install/activate events don't fire on wake-up) + if (selfArg.registration?.active) { + logger.log('info', 'SW woke up (already activated) - connecting to server'); + this.connectToServer(); + } } /** diff --git a/ts_web_serviceworker/env.ts b/ts_web_serviceworker/env.ts index 66216f2..18fa2e3 100644 --- a/ts_web_serviceworker/env.ts +++ b/ts_web_serviceworker/env.ts @@ -16,5 +16,6 @@ export interface ServiceWindow extends Window { location: any; skipWaiting: any; clients: any; + registration?: ServiceWorkerRegistration; } declare var self: Window; \ No newline at end of file