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