feat(registry): Add hot-reload websocket, embedded UI bundling, and multi-platform Deno build tasks
Introduce a ReloadSocketManager and client ReloadService for automatic page reloads when the server restarts. Serve UI assets from an embedded generated file and add Deno tasks to bundle the UI and compile native binaries for multiple platforms. Also update dev watch workflow and ignore generated embedded UI file.
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@stack.gallery/registry',
|
||||
version: '1.0.1',
|
||||
version: '1.1.0',
|
||||
description: 'Enterprise-grade multi-protocol package registry'
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { initDb, closeDb, isDbConnected } from './models/db.ts';
|
||||
import { StackGalleryAuthProvider } from './providers/auth.provider.ts';
|
||||
import { StackGalleryStorageHooks } from './providers/storage.provider.ts';
|
||||
import { ApiRouter } from './api/router.ts';
|
||||
import { getEmbeddedFile } from './embedded-ui.generated.ts';
|
||||
import { ReloadSocketManager } from './reload-socket.ts';
|
||||
|
||||
export interface IRegistryConfig {
|
||||
// MongoDB configuration
|
||||
@@ -41,6 +43,7 @@ export class StackGalleryRegistry {
|
||||
private authProvider: StackGalleryAuthProvider | null = null;
|
||||
private storageHooks: StackGalleryStorageHooks | null = null;
|
||||
private apiRouter: ApiRouter | null = null;
|
||||
private reloadSocket: ReloadSocketManager | null = null;
|
||||
private isInitialized = false;
|
||||
|
||||
constructor(config: IRegistryConfig) {
|
||||
@@ -110,6 +113,9 @@ export class StackGalleryRegistry {
|
||||
this.apiRouter = new ApiRouter();
|
||||
console.log('[StackGalleryRegistry] API router initialized');
|
||||
|
||||
// Initialize reload socket for hot reload
|
||||
this.reloadSocket = new ReloadSocketManager();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('[StackGalleryRegistry] Initialization complete');
|
||||
}
|
||||
@@ -182,56 +188,40 @@ export class StackGalleryRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket upgrade for hot reload
|
||||
if (path === '/ws/reload' && request.headers.get('upgrade') === 'websocket') {
|
||||
return this.reloadSocket!.handleUpgrade(request);
|
||||
}
|
||||
|
||||
// Serve static UI files
|
||||
return await this.serveStaticFile(path);
|
||||
return this.serveStaticFile(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve static files from UI dist
|
||||
* Serve static files from embedded UI
|
||||
*/
|
||||
private async serveStaticFile(path: string): Promise<Response> {
|
||||
const uiDistPath = './ui/dist/registry-ui/browser';
|
||||
private serveStaticFile(path: string): Response {
|
||||
const filePath = path === '/' ? '/index.html' : path;
|
||||
|
||||
// Map path to file
|
||||
let filePath = path === '/' ? '/index.html' : path;
|
||||
|
||||
// Content type mapping
|
||||
const contentTypes: Record<string, string> = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
};
|
||||
|
||||
try {
|
||||
const fullPath = `${uiDistPath}${filePath}`;
|
||||
const file = await Deno.readFile(fullPath);
|
||||
const ext = filePath.substring(filePath.lastIndexOf('.'));
|
||||
const contentType = contentTypes[ext] || 'application/octet-stream';
|
||||
|
||||
return new Response(file, {
|
||||
// Get embedded file
|
||||
const embeddedFile = getEmbeddedFile(filePath);
|
||||
if (embeddedFile) {
|
||||
return new Response(embeddedFile.data, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': contentType },
|
||||
headers: { 'Content-Type': embeddedFile.contentType },
|
||||
});
|
||||
} catch {
|
||||
// For SPA routing, serve index.html for unknown paths
|
||||
try {
|
||||
const indexFile = await Deno.readFile(`${uiDistPath}/index.html`);
|
||||
return new Response(indexFile, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
} catch {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
// SPA fallback: serve index.html for unknown paths
|
||||
const indexFile = getEmbeddedFile('/index.html');
|
||||
if (indexFile) {
|
||||
return new Response(indexFile.data, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
65
ts/reload-socket.ts
Normal file
65
ts/reload-socket.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* WebSocket manager for hot reload
|
||||
* Generates a unique instance ID on startup and broadcasts it to connected clients.
|
||||
* When the server restarts, clients detect the new ID and reload the page.
|
||||
*/
|
||||
export class ReloadSocketManager {
|
||||
private instanceId: string;
|
||||
private clients: Set<WebSocket> = new Set();
|
||||
|
||||
constructor() {
|
||||
this.instanceId = crypto.randomUUID();
|
||||
console.log(`[ReloadSocket] Instance ID: ${this.instanceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current instance ID
|
||||
*/
|
||||
getInstanceId(): string {
|
||||
return this.instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebSocket upgrade request
|
||||
*/
|
||||
handleUpgrade(request: Request): Response {
|
||||
const { socket, response } = Deno.upgradeWebSocket(request);
|
||||
|
||||
socket.onopen = () => {
|
||||
this.clients.add(socket);
|
||||
console.log(`[ReloadSocket] Client connected (${this.clients.size} total)`);
|
||||
// Send instance ID immediately
|
||||
socket.send(JSON.stringify({ type: 'instance', id: this.instanceId }));
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
this.clients.delete(socket);
|
||||
console.log(`[ReloadSocket] Client disconnected (${this.clients.size} remaining)`);
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error('[ReloadSocket] WebSocket error:', error);
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to all connected clients
|
||||
*/
|
||||
broadcast(message: object): void {
|
||||
const msg = JSON.stringify(message);
|
||||
for (const client of this.clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of connected clients
|
||||
*/
|
||||
getClientCount(): number {
|
||||
return this.clients.size;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user