@mauroandre/velojs 0.0.1

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/src/server.tsx ADDED
@@ -0,0 +1,347 @@
1
+ import { Hono, type Context, type MiddlewareHandler } from "hono";
2
+ import { logger } from "hono/logger";
3
+ import { trimTrailingSlash } from "hono/trailing-slash";
4
+ import { render as preactRender } from "preact-render-to-string";
5
+ import { Router } from "wouter-preact";
6
+ import type { ComponentType, VNode } from "preact";
7
+ import type { RouteNode, RouteModule, LoaderArgs, AppRoutes } from "./types.js";
8
+ import { AsyncLocalStorage } from "node:async_hooks";
9
+
10
+ // ============================================
11
+ // ASYNC LOCAL STORAGE - Dados isolados por request
12
+ // ============================================
13
+
14
+ export const serverDataStorage = new AsyncLocalStorage<
15
+ Record<string, unknown>
16
+ >();
17
+
18
+ // Expõe via globalThis para hooks.tsx acessar sem importar node:async_hooks
19
+ (globalThis as any).__veloServerData = serverDataStorage;
20
+
21
+ // ============================================
22
+ // ON-SERVER HOOK - Access the underlying HTTP server
23
+ // ============================================
24
+
25
+ type ServerCallback = (server: import("http").Server) => void;
26
+ const serverCallbacks: ServerCallback[] = [];
27
+ let activeServer: import("http").Server | null = null;
28
+
29
+ export function onServer(fn: ServerCallback): void {
30
+ if (activeServer) { fn(activeServer); return; }
31
+ serverCallbacks.push(fn);
32
+ }
33
+
34
+ function flushServerCallbacks(server: import("http").Server): void {
35
+ activeServer = server;
36
+ for (const fn of serverCallbacks) fn(server);
37
+ serverCallbacks.length = 0;
38
+ }
39
+
40
+ // ============================================
41
+ // SERVER OPTIONS
42
+ // ============================================
43
+
44
+ export interface StartServerOptions {
45
+ routes: AppRoutes;
46
+ port?: number;
47
+ }
48
+
49
+ // ============================================
50
+ // ADD ROUTES - Permite registrar rotas custom
51
+ // ============================================
52
+
53
+ const pendingRoutes: Array<(app: Hono) => void | Promise<void>> = [];
54
+
55
+ export function addRoutes(fn: (app: Hono) => void | Promise<void>): void {
56
+ pendingRoutes.push(fn);
57
+ }
58
+
59
+ // ============================================
60
+ // RENDER PAGE - SSR ou JSON para navegação SPA
61
+ // ============================================
62
+
63
+ const renderPage = (c: Context, Component: VNode, data?: unknown) => {
64
+ // Navegação SPA - retorna apenas JSON
65
+ if (c.req.query("_data") === "1") {
66
+ return c.json(data ?? null);
67
+ }
68
+
69
+ // SSR - renderiza HTML completo dentro do contexto isolado
70
+ const path = c.req.path;
71
+ const html = serverDataStorage.run(
72
+ (data as Record<string, unknown>) ?? {},
73
+ () => {
74
+ return preactRender(<Router ssrPath={path}>{Component}</Router>);
75
+ }
76
+ );
77
+
78
+ // Sem dados - retorna HTML simples
79
+ if (!data) {
80
+ return c.html(html);
81
+ }
82
+
83
+ // Com dados - injeta window.__PAGE_DATA__ no <head> (antes dos scripts do app)
84
+ const script = `<script>window.__PAGE_DATA__=${JSON.stringify(
85
+ data
86
+ )}</script>`;
87
+ return c.html(html.replace("</head>", `${script}</head>`));
88
+ };
89
+
90
+ // ============================================
91
+ // LOAD PAGE - Executa loaders e coleta componentes
92
+ // ============================================
93
+
94
+ const loadPage = async (modules: RouteModule[], c: Context) => {
95
+ const params = c.req.param();
96
+ const query = c.req.query();
97
+ const loaderArgs: LoaderArgs = { params, query, c };
98
+
99
+ // Executa todos os loaders em paralelo
100
+ const results = await Promise.all(
101
+ modules.map(async (module) => {
102
+ if (!module.loader) return null;
103
+ const loaderData = await module.loader(loaderArgs);
104
+ const moduleId = module.metadata?.moduleId;
105
+ return moduleId ? { moduleId, loaderData } : null;
106
+ })
107
+ );
108
+
109
+ // Monta objeto com moduleId como chave + params/query/pathname para hooks
110
+ const data: Record<string, unknown> = {
111
+ __params: params,
112
+ __query: query,
113
+ __pathname: c.req.path,
114
+ };
115
+ for (const result of results) {
116
+ if (result) {
117
+ data[result.moduleId] = result.loaderData;
118
+ }
119
+ }
120
+
121
+ return {
122
+ components: modules.map((m) => m.Component),
123
+ data,
124
+ };
125
+ };
126
+
127
+ // ============================================
128
+ // NEST COMPONENTS - Aninha Layout > Layout > Page
129
+ // ============================================
130
+
131
+ const nestComponents = (components: ComponentType<any>[]): VNode => {
132
+ if (components.length === 0) return null as any;
133
+
134
+ const validComponents = components.filter(Boolean);
135
+ if (validComponents.length === 0) return null as any;
136
+
137
+ if (validComponents.length === 1) {
138
+ const Page = validComponents[0]!;
139
+ return <Page />;
140
+ }
141
+
142
+ const Page = validComponents[validComponents.length - 1]!;
143
+ const layouts = validComponents.slice(0, -1);
144
+
145
+ return layouts.reduceRight((child, Layout) => {
146
+ return <Layout>{child}</Layout>;
147
+ }, (<Page />) as VNode);
148
+ };
149
+
150
+ // ============================================
151
+ // REGISTER ROUTES - Gera rotas do Hono dinamicamente
152
+ // ============================================
153
+
154
+ const registerRoutes = (
155
+ app: Hono,
156
+ nodes: RouteNode[],
157
+ parentModules: RouteModule[] = [],
158
+ parentMiddlewares: MiddlewareHandler[] = []
159
+ ) => {
160
+ for (const node of nodes) {
161
+ const currentModules = [...parentModules, node.module];
162
+ // Acumula middlewares: pai → filho
163
+ const currentMiddlewares = [
164
+ ...parentMiddlewares,
165
+ ...(node.middlewares || []),
166
+ ];
167
+
168
+ if (node.children) {
169
+ // Tem filhos - continua recursão com middlewares acumulados
170
+ registerRoutes(
171
+ app,
172
+ node.children,
173
+ currentModules,
174
+ currentMiddlewares
175
+ );
176
+ } else {
177
+ // Folha - registra rota usando metadata.fullPath
178
+ const fullPath = node.module.metadata?.fullPath;
179
+ if (!fullPath) {
180
+ console.warn(
181
+ `Module ${node.module.metadata?.moduleId} has no fullPath`
182
+ );
183
+ continue;
184
+ }
185
+
186
+ const handler = async (c: Context) => {
187
+ const { components, data } = await loadPage(currentModules, c);
188
+ const nested = nestComponents(components);
189
+ return renderPage(c, nested, data);
190
+ };
191
+
192
+ if (currentMiddlewares.length > 0) {
193
+ app.on(["GET"], [fullPath], ...currentMiddlewares, handler);
194
+ } else {
195
+ app.on(["GET"], [fullPath], handler);
196
+ }
197
+ }
198
+ }
199
+ };
200
+
201
+ // ============================================
202
+ // REGISTER ACTION ROUTES - Registra POST para actions
203
+ // ============================================
204
+
205
+ const registerActionRoutes = (
206
+ app: Hono,
207
+ nodes: RouteNode[],
208
+ parentMiddlewares: MiddlewareHandler[] = []
209
+ ) => {
210
+ for (const node of nodes) {
211
+ const moduleId = node.module.metadata?.moduleId;
212
+ // Acumula middlewares: pai → filho
213
+ const currentMiddlewares = [
214
+ ...parentMiddlewares,
215
+ ...(node.middlewares || []),
216
+ ];
217
+
218
+ if (moduleId) {
219
+ // Encontra todas as actions do módulo
220
+ const actionKeys = Object.keys(node.module).filter((k) =>
221
+ k.startsWith("action_")
222
+ );
223
+
224
+ for (const actionKey of actionKeys) {
225
+ const actionName = actionKey.replace("action_", "");
226
+ const action = (
227
+ node.module as unknown as Record<string, unknown>
228
+ )[actionKey] as
229
+ | ((body: unknown) => Promise<unknown>)
230
+ | undefined;
231
+
232
+ if (typeof action === "function") {
233
+ const actionPath = `/_action/${moduleId}/${actionName}`;
234
+ const handler = async (c: Context) => {
235
+ let body = {};
236
+ try {
237
+ body = await c.req.json();
238
+ } catch {
239
+ // No body - ok for actions without params
240
+ }
241
+ // Passa ActionArgs para a action
242
+ const actionArgs = {
243
+ body,
244
+ params: c.req.param(),
245
+ query: c.req.query(),
246
+ c,
247
+ };
248
+ try {
249
+ const result = await action(actionArgs);
250
+ return c.json(result ?? { ok: true });
251
+ } catch (error) {
252
+ const message =
253
+ error instanceof Error
254
+ ? error.message
255
+ : "Action failed";
256
+ return c.json({ error: message }, 500);
257
+ }
258
+ };
259
+
260
+ if (currentMiddlewares.length > 0) {
261
+ app.on(
262
+ ["POST"],
263
+ [actionPath],
264
+ ...currentMiddlewares,
265
+ handler
266
+ );
267
+ } else {
268
+ app.on(["POST"], [actionPath], handler);
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ // Recursivamente registra actions dos filhos com middlewares acumulados
275
+ if (node.children) {
276
+ registerActionRoutes(app, node.children, currentMiddlewares);
277
+ }
278
+ }
279
+ };
280
+
281
+ // ============================================
282
+ // CREATE APP - Cria app Hono com rotas
283
+ // ============================================
284
+
285
+ export const createApp = async (routes: AppRoutes): Promise<Hono> => {
286
+ const app = new Hono();
287
+
288
+ app.use(trimTrailingSlash());
289
+
290
+ if (process.env.NODE_ENV !== "production") {
291
+ app.use("*", logger());
292
+ }
293
+
294
+ // Custom routes (registradas via addRoutes no server.tsx do app)
295
+ for (const fn of pendingRoutes) {
296
+ await fn(app);
297
+ }
298
+
299
+ // Page routes (dinâmico)
300
+ registerRoutes(app, routes);
301
+
302
+ // Action routes (dinâmico)
303
+ registerActionRoutes(app, routes);
304
+
305
+ // Dev mode: flush server callbacks using Vite's HTTP server
306
+ if (process.env.NODE_ENV !== "production" && !activeServer) {
307
+ const devServer = (globalThis as any).__veloDevServer;
308
+ if (devServer) flushServerCallbacks(devServer);
309
+ }
310
+
311
+ return app;
312
+ };
313
+
314
+ // ============================================
315
+ // START SERVER - Entry point principal
316
+ // ============================================
317
+
318
+ export const startServer = async (options: StartServerOptions) => {
319
+ const { routes, port = Number(process.env.SERVER_PORT) || 3000 } = options;
320
+ const app = await createApp(routes);
321
+
322
+ // Production: serve static files and start server
323
+ if (process.env.NODE_ENV === "production") {
324
+ const { serve } = await import("@hono/node-server");
325
+ const { serveStatic } = await import("@hono/node-server/serve-static");
326
+ const { dirname, join } = await import("node:path");
327
+ const { fileURLToPath } = await import("node:url");
328
+
329
+ const __dirname = dirname(fileURLToPath(import.meta.url));
330
+ const clientDir = join(__dirname, "client");
331
+
332
+ // Serve static files from dist/client/ if STATIC_BASE_URL is not external
333
+ const staticUrl = process.env.STATIC_BASE_URL || "";
334
+ if (!staticUrl.startsWith("http")) {
335
+ app.use("/*", serveStatic({ root: clientDir }));
336
+ }
337
+
338
+ console.log(`Server running on http://localhost:${port}`);
339
+ const server = serve({ fetch: app.fetch, port });
340
+ flushServerCallbacks(server as unknown as import("http").Server);
341
+ }
342
+
343
+ return app;
344
+ };
345
+
346
+ // Export createApp for Vite dev server
347
+ export default createApp;
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { ComponentType } from "preact";
2
+ import type { Context, MiddlewareHandler } from "hono";
3
+
4
+ export interface LoaderArgs {
5
+ params: Record<string, string>;
6
+ query: Record<string, string>;
7
+ c: Context;
8
+ }
9
+
10
+ export interface ActionArgs<TBody = unknown> {
11
+ body: TBody;
12
+ params?: Record<string, string>;
13
+ query?: Record<string, string>;
14
+ c?: Context;
15
+ }
16
+
17
+ export interface Metadata {
18
+ moduleId: string;
19
+ fullPath?: string;
20
+ path?: string;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface RouteModule {
25
+ Component: ComponentType<any>;
26
+ loader?: (args: LoaderArgs) => Promise<any>;
27
+ metadata?: Metadata;
28
+ [key: `action_${string}`]: (args: ActionArgs<any>) => Promise<any>;
29
+ }
30
+
31
+ export interface RouteNode {
32
+ path?: string;
33
+ module: RouteModule;
34
+ children?: RouteNode[];
35
+ middlewares?: MiddlewareHandler[];
36
+ isRoot?: boolean;
37
+ }
38
+
39
+ export type AppRoutes = RouteNode[];