nyte 1.0.0 → 1.1.0
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.
- package/dist/client/entry.client.d.ts +1 -1
- package/dist/client/entry.client.js +65 -176
- package/dist/hotReload.js +3 -2
- package/dist/rpc/annotations.d.ts +13 -0
- package/dist/rpc/annotations.js +22 -0
- package/dist/rpc/server.js +18 -2
- package/package.json +6 -1
- package/src/client/entry.client.tsx +77 -206
- package/src/hotReload.ts +3 -2
- package/src/rpc/annotations.ts +30 -0
- package/src/rpc/server.ts +22 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare function DevIndicator(): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DevIndicator = DevIndicator;
|
|
36
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
37
38
|
/*
|
|
38
39
|
* This file is part of the Nyte.js Project.
|
|
@@ -214,184 +215,72 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
|
|
|
214
215
|
// Adiciona o indicador de dev se não for produção
|
|
215
216
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [content, process.env.NODE_ENV !== 'production' && (0, jsx_runtime_1.jsx)(DevIndicator, {})] }));
|
|
216
217
|
}
|
|
217
|
-
// --- Constantes de Configuração ---
|
|
218
|
-
const DEV_INDICATOR_SIZE = 48;
|
|
219
|
-
const DEV_INDICATOR_CORNERS = [
|
|
220
|
-
{ top: 16, left: 16 }, // 0: topo-esquerda
|
|
221
|
-
{ top: 16, right: 16 }, // 1: topo-direita
|
|
222
|
-
{ bottom: 16, left: 16 }, // 2: baixo-esquerda
|
|
223
|
-
{ bottom: 16, right: 16 }, // 3: baixo-direita
|
|
224
|
-
];
|
|
225
218
|
function DevIndicator() {
|
|
226
|
-
const [
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Posição visual do indicador durante o arraste
|
|
230
|
-
const [position, setPosition] = (0, react_1.useState)({ top: 0, left: 0 });
|
|
231
|
-
const indicatorRef = (0, react_1.useRef)(null);
|
|
232
|
-
const dragStartRef = (0, react_1.useRef)(null);
|
|
233
|
-
// Escuta eventos de hot reload para mostrar estado de build
|
|
234
|
-
(0, react_1.useEffect)(() => {
|
|
235
|
-
if (typeof window === 'undefined')
|
|
236
|
-
return;
|
|
237
|
-
const handleHotReloadMessage = (event) => {
|
|
238
|
-
try {
|
|
239
|
-
const message = JSON.parse(event.data);
|
|
240
|
-
// Quando detecta mudança em arquivo, ativa loading
|
|
241
|
-
if (message.type === 'frontend-reload' ||
|
|
242
|
-
message.type === 'backend-api-reload' ||
|
|
243
|
-
message.type === 'src-reload') {
|
|
244
|
-
setIsBuilding(true);
|
|
245
|
-
}
|
|
246
|
-
// Quando o build termina ou servidor fica pronto, desativa loading
|
|
247
|
-
if (message.type === 'server-ready' || message.type === 'build-complete') {
|
|
248
|
-
setIsBuilding(false);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
catch (e) {
|
|
252
|
-
// Ignora mensagens que não são JSON
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
// Intercepta mensagens WebSocket
|
|
256
|
-
const originalWebSocket = window.WebSocket;
|
|
257
|
-
window.WebSocket = class extends originalWebSocket {
|
|
258
|
-
constructor(url, protocols) {
|
|
259
|
-
super(url, protocols);
|
|
260
|
-
this.addEventListener('message', (event) => {
|
|
261
|
-
if (url.toString().includes('hweb-hotreload')) {
|
|
262
|
-
handleHotReloadMessage(event);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
return () => {
|
|
268
|
-
window.WebSocket = originalWebSocket;
|
|
269
|
-
};
|
|
270
|
-
}, []);
|
|
271
|
-
// --- Estilos Dinâmicos ---
|
|
272
|
-
const getIndicatorStyle = () => {
|
|
273
|
-
const baseStyle = {
|
|
274
|
-
position: 'fixed',
|
|
275
|
-
zIndex: 9999,
|
|
276
|
-
width: DEV_INDICATOR_SIZE,
|
|
277
|
-
height: DEV_INDICATOR_SIZE,
|
|
278
|
-
borderRadius: '50%',
|
|
279
|
-
background: isBuilding
|
|
280
|
-
? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
|
|
281
|
-
: 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
|
|
282
|
-
color: 'white',
|
|
283
|
-
fontWeight: 'bold',
|
|
284
|
-
fontSize: 28,
|
|
285
|
-
boxShadow: isBuilding
|
|
286
|
-
? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
|
|
287
|
-
: '0 4px 15px rgba(0,0,0,0.2)',
|
|
288
|
-
display: 'flex',
|
|
289
|
-
alignItems: 'center',
|
|
290
|
-
justifyContent: 'center',
|
|
291
|
-
cursor: isDragging ? 'grabbing' : 'grab',
|
|
292
|
-
userSelect: 'none',
|
|
293
|
-
transition: isDragging ? 'none' : 'all 0.3s ease-out',
|
|
294
|
-
animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
|
|
295
|
-
};
|
|
296
|
-
if (isDragging) {
|
|
297
|
-
return {
|
|
298
|
-
...baseStyle,
|
|
299
|
-
top: position.top,
|
|
300
|
-
left: position.left,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
return { ...baseStyle, ...DEV_INDICATOR_CORNERS[corner] };
|
|
304
|
-
};
|
|
305
|
-
const getMenuPositionStyle = () => {
|
|
306
|
-
// Posiciona o menu dependendo do canto
|
|
307
|
-
switch (corner) {
|
|
308
|
-
case 0: return { top: '110%', left: '0' }; // Top-Left
|
|
309
|
-
case 1: return { top: '110%', right: '0' }; // Top-Right
|
|
310
|
-
case 2: return { bottom: '110%', left: '0' }; // Bottom-Left
|
|
311
|
-
case 3: return { bottom: '110%', right: '0' }; // Bottom-Right
|
|
312
|
-
default: return {};
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
// --- Lógica de Eventos ---
|
|
316
|
-
const handleMouseDown = (e) => {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
dragStartRef.current = { x: e.clientX, y: e.clientY, moved: false };
|
|
319
|
-
if (indicatorRef.current) {
|
|
320
|
-
const rect = indicatorRef.current.getBoundingClientRect();
|
|
321
|
-
setPosition({ top: rect.top, left: rect.left });
|
|
322
|
-
}
|
|
323
|
-
setIsDragging(true);
|
|
324
|
-
};
|
|
325
|
-
const handleMouseMove = (0, react_1.useCallback)((e) => {
|
|
326
|
-
if (!isDragging || !dragStartRef.current)
|
|
327
|
-
return;
|
|
328
|
-
const deltaX = e.clientX - dragStartRef.current.x;
|
|
329
|
-
const deltaY = e.clientY - dragStartRef.current.y;
|
|
330
|
-
// Diferencia clique de arrastar (threshold de 5px)
|
|
331
|
-
if (!dragStartRef.current.moved && Math.hypot(deltaX, deltaY) > 5) {
|
|
332
|
-
dragStartRef.current.moved = true;
|
|
333
|
-
}
|
|
334
|
-
if (dragStartRef.current.moved) {
|
|
335
|
-
setPosition(prevPos => ({
|
|
336
|
-
top: prevPos.top + deltaY,
|
|
337
|
-
left: prevPos.left + deltaX,
|
|
338
|
-
}));
|
|
339
|
-
// Atualiza a referência para o próximo movimento
|
|
340
|
-
dragStartRef.current.x = e.clientX;
|
|
341
|
-
dragStartRef.current.y = e.clientY;
|
|
342
|
-
}
|
|
343
|
-
}, [isDragging]);
|
|
344
|
-
const handleMouseUp = (0, react_1.useCallback)((e) => {
|
|
345
|
-
if (!isDragging)
|
|
346
|
-
return;
|
|
347
|
-
setIsDragging(false);
|
|
348
|
-
// Se moveu, calcula o canto mais próximo
|
|
349
|
-
if (dragStartRef.current?.moved) {
|
|
350
|
-
const { clientX, clientY } = e;
|
|
351
|
-
const w = window.innerWidth;
|
|
352
|
-
const h = window.innerHeight;
|
|
353
|
-
const dists = [
|
|
354
|
-
Math.hypot(clientX, clientY), // TL
|
|
355
|
-
Math.hypot(w - clientX, clientY), // TR
|
|
356
|
-
Math.hypot(clientX, h - clientY), // BL
|
|
357
|
-
Math.hypot(w - clientX, h - clientY), // BR
|
|
358
|
-
];
|
|
359
|
-
setCorner(dists.indexOf(Math.min(...dists)));
|
|
360
|
-
}
|
|
361
|
-
dragStartRef.current = null;
|
|
362
|
-
}, [isDragging]);
|
|
363
|
-
// Adiciona e remove listeners globais
|
|
364
|
-
(0, react_1.useEffect)(() => {
|
|
365
|
-
if (isDragging) {
|
|
366
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
367
|
-
window.addEventListener('mouseup', handleMouseUp);
|
|
368
|
-
}
|
|
369
|
-
return () => {
|
|
370
|
-
window.removeEventListener('mousemove', handleMouseMove);
|
|
371
|
-
window.removeEventListener('mouseup', handleMouseUp);
|
|
372
|
-
};
|
|
373
|
-
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
219
|
+
const [isVisible, setIsVisible] = (0, react_1.useState)(true);
|
|
220
|
+
if (!isVisible)
|
|
221
|
+
return null;
|
|
374
222
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { children: `
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
223
|
+
@keyframes nyte-pulse {
|
|
224
|
+
0% { opacity: 0.4; }
|
|
225
|
+
50% { opacity: 1; }
|
|
226
|
+
100% { opacity: 0.4; }
|
|
227
|
+
}
|
|
228
|
+
.nyte-dev-badge {
|
|
229
|
+
position: fixed;
|
|
230
|
+
bottom: 20px;
|
|
231
|
+
right: 20px;
|
|
232
|
+
z-index: 999999;
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 12px;
|
|
236
|
+
padding: 8px 14px;
|
|
237
|
+
background: rgba(15, 15, 20, 0.8);
|
|
238
|
+
backdrop-filter: blur(12px);
|
|
239
|
+
-webkit-backdrop-filter: blur(12px);
|
|
240
|
+
|
|
241
|
+
border-radius: 10px;
|
|
242
|
+
color: #fff;
|
|
243
|
+
font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
letter-spacing: 0.05em;
|
|
247
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
248
|
+
transition: all 0.2s ease;
|
|
249
|
+
cursor: default;
|
|
250
|
+
user-select: none;
|
|
251
|
+
}
|
|
252
|
+
.nyte-dev-badge:hover {
|
|
253
|
+
border-color: rgba(142, 45, 226, 0.5);
|
|
254
|
+
transform: translateY(-2px);
|
|
255
|
+
}
|
|
256
|
+
.nyte-status-dot {
|
|
257
|
+
width: 8px;
|
|
258
|
+
height: 8px;
|
|
259
|
+
background: #10b981; /* Verde esmeralda */
|
|
260
|
+
border-radius: 50%;
|
|
261
|
+
box-shadow: 0 0 10px #10b981;
|
|
262
|
+
animation: nyte-pulse 2s infinite ease-in-out;
|
|
263
|
+
}
|
|
264
|
+
.nyte-label {
|
|
265
|
+
color: rgba(255, 255, 255, 0.5);
|
|
266
|
+
text-transform: uppercase;
|
|
267
|
+
font-size: 10px;
|
|
268
|
+
}
|
|
269
|
+
.nyte-logo {
|
|
270
|
+
background: linear-gradient(135deg, #00a3a3, #808080);
|
|
271
|
+
-webkit-background-clip: text;
|
|
272
|
+
-webkit-text-fill-color: transparent;
|
|
273
|
+
font-weight: 800;
|
|
274
|
+
}
|
|
275
|
+
` }), (0, jsx_runtime_1.jsxs)("div", { className: "nyte-dev-badge", children: [(0, jsx_runtime_1.jsx)("div", { className: "nyte-status-dot" }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("span", { className: "nyte-logo", children: "NYTE.JS" }) }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setIsVisible(false), style: {
|
|
276
|
+
background: 'none',
|
|
277
|
+
border: 'none',
|
|
278
|
+
color: 'rgba(255,255,255,0.3)',
|
|
279
|
+
cursor: 'pointer',
|
|
280
|
+
fontSize: '14px',
|
|
281
|
+
padding: '0 0 0 4px',
|
|
282
|
+
marginLeft: '4px'
|
|
283
|
+
}, title: "Fechar", children: "\u00D7" })] })] }));
|
|
395
284
|
}
|
|
396
285
|
// --- Inicialização do Cliente (CSR - Client-Side Rendering) ---
|
|
397
286
|
function deobfuscateData(obfuscated) {
|
package/dist/hotReload.js
CHANGED
|
@@ -213,6 +213,7 @@ class HotReloadManager {
|
|
|
213
213
|
});
|
|
214
214
|
try {
|
|
215
215
|
this.frontendChangeCallback?.();
|
|
216
|
+
this.backendApiChangeCallback?.();
|
|
216
217
|
await Promise.race([buildPromise, timeoutPromise]);
|
|
217
218
|
dm.end(`Build complete for ${path.basename(filePath)}, reloading frontend.`);
|
|
218
219
|
this.frontendChangeCallback?.();
|
|
@@ -230,13 +231,13 @@ class HotReloadManager {
|
|
|
230
231
|
}
|
|
231
232
|
// Se for arquivo de backend, recarrega o módulo e notifica
|
|
232
233
|
if (isBackendFile) {
|
|
233
|
-
console_1.default.logWithout(console_1.Levels.INFO,
|
|
234
|
+
console_1.default.logWithout(console_1.Levels.INFO, undefined, `Reloading backend...`);
|
|
234
235
|
this.backendApiChangeCallback?.();
|
|
235
236
|
this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
|
|
236
237
|
}
|
|
237
238
|
// Fallback: se não for nem frontend nem backend detectado, recarrega tudo
|
|
238
239
|
if (!isFrontendFile && !isBackendFile) {
|
|
239
|
-
console_1.default.logWithout(console_1.Levels.INFO,
|
|
240
|
+
console_1.default.logWithout(console_1.Levels.INFO, undefined, `Reloading application...`);
|
|
240
241
|
this.frontendChangeCallback?.();
|
|
241
242
|
this.backendApiChangeCallback?.();
|
|
242
243
|
this.notifyClients('src-reload', { file: filePath, event: 'change' });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Símbolo único para marcar funções expostas.
|
|
3
|
+
* Usamos Symbol para garantir que não possa ser falsificado via JSON no payload.
|
|
4
|
+
*/
|
|
5
|
+
export declare const RPC_EXPOSED_KEY: unique symbol;
|
|
6
|
+
type AnyFn = (...args: any[]) => any;
|
|
7
|
+
/**
|
|
8
|
+
* Marca uma ou mais funções como seguras para RPC.
|
|
9
|
+
*/
|
|
10
|
+
export default function Expose<T extends AnyFn>(fn: T): T;
|
|
11
|
+
export default function Expose<T extends AnyFn[]>(fns: [...T]): T;
|
|
12
|
+
export default function Expose<T extends AnyFn[]>(...fns: T): T;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RPC_EXPOSED_KEY = void 0;
|
|
4
|
+
exports.default = Expose;
|
|
5
|
+
/**
|
|
6
|
+
* Símbolo único para marcar funções expostas.
|
|
7
|
+
* Usamos Symbol para garantir que não possa ser falsificado via JSON no payload.
|
|
8
|
+
*/
|
|
9
|
+
exports.RPC_EXPOSED_KEY = Symbol('__rpc_exposed__');
|
|
10
|
+
function Expose(...input) {
|
|
11
|
+
const fns = Array.isArray(input[0]) ? input[0] : input;
|
|
12
|
+
for (const fn of fns) {
|
|
13
|
+
if (typeof fn !== 'function') {
|
|
14
|
+
throw new TypeError('Expose aceita apenas funções');
|
|
15
|
+
}
|
|
16
|
+
fn[exports.RPC_EXPOSED_KEY] = true;
|
|
17
|
+
}
|
|
18
|
+
// Retorno:
|
|
19
|
+
// - se veio uma função → retorna ela
|
|
20
|
+
// - se veio lista → retorna a lista
|
|
21
|
+
return input.length === 1 ? input[0] : input;
|
|
22
|
+
}
|
package/dist/rpc/server.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* you may not use this file except in compliance with the License.
|
|
8
8
|
* You may obtain a copy of the License at
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
11
|
*
|
|
12
12
|
* Unless required by applicable law or agreed to in writing, software
|
|
13
13
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
@@ -23,6 +23,8 @@ exports.executeRpc = executeRpc;
|
|
|
23
23
|
const fs_1 = __importDefault(require("fs"));
|
|
24
24
|
const path_1 = __importDefault(require("path"));
|
|
25
25
|
const http_1 = require("../api/http");
|
|
26
|
+
// Importamos a chave para verificar a anotação de segurança
|
|
27
|
+
const annotations_1 = require("./annotations");
|
|
26
28
|
const DEFAULT_ALLOWED_SERVER_DIRS = ['src/backend'];
|
|
27
29
|
function normalizeToPosix(p) {
|
|
28
30
|
return p.replace(/\\/g, '/');
|
|
@@ -52,7 +54,7 @@ function tryResolveWithinAllowedDirs(projectDir, allowedDirs, requestedFile) {
|
|
|
52
54
|
const baseAbs = path_1.default.resolve(projectDir, d);
|
|
53
55
|
// Interpret client path as relative to src/web (where it's typically authored)
|
|
54
56
|
const fromWebAbs = path_1.default.resolve(projectDir, 'src/web', req);
|
|
55
|
-
// Map: <project>/src/backend/*
|
|
57
|
+
// Map: <project>/src/backend/* (coming from ../../backend/* from web code)
|
|
56
58
|
const mappedFromWebAbs = fromWebAbs.replace(path_1.default.resolve(projectDir, 'backend') + path_1.default.sep, path_1.default.resolve(projectDir, 'src', 'backend') + path_1.default.sep);
|
|
57
59
|
// Also accept callers passing a backend-relative path like "./auth" or "auth"
|
|
58
60
|
const fromBackendAbs = path_1.default.resolve(baseAbs, req);
|
|
@@ -155,6 +157,20 @@ async function executeRpc(ctx, payload) {
|
|
|
155
157
|
if (typeof fnValue !== 'function') {
|
|
156
158
|
return { success: false, error: `RPC function not found: ${fnName}` };
|
|
157
159
|
}
|
|
160
|
+
// --- SECURITY CHECK (Expose Annotation or Allowlist) ---
|
|
161
|
+
// 1. Verifica se a função possui o Symbol definido em annotations.ts (Via wrapper Expose())
|
|
162
|
+
const isAnnotated = fnValue[annotations_1.RPC_EXPOSED_KEY];
|
|
163
|
+
// 2. Verifica se o módulo exporta uma lista explícita chamada 'exposed' ou 'rpcMethods'
|
|
164
|
+
// Isso permite o uso de "export function" sem wrapper, listando os nomes no final do arquivo.
|
|
165
|
+
const allowList = mod.exposed || mod.rpcMethods;
|
|
166
|
+
const isListed = Array.isArray(allowList) && allowList.includes(fnName);
|
|
167
|
+
if (!isAnnotated && !isListed) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: `Function '${fnName}' is not exposed via RPC. Mark it with Expose() or add it to an exported 'exposed' array.`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// ------------------------------------------
|
|
158
174
|
const rpcRequest = ctx.request ? new http_1.NyteRequest(ctx.request) : buildRpcRequestFromPayload(payload);
|
|
159
175
|
const result = await fnValue(rpcRequest, ...payload.args);
|
|
160
176
|
return { success: true, return: result };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nyte",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Nyte.js is a high-level framework for building web applications with ease and speed. It provides a robust set of tools and features to streamline development and enhance productivity.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"types": "./dist/eslint/index.d.ts",
|
|
48
48
|
"import": "./dist/eslint/index.js",
|
|
49
49
|
"require": "./dist/eslint/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./rpc": {
|
|
52
|
+
"types": "./dist/rpc/annotations.d.ts",
|
|
53
|
+
"import": "./dist/rpc/annotations.js",
|
|
54
|
+
"require": "./dist/rpc/annotations.js"
|
|
50
55
|
}
|
|
51
56
|
},
|
|
52
57
|
"peerDependencies": {
|
|
@@ -223,221 +223,92 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
|
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
|
|
226
|
-
// --- Constantes de Configuração ---
|
|
227
|
-
const DEV_INDICATOR_SIZE = 48;
|
|
228
|
-
const DEV_INDICATOR_CORNERS = [
|
|
229
|
-
{ top: 16, left: 16 }, // 0: topo-esquerda
|
|
230
|
-
{ top: 16, right: 16 }, // 1: topo-direita
|
|
231
|
-
{ bottom: 16, left: 16 }, // 2: baixo-esquerda
|
|
232
|
-
{ bottom: 16, right: 16 },// 3: baixo-direita
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
function DevIndicator() {
|
|
236
|
-
const [corner, setCorner] = useState(3); // Canto atual (0-3)
|
|
237
|
-
const [isDragging, setIsDragging] = useState(false); // Estado de arrastar
|
|
238
|
-
const [isBuilding, setIsBuilding] = useState(false); // Estado de build
|
|
239
|
-
|
|
240
|
-
// Posição visual do indicador durante o arraste
|
|
241
|
-
const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
|
242
|
-
|
|
243
|
-
const indicatorRef = useRef<HTMLDivElement>(null);
|
|
244
|
-
const dragStartRef = useRef<{ x: number; y: number; moved: boolean } | null>(null);
|
|
245
|
-
|
|
246
|
-
// Escuta eventos de hot reload para mostrar estado de build
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
if (typeof window === 'undefined') return;
|
|
249
|
-
|
|
250
|
-
const handleHotReloadMessage = (event: MessageEvent) => {
|
|
251
|
-
try {
|
|
252
|
-
const message = JSON.parse(event.data);
|
|
253
|
-
|
|
254
|
-
// Quando detecta mudança em arquivo, ativa loading
|
|
255
|
-
if (message.type === 'frontend-reload' ||
|
|
256
|
-
message.type === 'backend-api-reload' ||
|
|
257
|
-
message.type === 'src-reload') {
|
|
258
|
-
setIsBuilding(true);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Quando o build termina ou servidor fica pronto, desativa loading
|
|
262
|
-
if (message.type === 'server-ready' || message.type === 'build-complete') {
|
|
263
|
-
setIsBuilding(false);
|
|
264
|
-
}
|
|
265
|
-
} catch (e) {
|
|
266
|
-
// Ignora mensagens que não são JSON
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
// Intercepta mensagens WebSocket
|
|
271
|
-
const originalWebSocket = window.WebSocket;
|
|
272
|
-
window.WebSocket = class extends originalWebSocket {
|
|
273
|
-
constructor(url: string | URL, protocols?: string | string[]) {
|
|
274
|
-
super(url, protocols);
|
|
275
|
-
|
|
276
|
-
this.addEventListener('message', (event) => {
|
|
277
|
-
if (url.toString().includes('hweb-hotreload')) {
|
|
278
|
-
handleHotReloadMessage(event);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
} as any;
|
|
283
|
-
|
|
284
|
-
return () => {
|
|
285
|
-
window.WebSocket = originalWebSocket;
|
|
286
|
-
};
|
|
287
|
-
}, []);
|
|
288
|
-
|
|
289
|
-
// --- Estilos Dinâmicos ---
|
|
290
|
-
const getIndicatorStyle = (): React.CSSProperties => {
|
|
291
|
-
const baseStyle: React.CSSProperties = {
|
|
292
|
-
position: 'fixed',
|
|
293
|
-
zIndex: 9999,
|
|
294
|
-
width: DEV_INDICATOR_SIZE,
|
|
295
|
-
height: DEV_INDICATOR_SIZE,
|
|
296
|
-
borderRadius: '50%',
|
|
297
|
-
background: isBuilding
|
|
298
|
-
? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
|
|
299
|
-
: 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
|
|
300
|
-
color: 'white',
|
|
301
|
-
fontWeight: 'bold',
|
|
302
|
-
fontSize: 28,
|
|
303
|
-
boxShadow: isBuilding
|
|
304
|
-
? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
|
|
305
|
-
: '0 4px 15px rgba(0,0,0,0.2)',
|
|
306
|
-
display: 'flex',
|
|
307
|
-
alignItems: 'center',
|
|
308
|
-
justifyContent: 'center',
|
|
309
|
-
cursor: isDragging ? 'grabbing' : 'grab',
|
|
310
|
-
userSelect: 'none',
|
|
311
|
-
transition: isDragging ? 'none' : 'all 0.3s ease-out',
|
|
312
|
-
animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
|
|
313
|
-
};
|
|
314
226
|
|
|
315
|
-
if (isDragging) {
|
|
316
|
-
return {
|
|
317
|
-
...baseStyle,
|
|
318
|
-
top: position.top,
|
|
319
|
-
left: position.left,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return { ...baseStyle, ...DEV_INDICATOR_CORNERS[corner] };
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const getMenuPositionStyle = (): React.CSSProperties => {
|
|
327
|
-
// Posiciona o menu dependendo do canto
|
|
328
|
-
switch (corner) {
|
|
329
|
-
case 0: return { top: '110%', left: '0' }; // Top-Left
|
|
330
|
-
case 1: return { top: '110%', right: '0' }; // Top-Right
|
|
331
|
-
case 2: return { bottom: '110%', left: '0' }; // Bottom-Left
|
|
332
|
-
case 3: return { bottom: '110%', right: '0' }; // Bottom-Right
|
|
333
|
-
default: return {};
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// --- Lógica de Eventos ---
|
|
338
|
-
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
339
|
-
e.preventDefault();
|
|
340
|
-
dragStartRef.current = { x: e.clientX, y: e.clientY, moved: false };
|
|
341
|
-
if (indicatorRef.current) {
|
|
342
|
-
const rect = indicatorRef.current.getBoundingClientRect();
|
|
343
|
-
setPosition({ top: rect.top, left: rect.left });
|
|
344
|
-
}
|
|
345
|
-
setIsDragging(true);
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
349
|
-
if (!isDragging || !dragStartRef.current) return;
|
|
350
227
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Diferencia clique de arrastar (threshold de 5px)
|
|
355
|
-
if (!dragStartRef.current.moved && Math.hypot(deltaX, deltaY) > 5) {
|
|
356
|
-
dragStartRef.current.moved = true;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (dragStartRef.current.moved) {
|
|
360
|
-
setPosition(prevPos => ({
|
|
361
|
-
top: prevPos.top + deltaY,
|
|
362
|
-
left: prevPos.left + deltaX,
|
|
363
|
-
}));
|
|
364
|
-
// Atualiza a referência para o próximo movimento
|
|
365
|
-
dragStartRef.current.x = e.clientX;
|
|
366
|
-
dragStartRef.current.y = e.clientY;
|
|
367
|
-
}
|
|
368
|
-
}, [isDragging]);
|
|
369
|
-
|
|
370
|
-
const handleMouseUp = useCallback((e: MouseEvent) => {
|
|
371
|
-
if (!isDragging) return;
|
|
372
|
-
setIsDragging(false);
|
|
373
|
-
|
|
374
|
-
// Se moveu, calcula o canto mais próximo
|
|
375
|
-
if (dragStartRef.current?.moved) {
|
|
376
|
-
const { clientX, clientY } = e;
|
|
377
|
-
const w = window.innerWidth;
|
|
378
|
-
const h = window.innerHeight;
|
|
379
|
-
|
|
380
|
-
const dists = [
|
|
381
|
-
Math.hypot(clientX, clientY), // TL
|
|
382
|
-
Math.hypot(w - clientX, clientY), // TR
|
|
383
|
-
Math.hypot(clientX, h - clientY), // BL
|
|
384
|
-
Math.hypot(w - clientX, h - clientY), // BR
|
|
385
|
-
];
|
|
386
|
-
setCorner(dists.indexOf(Math.min(...dists)));
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
dragStartRef.current = null;
|
|
390
|
-
}, [isDragging]);
|
|
391
|
-
|
|
392
|
-
// Adiciona e remove listeners globais
|
|
393
|
-
useEffect(() => {
|
|
394
|
-
if (isDragging) {
|
|
395
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
396
|
-
window.addEventListener('mouseup', handleMouseUp);
|
|
397
|
-
}
|
|
398
|
-
return () => {
|
|
399
|
-
window.removeEventListener('mousemove', handleMouseMove);
|
|
400
|
-
window.removeEventListener('mouseup', handleMouseUp);
|
|
401
|
-
};
|
|
402
|
-
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
228
|
+
export function DevIndicator() {
|
|
229
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
403
230
|
|
|
231
|
+
if (!isVisible) return null;
|
|
404
232
|
|
|
405
233
|
return (
|
|
406
234
|
<>
|
|
407
235
|
<style>
|
|
408
236
|
{`
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
237
|
+
@keyframes nyte-pulse {
|
|
238
|
+
0% { opacity: 0.4; }
|
|
239
|
+
50% { opacity: 1; }
|
|
240
|
+
100% { opacity: 0.4; }
|
|
241
|
+
}
|
|
242
|
+
.nyte-dev-badge {
|
|
243
|
+
position: fixed;
|
|
244
|
+
bottom: 20px;
|
|
245
|
+
right: 20px;
|
|
246
|
+
z-index: 999999;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 12px;
|
|
250
|
+
padding: 8px 14px;
|
|
251
|
+
background: rgba(15, 15, 20, 0.8);
|
|
252
|
+
backdrop-filter: blur(12px);
|
|
253
|
+
-webkit-backdrop-filter: blur(12px);
|
|
254
|
+
|
|
255
|
+
border-radius: 10px;
|
|
256
|
+
color: #fff;
|
|
257
|
+
font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
letter-spacing: 0.05em;
|
|
261
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
262
|
+
transition: all 0.2s ease;
|
|
263
|
+
cursor: default;
|
|
264
|
+
user-select: none;
|
|
265
|
+
}
|
|
266
|
+
.nyte-dev-badge:hover {
|
|
267
|
+
border-color: rgba(142, 45, 226, 0.5);
|
|
268
|
+
transform: translateY(-2px);
|
|
269
|
+
}
|
|
270
|
+
.nyte-status-dot {
|
|
271
|
+
width: 8px;
|
|
272
|
+
height: 8px;
|
|
273
|
+
background: #10b981; /* Verde esmeralda */
|
|
274
|
+
border-radius: 50%;
|
|
275
|
+
box-shadow: 0 0 10px #10b981;
|
|
276
|
+
animation: nyte-pulse 2s infinite ease-in-out;
|
|
277
|
+
}
|
|
278
|
+
.nyte-label {
|
|
279
|
+
color: rgba(255, 255, 255, 0.5);
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
font-size: 10px;
|
|
282
|
+
}
|
|
283
|
+
.nyte-logo {
|
|
284
|
+
background: linear-gradient(135deg, #00a3a3, #808080);
|
|
285
|
+
-webkit-background-clip: text;
|
|
286
|
+
-webkit-text-fill-color: transparent;
|
|
287
|
+
font-weight: 800;
|
|
288
|
+
}
|
|
289
|
+
`}
|
|
429
290
|
</style>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
291
|
+
|
|
292
|
+
<div className="nyte-dev-badge">
|
|
293
|
+
<div className="nyte-status-dot" />
|
|
294
|
+
<div>
|
|
295
|
+
<span className="nyte-logo">NYTE.JS</span>
|
|
296
|
+
</div>
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => setIsVisible(false)}
|
|
299
|
+
style={{
|
|
300
|
+
background: 'none',
|
|
301
|
+
border: 'none',
|
|
302
|
+
color: 'rgba(255,255,255,0.3)',
|
|
303
|
+
cursor: 'pointer',
|
|
304
|
+
fontSize: '14px',
|
|
305
|
+
padding: '0 0 0 4px',
|
|
306
|
+
marginLeft: '4px'
|
|
307
|
+
}}
|
|
308
|
+
title="Fechar"
|
|
309
|
+
>
|
|
310
|
+
×
|
|
311
|
+
</button>
|
|
441
312
|
</div>
|
|
442
313
|
</>
|
|
443
314
|
);
|
package/src/hotReload.ts
CHANGED
|
@@ -219,6 +219,7 @@ export class HotReloadManager {
|
|
|
219
219
|
|
|
220
220
|
try {
|
|
221
221
|
this.frontendChangeCallback?.();
|
|
222
|
+
this.backendApiChangeCallback?.();
|
|
222
223
|
await Promise.race([buildPromise, timeoutPromise]);
|
|
223
224
|
dm.end(`Build complete for ${path.basename(filePath)}, reloading frontend.`);
|
|
224
225
|
this.frontendChangeCallback?.();
|
|
@@ -235,14 +236,14 @@ export class HotReloadManager {
|
|
|
235
236
|
|
|
236
237
|
// Se for arquivo de backend, recarrega o módulo e notifica
|
|
237
238
|
if (isBackendFile) {
|
|
238
|
-
Console.logWithout(Levels.INFO,
|
|
239
|
+
Console.logWithout(Levels.INFO, undefined,`Reloading backend...`);
|
|
239
240
|
this.backendApiChangeCallback?.();
|
|
240
241
|
this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
// Fallback: se não for nem frontend nem backend detectado, recarrega tudo
|
|
244
245
|
if (!isFrontendFile && !isBackendFile) {
|
|
245
|
-
Console.logWithout(Levels.INFO,
|
|
246
|
+
Console.logWithout(Levels.INFO, undefined,`Reloading application...`);
|
|
246
247
|
this.frontendChangeCallback?.();
|
|
247
248
|
this.backendApiChangeCallback?.();
|
|
248
249
|
this.notifyClients('src-reload', { file: filePath, event: 'change' });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Símbolo único para marcar funções expostas.
|
|
3
|
+
* Usamos Symbol para garantir que não possa ser falsificado via JSON no payload.
|
|
4
|
+
*/
|
|
5
|
+
export const RPC_EXPOSED_KEY = Symbol('__rpc_exposed__');
|
|
6
|
+
|
|
7
|
+
type AnyFn = (...args: any[]) => any;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Marca uma ou mais funções como seguras para RPC.
|
|
11
|
+
*/
|
|
12
|
+
export default function Expose<T extends AnyFn>(fn: T): T;
|
|
13
|
+
export default function Expose<T extends AnyFn[]>(fns: [...T]): T;
|
|
14
|
+
export default function Expose<T extends AnyFn[]>(...fns: T): T;
|
|
15
|
+
export default function Expose(...input: any[]): any {
|
|
16
|
+
const fns: AnyFn[] =
|
|
17
|
+
Array.isArray(input[0]) ? input[0] : input;
|
|
18
|
+
|
|
19
|
+
for (const fn of fns) {
|
|
20
|
+
if (typeof fn !== 'function') {
|
|
21
|
+
throw new TypeError('Expose aceita apenas funções');
|
|
22
|
+
}
|
|
23
|
+
(fn as any)[RPC_EXPOSED_KEY] = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Retorno:
|
|
27
|
+
// - se veio uma função → retorna ela
|
|
28
|
+
// - se veio lista → retorna a lista
|
|
29
|
+
return input.length === 1 ? input[0] : input;
|
|
30
|
+
}
|
package/src/rpc/server.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
7
7
|
* You may obtain a copy of the License at
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
10
|
*
|
|
11
11
|
* Unless required by applicable law or agreed to in writing, software
|
|
12
12
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
@@ -20,6 +20,8 @@ import path from 'path';
|
|
|
20
20
|
import { RpcRequestPayload, RpcResponsePayload } from './types';
|
|
21
21
|
import { NyteRequest } from '../api/http';
|
|
22
22
|
import type { GenericRequest } from '../types/framework';
|
|
23
|
+
// Importamos a chave para verificar a anotação de segurança
|
|
24
|
+
import { RPC_EXPOSED_KEY } from './annotations';
|
|
23
25
|
|
|
24
26
|
const DEFAULT_ALLOWED_SERVER_DIRS = ['src/backend'] as const;
|
|
25
27
|
|
|
@@ -64,7 +66,7 @@ function tryResolveWithinAllowedDirs(projectDir: string, allowedDirs: string[],
|
|
|
64
66
|
// Interpret client path as relative to src/web (where it's typically authored)
|
|
65
67
|
const fromWebAbs = path.resolve(projectDir, 'src/web', req);
|
|
66
68
|
|
|
67
|
-
// Map: <project>/src/backend/*
|
|
69
|
+
// Map: <project>/src/backend/* (coming from ../../backend/* from web code)
|
|
68
70
|
const mappedFromWebAbs = fromWebAbs.replace(
|
|
69
71
|
path.resolve(projectDir, 'backend') + path.sep,
|
|
70
72
|
path.resolve(projectDir, 'src', 'backend') + path.sep
|
|
@@ -180,6 +182,23 @@ export async function executeRpc(ctx: RpcExecutionContext, payload: any): Promis
|
|
|
180
182
|
return { success: false, error: `RPC function not found: ${fnName}` };
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
// --- SECURITY CHECK (Expose Annotation or Allowlist) ---
|
|
186
|
+
// 1. Verifica se a função possui o Symbol definido em annotations.ts (Via wrapper Expose())
|
|
187
|
+
const isAnnotated = (fnValue as any)[RPC_EXPOSED_KEY];
|
|
188
|
+
|
|
189
|
+
// 2. Verifica se o módulo exporta uma lista explícita chamada 'exposed' ou 'rpcMethods'
|
|
190
|
+
// Isso permite o uso de "export function" sem wrapper, listando os nomes no final do arquivo.
|
|
191
|
+
const allowList = mod.exposed || mod.rpcMethods;
|
|
192
|
+
const isListed = Array.isArray(allowList) && allowList.includes(fnName);
|
|
193
|
+
|
|
194
|
+
if (!isAnnotated && !isListed) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error: `Function '${fnName}' is not exposed via RPC. Mark it with Expose() or add it to an exported 'exposed' array.`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// ------------------------------------------
|
|
201
|
+
|
|
183
202
|
const rpcRequest = ctx.request ? new NyteRequest(ctx.request) : buildRpcRequestFromPayload(payload);
|
|
184
203
|
const result = await fnValue(rpcRequest, ...payload.args);
|
|
185
204
|
return { success: true, return: result };
|
|
@@ -187,4 +206,4 @@ export async function executeRpc(ctx: RpcExecutionContext, payload: any): Promis
|
|
|
187
206
|
const message = typeof err?.message === 'string' ? err.message : 'Unknown RPC error';
|
|
188
207
|
return { success: false, error: message };
|
|
189
208
|
}
|
|
190
|
-
}
|
|
209
|
+
}
|