@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/LICENSE +21 -0
- package/README.md +1049 -0
- package/bin/velojs.js +15 -0
- package/package.json +120 -0
- package/src/cli.ts +83 -0
- package/src/client.tsx +79 -0
- package/src/components.tsx +155 -0
- package/src/config.ts +29 -0
- package/src/cookie.ts +7 -0
- package/src/factory.ts +1 -0
- package/src/hooks.tsx +266 -0
- package/src/index.ts +19 -0
- package/src/init.ts +177 -0
- package/src/server.tsx +347 -0
- package/src/types.ts +39 -0
- package/src/vite.ts +937 -0
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[];
|