hightjs 0.2.42 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="CopilotDiffPersistence">
4
+ <option name="pendingDiffs">
5
+ <map>
6
+ <entry key="$PROJECT_DIR$/src/types/websocket.ts">
7
+ <value>
8
+ <PendingDiffInfo>
9
+ <option name="filePath" value="$PROJECT_DIR$/src/types/websocket.ts" />
10
+ <option name="updatedContent" value="import { IncomingMessage } from 'http';&#10;import { WebSocket } from 'ws';&#10;&#10;export interface WebSocketContext {&#10; ws: WebSocket;&#10; req: IncomingMessage;&#10; url: URL;&#10; params: Record&lt;string, string&gt;;&#10; query: Record&lt;string, string&gt;;&#10; send: (data: any) =&gt; void;&#10; close: (code?: number, reason?: string) =&gt; void;&#10; broadcast: (data: any, exclude?: WebSocket[]) =&gt; void;&#10;}&#10;&#10;export interface WebSocketHandler {&#10; (ctx: WebSocketContext): void | Promise&lt;void&gt;;&#10;}&#10;&#10;export interface WebSocketRoute {&#10; path: string;&#10; handler: WebSocketHandler;&#10; pathPattern?: RegExp;&#10; paramNames?: string[];&#10;}&#10;&#10;export interface WebSocketServer {&#10; route: (path: string, handler: WebSocketHandler) =&gt; void;&#10; start: (port?: number) =&gt; void;&#10; broadcast: (data: any) =&gt; void;&#10; getConnections: () =&gt; WebSocket[];&#10;}" />
11
+ </PendingDiffInfo>
12
+ </value>
13
+ </entry>
14
+ <entry key="$PROJECT_DIR$/src/websocket/adapter.ts">
15
+ <value>
16
+ <PendingDiffInfo>
17
+ <option name="filePath" value="$PROJECT_DIR$/src/websocket/adapter.ts" />
18
+ <option name="updatedContent" value="import { Server } from 'http';&#10;import { WebSocketServer as WSServer } from 'ws';&#10;import { HightWSServer } from './server';&#10;import { WebSocketServer, WebSocketHandler } from '../types/websocket';&#10;import Console, { Colors } from '../api/console';&#10;&#10;export class WebSocketAdapter {&#10; private wsServer: HightWSServer;&#10; private httpServer: Server | null = null;&#10; private wss: WSServer | null = null;&#10;&#10; constructor() {&#10; this.wsServer = new HightWSServer();&#10; }&#10;&#10; /**&#10; * Integra WebSocket com um servidor HTTP existente&#10; */&#10; attachToHttpServer(httpServer: Server): void {&#10; this.httpServer = httpServer;&#10; this.wss = new WSServer({ &#10; server: httpServer,&#10; perMessageDeflate: false &#10; });&#10;&#10; console.log(`${Colors.FgGreen}✓${Colors.Reset} WebSocket attached to HTTP server`);&#10;&#10; this.wss.on('connection', (ws, req) =&gt; {&#10; this.wsServer['handleConnection'](ws, req);&#10; });&#10;&#10; this.wss.on('error', (error) =&gt; {&#10; console.error(`${Colors.FgRed}[WebSocket Server Error]${Colors.Reset}`, error);&#10; });&#10; }&#10;&#10; /**&#10; * Adiciona uma rota WebSocket&#10; */&#10; route(path: string, handler: WebSocketHandler): void {&#10; this.wsServer.route(path, handler);&#10; }&#10;&#10; /**&#10; * Faz broadcast para todas as conexões&#10; */&#10; broadcast(data: any): void {&#10; this.wsServer.broadcast(data);&#10; }&#10;&#10; /**&#10; * Retorna todas as conexões ativas&#10; */&#10; getConnections() {&#10; return this.wsServer.getConnections();&#10; }&#10;&#10; /**&#10; * Inicia servidor WebSocket standalone&#10; */&#10; startStandalone(port: number = 8080): void {&#10; this.wsServer.start(port);&#10; }&#10;}" />
19
+ </PendingDiffInfo>
20
+ </value>
21
+ </entry>
22
+ <entry key="$PROJECT_DIR$/src/websocket/client.ts">
23
+ <value>
24
+ <PendingDiffInfo>
25
+ <option name="filePath" value="$PROJECT_DIR$/src/websocket/client.ts" />
26
+ <option name="originalContent" value="&#10;" />
27
+ <option name="updatedContent" value="import React from 'react';&#10;&#10;export class HightWSClient {&#10; private ws: WebSocket | null = null;&#10; private url: string;&#10; private reconnectAttempts: number = 0;&#10; private maxReconnectAttempts: number = 5;&#10; private reconnectDelay: number = 1000;&#10; private listeners: Map&lt;string, Function[]&gt; = new Map();&#10;&#10; constructor(url: string) {&#10; this.url = url;&#10; this.connect();&#10; }&#10;&#10; private connect(): void {&#10; try {&#10; this.ws = new WebSocket(this.url);&#10; &#10; this.ws.onopen = (event) =&gt; {&#10; console.log('[HightWS] Connected to WebSocket server');&#10; this.reconnectAttempts = 0;&#10; this.emit('open', event);&#10; };&#10;&#10; this.ws.onmessage = (event) =&gt; {&#10; try {&#10; const data = JSON.parse(event.data);&#10; this.emit('message', data);&#10; } catch (error) {&#10; // Se não for JSON, envia como string&#10; this.emit('message', event.data);&#10; }&#10; };&#10;&#10; this.ws.onclose = (event) =&gt; {&#10; console.log('[HightWS] Connection closed');&#10; this.emit('close', event);&#10; this.attemptReconnect();&#10; };&#10;&#10; this.ws.onerror = (event) =&gt; {&#10; console.error('[HightWS] WebSocket error:', event);&#10; this.emit('error', event);&#10; };&#10;&#10; } catch (error) {&#10; console.error('[HightWS] Failed to connect:', error);&#10; this.attemptReconnect();&#10; }&#10; }&#10;&#10; private attemptReconnect(): void {&#10; if (this.reconnectAttempts &lt; this.maxReconnectAttempts) {&#10; this.reconnectAttempts++;&#10; console.log(`[HightWS] Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);&#10; &#10; setTimeout(() =&gt; {&#10; this.connect();&#10; }, this.reconnectDelay * this.reconnectAttempts);&#10; } else {&#10; console.error('[HightWS] Max reconnection attempts reached');&#10; this.emit('maxReconnectAttemptsReached');&#10; }&#10; }&#10;&#10; public send(data: any): void {&#10; if (this.ws &amp;&amp; this.ws.readyState === WebSocket.OPEN) {&#10; const message = typeof data === 'string' ? data : JSON.stringify(data);&#10; this.ws.send(message);&#10; } else {&#10; console.warn('[HightWS] WebSocket is not connected');&#10; }&#10; }&#10;&#10; public on(event: string, callback: Function): void {&#10; if (!this.listeners.has(event)) {&#10; this.listeners.set(event, []);&#10; }&#10; this.listeners.get(event)!.push(callback);&#10; }&#10;&#10; public off(event: string, callback?: Function): void {&#10; if (!this.listeners.has(event)) return;&#10;&#10; if (callback) {&#10; const callbacks = this.listeners.get(event)!;&#10; const index = callbacks.indexOf(callback);&#10; if (index &gt; -1) {&#10; callbacks.splice(index, 1);&#10; }&#10; } else {&#10; this.listeners.delete(event);&#10; }&#10; }&#10;&#10; private emit(event: string, data?: any): void {&#10; if (this.listeners.has(event)) {&#10; this.listeners.get(event)!.forEach(callback =&gt; {&#10; try {&#10; callback(data);&#10; } catch (error) {&#10; console.error(`[HightWS] Error in event listener for ${event}:`, error);&#10; }&#10; });&#10; }&#10; }&#10;&#10; public close(): void {&#10; if (this.ws) {&#10; this.ws.close();&#10; this.ws = null;&#10; }&#10; }&#10;&#10; public get readyState(): number {&#10; return this.ws ? this.ws.readyState : WebSocket.CLOSED;&#10; }&#10;&#10; public get isConnected(): boolean {&#10; return this.ws !== null &amp;&amp; this.ws.readyState === WebSocket.OPEN;&#10; }&#10;}&#10;&#10;// Hook React para usar WebSocket&#10;export function useWebSocket(url: string) {&#10; const [client, setClient] = React.useState&lt;HightWSClient | null&gt;(null);&#10; const [isConnected, setIsConnected] = React.useState(false);&#10; const [lastMessage, setLastMessage] = React.useState&lt;any&gt;(null);&#10;&#10; React.useEffect(() =&gt; {&#10; const wsClient = new HightWSClient(url);&#10;&#10; wsClient.on('open', () =&gt; {&#10; setIsConnected(true);&#10; });&#10;&#10; wsClient.on('close', () =&gt; {&#10; setIsConnected(false);&#10; });&#10;&#10; wsClient.on('message', (data) =&gt; {&#10; setLastMessage(data);&#10; });&#10;&#10; setClient(wsClient);&#10;&#10; return () =&gt; {&#10; wsClient.close();&#10; };&#10; }, [url]);&#10;&#10; const sendMessage = React.useCallback((data: any) =&gt; {&#10; if (client) {&#10; client.send(data);&#10; }&#10; }, [client]);&#10;&#10; return {&#10; client,&#10; isConnected,&#10; lastMessage,&#10; sendMessage&#10; };&#10;}" />
28
+ </PendingDiffInfo>
29
+ </value>
30
+ </entry>
31
+ <entry key="$PROJECT_DIR$/src/websocket/example.ts">
32
+ <value>
33
+ <PendingDiffInfo>
34
+ <option name="filePath" value="$PROJECT_DIR$/src/websocket/example.ts" />
35
+ <option name="updatedContent" value="// Exemplo de uso do WebSocket no HightJS&#10;&#10;// ============================================&#10;// BACKEND - Como criar rotas WebSocket&#10;// ============================================&#10;&#10;// Em src/web/backend/websocket.ts (ou qualquer arquivo backend)&#10;import { ws } from 'hightjs/ws';&#10;&#10;// Rota simples de chat&#10;ws('/chat', (ctx) =&gt; {&#10; console.log('Nova conexão no chat:', ctx.req.socket.remoteAddress);&#10; &#10; // Envia mensagem de boas-vindas&#10; ctx.send({&#10; type: 'welcome',&#10; message: 'Bem-vindo ao chat!'&#10; });&#10;&#10; // Escuta mensagens do cliente&#10; ctx.ws.on('message', (data) =&gt; {&#10; try {&#10; const message = JSON.parse(data.toString());&#10; &#10; // Faz broadcast da mensagem para todos os outros clientes&#10; ctx.broadcast({&#10; type: 'message',&#10; user: message.user || 'Anônimo',&#10; text: message.text,&#10; timestamp: new Date().toISOString()&#10; }, [ctx.ws]); // Exclui o remetente&#10; &#10; } catch (error) {&#10; console.error('Erro ao processar mensagem:', error);&#10; }&#10; });&#10;&#10; // Quando a conexão fechar&#10; ctx.ws.on('close', () =&gt; {&#10; console.log('Cliente desconectado do chat');&#10; });&#10;});&#10;&#10;// Rota com parâmetros&#10;ws('/room/:roomId', (ctx) =&gt; {&#10; const { roomId } = ctx.params;&#10; console.log(`Nova conexão na sala: ${roomId}`);&#10; &#10; ctx.send({&#10; type: 'joined',&#10; room: roomId,&#10; message: `Você entrou na sala ${roomId}`&#10; });&#10;});&#10;&#10;// Rota para notificações em tempo real&#10;ws('/notifications/:userId', (ctx) =&gt; {&#10; const { userId } = ctx.params;&#10; &#10; // Pode usar query params também&#10; const token = ctx.query.token;&#10; &#10; if (!token) {&#10; ctx.close(1008, 'Token obrigatório');&#10; return;&#10; }&#10; &#10; console.log(`Usuário ${userId} conectado para notificações`);&#10; &#10; // Simula envio de notificação a cada 30 segundos&#10; const interval = setInterval(() =&gt; {&#10; ctx.send({&#10; type: 'notification',&#10; title: 'Nova notificação',&#10; body: 'Você tem uma nova mensagem',&#10; timestamp: new Date().toISOString()&#10; });&#10; }, 30000);&#10; &#10; ctx.ws.on('close', () =&gt; {&#10; clearInterval(interval);&#10; console.log(`Usuário ${userId} desconectado das notificações`);&#10; });&#10;});&#10;&#10;// ============================================&#10;// FRONTEND - Como usar WebSocket no React&#10;// ============================================&#10;&#10;// Em qualquer componente React&#10;import React from 'react';&#10;import { HightWSClient, useWebSocket } from 'hightjs/ws';&#10;&#10;// Método 1: Usando o hook useWebSocket (mais fácil)&#10;function ChatComponent() {&#10; const { client, isConnected, lastMessage, sendMessage } = useWebSocket('ws://localhost:8080/chat');&#10; const [messages, setMessages] = React.useState([]);&#10; const [newMessage, setNewMessage] = React.useState('');&#10;&#10; // Atualiza mensagens quando recebe uma nova&#10; React.useEffect(() =&gt; {&#10; if (lastMessage &amp;&amp; lastMessage.type === 'message') {&#10; setMessages(prev =&gt; [...prev, lastMessage]);&#10; }&#10; }, [lastMessage]);&#10;&#10; const handleSendMessage = () =&gt; {&#10; if (newMessage.trim() &amp;&amp; isConnected) {&#10; sendMessage({&#10; user: 'Seu Nome',&#10; text: newMessage&#10; });&#10; setNewMessage('');&#10; }&#10; };&#10;&#10; return (&#10; &lt;div&gt;&#10; &lt;div&gt;Status: {isConnected ? 'Conectado' : 'Desconectado'}&lt;/div&gt;&#10; &#10; &lt;div style={{ height: '300px', overflow: 'auto', border: '1px solid #ccc' }}&gt;&#10; {messages.map((msg, index) =&gt; (&#10; &lt;div key={index}&gt;&#10; &lt;strong&gt;{msg.user}:&lt;/strong&gt; {msg.text}&#10; &lt;small&gt; ({new Date(msg.timestamp).toLocaleTimeString()})&lt;/small&gt;&#10; &lt;/div&gt;&#10; ))}&#10; &lt;/div&gt;&#10; &#10; &lt;div&gt;&#10; &lt;input&#10; value={newMessage}&#10; onChange={(e) =&gt; setNewMessage(e.target.value)}&#10; onKeyPress={(e) =&gt; e.key === 'Enter' &amp;&amp; handleSendMessage()}&#10; placeholder=&quot;Digite sua mensagem...&quot;&#10; /&gt;&#10; &lt;button onClick={handleSendMessage} disabled={!isConnected}&gt;&#10; Enviar&#10; &lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; );&#10;}&#10;&#10;// Método 2: Usando a classe HightWSClient diretamente (mais controle)&#10;function NotificationsComponent() {&#10; const [client, setClient] = React.useState(null);&#10; const [notifications, setNotifications] = React.useState([]);&#10; const userId = '123'; // ID do usuário atual&#10;&#10; React.useEffect(() =&gt; {&#10; const wsClient = new HightWSClient(`ws://localhost:8080/notifications/${userId}?token=abc123`);&#10; &#10; wsClient.on('open', () =&gt; {&#10; console.log('Conectado às notificações');&#10; });&#10;&#10; wsClient.on('message', (data) =&gt; {&#10; if (data.type === 'notification') {&#10; setNotifications(prev =&gt; [data, ...prev.slice(0, 9)]); // Mantém só as 10 últimas&#10; &#10; // Pode mostrar notificação do browser também&#10; if (Notification.permission === 'granted') {&#10; new Notification(data.title, { body: data.body });&#10; }&#10; }&#10; });&#10;&#10; wsClient.on('close', () =&gt; {&#10; console.log('Desconectado das notificações');&#10; });&#10;&#10; setClient(wsClient);&#10;&#10; return () =&gt; {&#10; wsClient.close();&#10; };&#10; }, [userId]);&#10;&#10; return (&#10; &lt;div&gt;&#10; &lt;h3&gt;Notificações em tempo real&lt;/h3&gt;&#10; {notifications.map((notif, index) =&gt; (&#10; &lt;div key={index} style={{ padding: '10px', border: '1px solid #ddd', margin: '5px 0' }}&gt;&#10; &lt;strong&gt;{notif.title}&lt;/strong&gt;&#10; &lt;p&gt;{notif.body}&lt;/p&gt;&#10; &lt;small&gt;{new Date(notif.timestamp).toLocaleString()}&lt;/small&gt;&#10; &lt;/div&gt;&#10; ))}&#10; &lt;/div&gt;&#10; );&#10;}&#10;&#10;// ============================================&#10;// INICIANDO O SERVIDOR WEBSOCKET&#10;// ============================================&#10;&#10;// No seu arquivo principal de servidor (onde você inicia o HightJS)&#10;import { createWebSocketServer } from 'hightjs/ws';&#10;&#10;// Método 1: Servidor WebSocket independente&#10;const wsServer = createWebSocketServer();&#10;wsServer.start(8080); // Inicia na porta 8080&#10;&#10;// Método 2: Integrar com servidor HTTP existente (Express/Fastify)&#10;// Isso ainda precisa ser implementado nos adapters, mas a estrutura está pronta&#10;&#10;export default ChatComponent;" />
36
+ </PendingDiffInfo>
37
+ </value>
38
+ </entry>
39
+ <entry key="$PROJECT_DIR$/src/websocket/factory.ts">
40
+ <value>
41
+ <PendingDiffInfo>
42
+ <option name="filePath" value="$PROJECT_DIR$/src/websocket/factory.ts" />
43
+ <option name="updatedContent" value="import { HightWSServer } from './server';&#10;import { WebSocketServer } from '../types/websocket';&#10;&#10;/**&#10; * Cria uma nova instância do servidor WebSocket HightJS&#10; * @returns Uma nova instância do servidor WebSocket&#10; */&#10;export function createWebSocketServer(): WebSocketServer {&#10; return new HightWSServer();&#10;}&#10;&#10;/**&#10; * Instância global do servidor WebSocket (singleton)&#10; */&#10;let globalWSServer: WebSocketServer | null = null;&#10;&#10;/**&#10; * Retorna a instância global do servidor WebSocket ou cria uma nova&#10; * @returns A instância global do servidor WebSocket&#10; */&#10;export function getGlobalWebSocketServer(): WebSocketServer {&#10; if (!globalWSServer) {&#10; globalWSServer = new HightWSServer();&#10; }&#10; return globalWSServer;&#10;}&#10;&#10;/**&#10; * Helper para criar rotas WebSocket rapidamente&#10; * @param path Caminho da rota&#10; * @param handler Função handler da rota&#10; */&#10;export function ws(path: string, handler: (ctx: any) =&gt; void | Promise&lt;void&gt;) {&#10; const server = getGlobalWebSocketServer();&#10; server.route(path, handler);&#10; return server;&#10;}" />
44
+ </PendingDiffInfo>
45
+ </value>
46
+ </entry>
47
+ <entry key="$PROJECT_DIR$/src/websocket/index.ts">
48
+ <value>
49
+ <PendingDiffInfo>
50
+ <option name="filePath" value="$PROJECT_DIR$/src/websocket/index.ts" />
51
+ <option name="originalContent" value="&#10;&#10;" />
52
+ <option name="updatedContent" value="export { HightWSServer } from './server';&#10;export { WebSocketContext, WebSocketHandler, WebSocketRoute, WebSocketServer } from '../types/websocket';&#10;export { createWebSocketServer } from './factory';&#10;export { HightWSClient, useWebSocket } from './client';" />
53
+ </PendingDiffInfo>
54
+ </value>
55
+ </entry>
56
+ <entry key="$PROJECT_DIR$/src/websocket/server.ts">
57
+ <value>
58
+ <PendingDiffInfo>
59
+ <option name="filePath" value="$PROJECT_DIR$/src/websocket/server.ts" />
60
+ <option name="updatedContent" value="import { WebSocket, WebSocketServer as WSServer } from 'ws';&#10;import { IncomingMessage } from 'http';&#10;import { URL } from 'url';&#10;import { WebSocketContext, WebSocketHandler, WebSocketRoute, WebSocketServer } from '../types/websocket';&#10;import Console, { Colors } from '../api/console';&#10;&#10;export class HightWSServer implements WebSocketServer {&#10; private wss: WSServer | null = null;&#10; private routes: WebSocketRoute[] = [];&#10; private connections: Set&lt;WebSocket&gt; = new Set();&#10;&#10; constructor() {&#10; this.setupRoutePatterns();&#10; }&#10;&#10; route(path: string, handler: WebSocketHandler): void {&#10; const route: WebSocketRoute = {&#10; path,&#10; handler,&#10; ...this.createPathPattern(path)&#10; };&#10; this.routes.push(route);&#10; console.log(`${Colors.FgBlue}[WebSocket]${Colors.Reset} Route registered: ${Colors.FgYellow}${path}${Colors.Reset}`);&#10; }&#10;&#10; start(port: number = 8080): void {&#10; this.wss = new WSServer({ &#10; port,&#10; perMessageDeflate: false &#10; });&#10;&#10; console.log(`${Colors.FgGreen}✓${Colors.Reset} WebSocket server listening on port ${Colors.FgYellow}${port}${Colors.Reset}`);&#10;&#10; this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) =&gt; {&#10; this.connections.add(ws);&#10; console.log(`${Colors.FgBlue}[WebSocket]${Colors.Reset} New connection from ${req.socket.remoteAddress}`);&#10;&#10; ws.on('close', () =&gt; {&#10; this.connections.delete(ws);&#10; console.log(`${Colors.FgBlue}[WebSocket]${Colors.Reset} Connection closed`);&#10; });&#10;&#10; ws.on('error', (error) =&gt; {&#10; console.error(`${Colors.FgRed}[WebSocket Error]${Colors.Reset}`, error);&#10; this.connections.delete(ws);&#10; });&#10;&#10; this.handleConnection(ws, req);&#10; });&#10;&#10; this.wss.on('error', (error) =&gt; {&#10; console.error(`${Colors.FgRed}[WebSocket Server Error]${Colors.Reset}`, error);&#10; });&#10; }&#10;&#10; broadcast(data: any): void {&#10; const message = typeof data === 'string' ? data : JSON.stringify(data);&#10; this.connections.forEach(ws =&gt; {&#10; if (ws.readyState === WebSocket.OPEN) {&#10; ws.send(message);&#10; }&#10; });&#10; }&#10;&#10; getConnections(): WebSocket[] {&#10; return Array.from(this.connections);&#10; }&#10;&#10; private handleConnection(ws: WebSocket, req: IncomingMessage): void {&#10; if (!req.url) return;&#10;&#10; const url = new URL(req.url, `http://${req.headers.host}`);&#10; const pathname = url.pathname;&#10;&#10; // Encontra a rota correspondente&#10; const matchedRoute = this.findMatchingRoute(pathname);&#10; if (!matchedRoute) {&#10; console.log(`${Colors.FgYellow}[WebSocket]${Colors.Reset} No route found for ${pathname}`);&#10; ws.close(1000, 'No route found');&#10; return;&#10; }&#10;&#10; // Extrai parâmetros da URL&#10; const params = this.extractParams(pathname, matchedRoute);&#10; const query = Object.fromEntries(url.searchParams.entries());&#10;&#10; // Cria o contexto&#10; const context: WebSocketContext = {&#10; ws,&#10; req,&#10; url,&#10; params,&#10; query,&#10; send: (data: any) =&gt; {&#10; if (ws.readyState === WebSocket.OPEN) {&#10; const message = typeof data === 'string' ? data : JSON.stringify(data);&#10; ws.send(message);&#10; }&#10; },&#10; close: (code?: number, reason?: string) =&gt; {&#10; ws.close(code || 1000, reason);&#10; },&#10; broadcast: (data: any, exclude?: WebSocket[]) =&gt; {&#10; const message = typeof data === 'string' ? data : JSON.stringify(data);&#10; const excludeSet = new Set(exclude || []);&#10; this.connections.forEach(connection =&gt; {&#10; if (connection.readyState === WebSocket.OPEN &amp;&amp; !excludeSet.has(connection)) {&#10; connection.send(message);&#10; }&#10; });&#10; }&#10; };&#10;&#10; try {&#10; matchedRoute.handler(context);&#10; } catch (error) {&#10; console.error(`${Colors.FgRed}[WebSocket Handler Error]${Colors.Reset}`, error);&#10; ws.close(1011, 'Internal server error');&#10; }&#10; }&#10;&#10; private createPathPattern(path: string): { pathPattern: RegExp; paramNames: string[] } {&#10; const paramNames: string[] = [];&#10; const pattern = path&#10; .replace(/\//g, '\\/')&#10; .replace(/:([^\/]+)/g, (_, paramName) =&gt; {&#10; paramNames.push(paramName);&#10; return '([^/]+)';&#10; })&#10; .replace(/\*/g, '.*');&#10;&#10; return {&#10; pathPattern: new RegExp(`^${pattern}$`),&#10; paramNames&#10; };&#10; }&#10;&#10; private findMatchingRoute(pathname: string): WebSocketRoute | null {&#10; return this.routes.find(route =&gt; {&#10; if (route.pathPattern) {&#10; return route.pathPattern.test(pathname);&#10; }&#10; return route.path === pathname;&#10; }) || null;&#10; }&#10;&#10; private extractParams(pathname: string, route: WebSocketRoute): Record&lt;string, string&gt; {&#10; const params: Record&lt;string, string&gt; = {};&#10; &#10; if (route.pathPattern &amp;&amp; route.paramNames) {&#10; const match = pathname.match(route.pathPattern);&#10; if (match) {&#10; route.paramNames.forEach((paramName, index) =&gt; {&#10; params[paramName] = match[index + 1];&#10; });&#10; }&#10; }&#10;&#10; return params;&#10; }&#10;&#10; private setupRoutePatterns(): void {&#10; // Reprocessa todos os patterns das rotas existentes&#10; this.routes.forEach(route =&gt; {&#10; const { pathPattern, paramNames } = this.createPathPattern(route.path);&#10; route.pathPattern = pathPattern;&#10; route.paramNames = paramNames;&#10; });&#10; }&#10;}" />
61
+ </PendingDiffInfo>
62
+ </value>
63
+ </entry>
64
+ </map>
65
+ </option>
66
+ </component>
67
+ </project>
package/README.md CHANGED
@@ -23,6 +23,7 @@ Caso tenha alguma dúvida, entre em contato por uma das redes abaixo:
23
23
  - [📦 Estrutura Recomendada](#-estrutura-recomendada)
24
24
  - [🖥️ Rotas Frontend (Páginas)](#-rotas-frontend)
25
25
  - [🌐 Rotas Backend (API)](#-rotas-backend)
26
+ - [🛜 WebSocket](#-websocket)
26
27
  - [🧩 Middlewares](#-middlewares)
27
28
  - [🔐 Autenticação (HightJS/auth)](#-autenticação-hightjsauth)
28
29
  - [🛠️ CLI](#-cli)
@@ -38,6 +39,10 @@ Caso tenha alguma dúvida, entre em contato por uma das redes abaixo:
38
39
  ## ✨ Principais Recursos
39
40
 
40
41
  - **Roteamento automático** de páginas [`src/web/routes`] e APIs [`src/web/backend/routes`]
42
+ - **React 19** com client-side hydration
43
+ - **TypeScript** first (totalmente tipado)
44
+ - **WebSockets** nativo nas rotas backend
45
+ - **Rotas dinâmicas** com parâmetros (frontend e backend)
41
46
  - **Middlewares** por pasta ou rota
42
47
  - **Hot Reload** nativo (WebSocket interno) em dev
43
48
  - **Layouts globais** e página 404 customizada
@@ -257,6 +262,53 @@ export default route;
257
262
 
258
263
  ---
259
264
 
265
+ ## 🛜 WebSocket
266
+
267
+ O HightJS possui suporte nativo a WebSockets nas rotas do backend.
268
+
269
+ ### Exemplo Prático
270
+
271
+ `src/web/backend/routes/chat.ts`:
272
+
273
+ ```ts
274
+ import {BackendRouteConfig, HightJSResponse} from 'hightjs';
275
+
276
+ const route: BackendRouteConfig = {
277
+ pattern: '/api/chat',
278
+ GET: async () => {
279
+ return HightJSResponse.json({ message: 'Chat HTTP endpoint' });
280
+ },
281
+ WS: (ctx) => {
282
+ console.log('Nova conexão WebSocket no chat');
283
+
284
+ ctx.send({
285
+ type: 'welcome',
286
+ message: 'Bem-vindo ao chat!'
287
+ });
288
+
289
+ ctx.ws.on('message', (data) => {
290
+ const message = JSON.parse(data.toString());
291
+ console.log(message)
292
+ // Broadcast para todos exceto o remetente
293
+ ctx.broadcast({
294
+ type: 'message',
295
+ user: message.user,
296
+ text: message.text,
297
+ timestamp: new Date().toISOString()
298
+ });
299
+ });
300
+
301
+ ctx.ws.on('close', () => {
302
+ console.log('Cliente desconectado');
303
+ });
304
+ }
305
+ };
306
+
307
+ export default route;
308
+ ```
309
+
310
+ ---
311
+
260
312
  ## 🧩 Middlewares
261
313
 
262
314
  Adicione middlewares:
package/dist/auth/core.js CHANGED
@@ -55,7 +55,7 @@ class HWebAuth {
55
55
  const sessionResult = this.sessionManager.createSession(user);
56
56
  // Callback de sessão se definido
57
57
  if (this.config.callbacks?.session) {
58
- sessionResult.session = await this.config.callbacks.session(sessionResult.session, user);
58
+ sessionResult.session = await this.config.callbacks.session({ session: sessionResult.session, user, provider: providerId });
59
59
  }
60
60
  return sessionResult;
61
61
  }
@@ -86,7 +86,7 @@ class HWebAuth {
86
86
  .clearCookie('hweb-auth-token', {
87
87
  path: '/',
88
88
  httpOnly: true,
89
- secure: true,
89
+ secure: this.config.secureCookies || false,
90
90
  sameSite: 'strict'
91
91
  });
92
92
  }
@@ -143,7 +143,7 @@ class HWebAuth {
143
143
  .json(data)
144
144
  .cookie('hweb-auth-token', token, {
145
145
  httpOnly: true,
146
- secure: true, // Always secure, even in development
146
+ secure: this.config.secureCookies || false, // Always secure, even in development
147
147
  sameSite: 'strict', // Prevent CSRF attacks
148
148
  maxAge: (this.config.session?.maxAge || 86400) * 1000,
149
149
  path: '/',
@@ -3,5 +3,5 @@ export * from './providers';
3
3
  export * from './core';
4
4
  export * from './routes';
5
5
  export * from './jwt';
6
- export { CredentialsProvider, DiscordProvider } from './providers';
6
+ export { CredentialsProvider, DiscordProvider, GoogleProvider } from './providers';
7
7
  export { createAuthRoutes } from './routes';
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.createAuthRoutes = exports.DiscordProvider = exports.CredentialsProvider = void 0;
17
+ exports.createAuthRoutes = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
18
18
  // Exportações principais do sistema de autenticação
19
19
  __exportStar(require("./types"), exports);
20
20
  __exportStar(require("./providers"), exports);
@@ -24,5 +24,6 @@ __exportStar(require("./jwt"), exports);
24
24
  var providers_1 = require("./providers");
25
25
  Object.defineProperty(exports, "CredentialsProvider", { enumerable: true, get: function () { return providers_1.CredentialsProvider; } });
26
26
  Object.defineProperty(exports, "DiscordProvider", { enumerable: true, get: function () { return providers_1.DiscordProvider; } });
27
+ Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return providers_1.GoogleProvider; } });
27
28
  var routes_1 = require("./routes");
28
29
  Object.defineProperty(exports, "createAuthRoutes", { enumerable: true, get: function () { return routes_1.createAuthRoutes; } });
@@ -0,0 +1,63 @@
1
+ import type { AuthProviderClass, AuthRoute, User } from '../types';
2
+ export interface GoogleConfig {
3
+ id?: string;
4
+ name?: string;
5
+ clientId: string;
6
+ clientSecret: string;
7
+ callbackUrl?: string;
8
+ successUrl?: string;
9
+ scope?: string[];
10
+ }
11
+ /**
12
+ * Provider para autenticação com Google OAuth2
13
+ *
14
+ * Este provider permite autenticação usando Google OAuth2.
15
+ * Automaticamente gerencia o fluxo OAuth completo e rotas necessárias.
16
+ *
17
+ * Exemplo de uso:
18
+ * ```typescript
19
+ * new GoogleProvider({
20
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
21
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
22
+ * callbackUrl: "http://localhost:3000/api/auth/callback/google"
23
+ * })
24
+ * ```
25
+ *
26
+ * Fluxo de autenticação:
27
+ * 1. GET /api/auth/signin/google - Gera URL e redireciona para Google
28
+ * 2. Google redireciona para /api/auth/callback/google com código
29
+ * 3. Provider troca código por token e busca dados do usuário
30
+ * 4. Retorna objeto User com dados do Google
31
+ */
32
+ export declare class GoogleProvider implements AuthProviderClass {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly type: string;
36
+ private config;
37
+ private readonly defaultScope;
38
+ constructor(config: GoogleConfig);
39
+ /**
40
+ * Método para gerar URL OAuth (usado pelo handleSignIn)
41
+ */
42
+ handleOauth(credentials?: Record<string, string>): string;
43
+ /**
44
+ * Método principal - redireciona para OAuth ou processa o callback
45
+ */
46
+ handleSignIn(credentials: Record<string, string>): Promise<User | string | null>;
47
+ /**
48
+ * Processa o callback do OAuth (troca o código pelo usuário)
49
+ */
50
+ private processOAuthCallback;
51
+ /**
52
+ * Rotas adicionais específicas do Google OAuth
53
+ */
54
+ additionalRoutes: AuthRoute[];
55
+ /**
56
+ * Gera a URL de autorização do Google
57
+ */
58
+ getAuthorizationUrl(): string;
59
+ /**
60
+ * Retorna a configuração pública do provider
61
+ */
62
+ getConfig(): any;
63
+ }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GoogleProvider = void 0;
4
+ const http_1 = require("../../api/http");
5
+ /**
6
+ * Provider para autenticação com Google OAuth2
7
+ *
8
+ * Este provider permite autenticação usando Google OAuth2.
9
+ * Automaticamente gerencia o fluxo OAuth completo e rotas necessárias.
10
+ *
11
+ * Exemplo de uso:
12
+ * ```typescript
13
+ * new GoogleProvider({
14
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
15
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
16
+ * callbackUrl: "http://localhost:3000/api/auth/callback/google"
17
+ * })
18
+ * ```
19
+ *
20
+ * Fluxo de autenticação:
21
+ * 1. GET /api/auth/signin/google - Gera URL e redireciona para Google
22
+ * 2. Google redireciona para /api/auth/callback/google com código
23
+ * 3. Provider troca código por token e busca dados do usuário
24
+ * 4. Retorna objeto User com dados do Google
25
+ */
26
+ class GoogleProvider {
27
+ constructor(config) {
28
+ this.type = 'google';
29
+ this.defaultScope = [
30
+ 'openid',
31
+ 'https://www.googleapis.com/auth/userinfo.email',
32
+ 'https://www.googleapis.com/auth/userinfo.profile'
33
+ ];
34
+ /**
35
+ * Rotas adicionais específicas do Google OAuth
36
+ */
37
+ this.additionalRoutes = [
38
+ // Rota de callback do Google
39
+ {
40
+ method: 'GET',
41
+ path: '/api/auth/callback/google',
42
+ handler: async (req, params) => {
43
+ const url = new URL(req.url || '', 'http://localhost');
44
+ const code = url.searchParams.get('code');
45
+ if (!code) {
46
+ return http_1.HightJSResponse.json({ error: 'Authorization code not provided' }, { status: 400 });
47
+ }
48
+ try {
49
+ // Delega o 'code' para o endpoint de signin principal
50
+ const authResponse = await fetch(`${req.headers.origin || 'http://localhost:3000'}/api/auth/signin`, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ body: JSON.stringify({
56
+ provider: this.id,
57
+ code,
58
+ })
59
+ });
60
+ if (authResponse.ok) {
61
+ // Propaga o cookie de sessão e redireciona para a URL de sucesso
62
+ const setCookieHeader = authResponse.headers.get('set-cookie');
63
+ if (this.config.successUrl) {
64
+ return http_1.HightJSResponse
65
+ .redirect(this.config.successUrl)
66
+ .header('Set-Cookie', setCookieHeader || '');
67
+ }
68
+ return http_1.HightJSResponse.json({ success: true })
69
+ .header('Set-Cookie', setCookieHeader || '');
70
+ }
71
+ else {
72
+ const errorText = await authResponse.text();
73
+ console.error(`[${this.id} Provider] Session creation failed during callback. Status: ${authResponse.status}, Body: ${errorText}`);
74
+ return http_1.HightJSResponse.json({ error: 'Session creation failed' }, { status: 500 });
75
+ }
76
+ }
77
+ catch (error) {
78
+ console.error(`[${this.id} Provider] Callback handler fetch error:`, error);
79
+ return http_1.HightJSResponse.json({ error: 'Internal server error' }, { status: 500 });
80
+ }
81
+ }
82
+ }
83
+ ];
84
+ this.config = config;
85
+ this.id = config.id || 'google';
86
+ this.name = config.name || 'Google';
87
+ }
88
+ /**
89
+ * Método para gerar URL OAuth (usado pelo handleSignIn)
90
+ */
91
+ handleOauth(credentials = {}) {
92
+ return this.getAuthorizationUrl();
93
+ }
94
+ /**
95
+ * Método principal - redireciona para OAuth ou processa o callback
96
+ */
97
+ async handleSignIn(credentials) {
98
+ // Se tem código, é o callback - processa a autenticação
99
+ if (credentials.code) {
100
+ return await this.processOAuthCallback(credentials);
101
+ }
102
+ // Se não tem código, é o início do OAuth - retorna a URL
103
+ return this.handleOauth(credentials);
104
+ }
105
+ /**
106
+ * Processa o callback do OAuth (troca o código pelo usuário)
107
+ */
108
+ async processOAuthCallback(credentials) {
109
+ try {
110
+ const { code } = credentials;
111
+ if (!code) {
112
+ throw new Error('Authorization code not provided');
113
+ }
114
+ // Troca o código por um access token
115
+ const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Content-Type': 'application/x-www-form-urlencoded',
119
+ },
120
+ body: new URLSearchParams({
121
+ client_id: this.config.clientId,
122
+ client_secret: this.config.clientSecret,
123
+ grant_type: 'authorization_code',
124
+ code,
125
+ redirect_uri: this.config.callbackUrl || '',
126
+ }),
127
+ });
128
+ if (!tokenResponse.ok) {
129
+ const error = await tokenResponse.text();
130
+ throw new Error(`Failed to exchange code for token: ${error}`);
131
+ }
132
+ const tokens = await tokenResponse.json();
133
+ // Busca os dados do usuário com o access token
134
+ const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
135
+ headers: {
136
+ 'Authorization': `Bearer ${tokens.access_token}`,
137
+ },
138
+ });
139
+ if (!userResponse.ok) {
140
+ throw new Error('Failed to fetch user data');
141
+ }
142
+ const googleUser = await userResponse.json();
143
+ // Retorna o objeto User padronizado
144
+ return {
145
+ id: googleUser.id,
146
+ name: googleUser.name,
147
+ email: googleUser.email,
148
+ image: googleUser.picture || null,
149
+ provider: this.id,
150
+ providerId: googleUser.id,
151
+ accessToken: tokens.access_token,
152
+ refreshToken: tokens.refresh_token
153
+ };
154
+ }
155
+ catch (error) {
156
+ console.error(`[${this.id} Provider] Error during OAuth callback:`, error);
157
+ return null;
158
+ }
159
+ }
160
+ /**
161
+ * Gera a URL de autorização do Google
162
+ */
163
+ getAuthorizationUrl() {
164
+ const params = new URLSearchParams({
165
+ client_id: this.config.clientId,
166
+ redirect_uri: this.config.callbackUrl || '',
167
+ response_type: 'code',
168
+ scope: (this.config.scope || this.defaultScope).join(' ')
169
+ });
170
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
171
+ }
172
+ /**
173
+ * Retorna a configuração pública do provider
174
+ */
175
+ getConfig() {
176
+ return {
177
+ id: this.id,
178
+ name: this.name,
179
+ type: this.type,
180
+ clientId: this.config.clientId, // Público
181
+ scope: this.config.scope || this.defaultScope,
182
+ callbackUrl: this.config.callbackUrl
183
+ };
184
+ }
185
+ }
186
+ exports.GoogleProvider = GoogleProvider;
@@ -1,2 +1,3 @@
1
1
  export { CredentialsProvider } from './providers/credentials';
2
2
  export { DiscordProvider } from './providers/discord';
3
+ export { GoogleProvider } from './providers/google';
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DiscordProvider = exports.CredentialsProvider = void 0;
3
+ exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
4
4
  // Exportações dos providers
5
5
  var credentials_1 = require("./providers/credentials");
6
6
  Object.defineProperty(exports, "CredentialsProvider", { enumerable: true, get: function () { return credentials_1.CredentialsProvider; } });
7
7
  var discord_1 = require("./providers/discord");
8
8
  Object.defineProperty(exports, "DiscordProvider", { enumerable: true, get: function () { return discord_1.DiscordProvider; } });
9
+ var google_1 = require("./providers/google");
10
+ Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return google_1.GoogleProvider; } });
@@ -48,7 +48,11 @@ export interface AuthConfig {
48
48
  };
49
49
  callbacks?: {
50
50
  signIn?: (user: User, account: any, profile: any) => boolean | Promise<boolean>;
51
- session?: (session: Session, user: User) => Session | Promise<Session>;
51
+ session?: ({ session, user, provider }: {
52
+ session: Session;
53
+ user: User;
54
+ provider: string;
55
+ }) => Session | Promise<Session>;
52
56
  jwt?: (token: any, user: User, account: any, profile: any) => any | Promise<any>;
53
57
  };
54
58
  session?: {
@@ -58,12 +62,7 @@ export interface AuthConfig {
58
62
  };
59
63
  secret?: string;
60
64
  debug?: boolean;
61
- }
62
- export interface AuthProvider {
63
- id: string;
64
- name: string;
65
- type: 'credentials';
66
- authorize?: (credentials: Record<string, string>) => Promise<User | null> | User | null;
65
+ secureCookies?: boolean;
67
66
  }
68
67
  export interface CredentialsConfig {
69
68
  id?: string;
@@ -49,7 +49,17 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
49
49
  const [params, setParams] = (0, react_1.useState)(initialParams);
50
50
  const findRouteForPath = (0, react_1.useCallback)((path) => {
51
51
  for (const route of routes) {
52
- const regexPattern = route.pattern.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
52
+ const regexPattern = route.pattern
53
+ // [[...param]] → opcional catch-all
54
+ .replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
55
+ // [...param] → obrigatório catch-all
56
+ .replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
57
+ // /[[param]] → opcional com barra também opcional
58
+ .replace(/\/\[\[(\w+)\]\]/g, '(?:/(?<$1>[^/]+))?')
59
+ // [[param]] → segmento opcional (sem barra anterior)
60
+ .replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
61
+ // [param] → segmento obrigatório
62
+ .replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
53
63
  const regex = new RegExp(`^${regexPattern}/?$`);
54
64
  const match = path.match(regex);
55
65
  if (match) {
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -4,14 +4,19 @@ export declare class HotReloadManager {
4
4
  private watchers;
5
5
  private projectDir;
6
6
  private clients;
7
- private pingInterval;
8
7
  private backendApiChangeCallback;
9
8
  private frontendChangeCallback;
9
+ private isShuttingDown;
10
+ private debounceTimers;
11
+ private customHotReloadListener;
10
12
  constructor(projectDir: string);
11
13
  start(): Promise<void>;
12
14
  handleUpgrade(request: IncomingMessage, socket: any, head: Buffer): void;
13
15
  private setupWebSocketServer;
16
+ private cleanupClient;
14
17
  private setupWatchers;
18
+ private debounce;
19
+ private handleAnySrcChange;
15
20
  private notifyClients;
16
21
  private restartServer;
17
22
  stop(): void;
@@ -19,5 +24,7 @@ export declare class HotReloadManager {
19
24
  private clearBackendCache;
20
25
  onBackendApiChange(callback: () => void): void;
21
26
  onFrontendChange(callback: () => void): void;
27
+ setHotReloadListener(listener: (file: string) => Promise<void> | void): void;
28
+ removeHotReloadListener(): void;
22
29
  private checkFrontendBuild;
23
30
  }