@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/hooks.tsx ADDED
@@ -0,0 +1,266 @@
1
+ import {
2
+ signal,
3
+ useSignal,
4
+ type Signal,
5
+ } from "@preact/signals";
6
+ import { useEffect } from "preact/hooks";
7
+ import {
8
+ useParams as wouterUseParams,
9
+ useLocation as wouterUseLocation,
10
+ } from "wouter-preact";
11
+
12
+ /**
13
+ * Força o signal a notificar mudanças após mutação de propriedades aninhadas
14
+ *
15
+ * Uso:
16
+ * ```tsx
17
+ * word.isChecked = !word.isChecked; // muta
18
+ * touch(pathAppliedData); // notifica
19
+ * ```
20
+ */
21
+ export function touch<T>(sig: Signal<T | null>): void {
22
+ if (sig.value !== null && typeof sig.value === "object") {
23
+ sig.value = { ...sig.value } as T;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Hidrata dados do loader fora de componentes (SSR only)
29
+ * Apenas lê do __PAGE_DATA__, nunca faz fetch
30
+ *
31
+ * Use para dados globais/compartilhados carregados no Layout.
32
+ * Para dados de página com suporte a navegação SPA, use useLoader() dentro do Component.
33
+ *
34
+ * Uso:
35
+ * ```tsx
36
+ * // No Layout - carrega dados globais
37
+ * export const { data: globalData } = Loader<GlobalType>();
38
+ *
39
+ * // No componente do Layout
40
+ * export const Component = ({ children }) => <div>{globalData.value?.name}{children}</div>;
41
+ *
42
+ * // Em outros módulos - importa o dado do Layout
43
+ * import { globalData } from "./Layout.js";
44
+ * ```
45
+ *
46
+ * @param moduleId - Injetado automaticamente pelo veloPlugin
47
+ */
48
+ export function Loader<T>(moduleId?: string): {
49
+ data: Signal<T | null>;
50
+ loading: Signal<boolean>;
51
+ } {
52
+ const loading = signal(false);
53
+
54
+ // Servidor: getter que sempre lê do AsyncLocalStorage (sem cache)
55
+ if (typeof window === "undefined") {
56
+ const data = {
57
+ get value(): T | null {
58
+ if (!moduleId) return null;
59
+ const storage = (globalThis as any).__veloServerData;
60
+ const serverData = storage?.getStore?.();
61
+ return (serverData?.[moduleId] as T) ?? null;
62
+ },
63
+ };
64
+ return { data: data as Signal<T | null>, loading };
65
+ }
66
+
67
+ // Cliente: apenas hidrata do __PAGE_DATA__, nunca faz fetch
68
+ // (Loader roda uma única vez no import do módulo, não é responsável por SPA)
69
+ const pageData = (window as any).__PAGE_DATA__;
70
+ const initialData =
71
+ moduleId && pageData?.[moduleId] ? (pageData[moduleId] as T) : null;
72
+
73
+ return { data: signal<T | null>(initialData), loading };
74
+ }
75
+
76
+ /**
77
+ * Hook para acessar dados do loader dentro de componentes
78
+ * Suporta SSR hydration e navegação SPA (faz fetch se necessário)
79
+ *
80
+ * Uso:
81
+ * ```tsx
82
+ * export const Component = () => {
83
+ * const { data, loading } = useLoader<MyType>();
84
+ * return <div>{data.value?.name}</div>;
85
+ * };
86
+ *
87
+ * // Com deps (ex: recarregar ao trocar de rota)
88
+ * const params = useParams();
89
+ * const { data } = useLoader<MyType>([params.id]);
90
+ * ```
91
+ *
92
+ * @param moduleId - Injetado automaticamente pelo veloPlugin
93
+ * @param deps - Array de dependências (triggers re-fetch quando mudam)
94
+ */
95
+ export function useLoader<T>(): { data: Signal<T | null>; loading: Signal<boolean>; refetch: () => void };
96
+ export function useLoader<T>(deps: any[]): { data: Signal<T | null>; loading: Signal<boolean>; refetch: () => void };
97
+ export function useLoader<T>(moduleId: string, deps?: any[]): { data: Signal<T | null>; loading: Signal<boolean>; refetch: () => void };
98
+ export function useLoader<T>(
99
+ moduleIdOrDeps?: string | any[],
100
+ deps?: any[],
101
+ ): {
102
+ data: Signal<T | null>;
103
+ loading: Signal<boolean>;
104
+ refetch: () => void;
105
+ } {
106
+ // Resolve args: usuário chama useLoader(deps), vite transforma em useLoader(moduleId, deps)
107
+ let moduleId: string | undefined;
108
+ let resolvedDeps: any[] | undefined;
109
+ if (Array.isArray(moduleIdOrDeps)) {
110
+ resolvedDeps = moduleIdOrDeps;
111
+ } else {
112
+ moduleId = moduleIdOrDeps;
113
+ resolvedDeps = deps;
114
+ }
115
+
116
+ // Servidor: pega dados do AsyncLocalStorage (isolado por request)
117
+ let initialData: T | null = null;
118
+
119
+ if (typeof window === "undefined" && moduleId) {
120
+ const storage = (globalThis as any).__veloServerData;
121
+ const serverData = storage?.getStore?.();
122
+ if (serverData?.[moduleId]) {
123
+ initialData = serverData[moduleId] as T;
124
+ }
125
+ }
126
+
127
+ // Cliente: tenta hidratar do __PAGE_DATA__
128
+ if (typeof window !== "undefined" && moduleId) {
129
+ const pageData = (window as any).__PAGE_DATA__;
130
+ if (pageData?.[moduleId]) {
131
+ initialData = pageData[moduleId] as T;
132
+ delete pageData[moduleId];
133
+ }
134
+ }
135
+
136
+ const data = useSignal<T | null>(initialData);
137
+ const loading = useSignal(false);
138
+
139
+ const fetchData = () => {
140
+ if (typeof window === "undefined" || !moduleId) return;
141
+ const currentPath = window.location.pathname;
142
+ const searchParams = new URLSearchParams(window.location.search);
143
+ searchParams.set("_data", "1");
144
+ loading.value = true;
145
+
146
+ fetch(`${currentPath}?${searchParams.toString()}`)
147
+ .then((res) => res.json())
148
+ .then((json: Record<string, unknown>) => {
149
+ data.value = json[moduleId] as T;
150
+ loading.value = false;
151
+ })
152
+ .catch((err) => {
153
+ console.error("Erro ao carregar dados:", err);
154
+ loading.value = false;
155
+ });
156
+ };
157
+
158
+ // Se não tem dado, faz fetch (navegação SPA)
159
+ // Na hidratação initialData !== null, então pula o fetch
160
+ // Quando deps mudam (ex: troca de rota), initialData será null e dispara fetch
161
+ useEffect(() => {
162
+ if (initialData !== null) return;
163
+ fetchData();
164
+ }, resolvedDeps ?? []);
165
+
166
+ return { data, loading, refetch: fetchData };
167
+ }
168
+
169
+ /**
170
+ * Hook para navegação programática
171
+ *
172
+ * Uso:
173
+ * ```tsx
174
+ * const navigate = useNavigate();
175
+ * navigate("/outra-pagina");
176
+ * ```
177
+ */
178
+ export function useNavigate() {
179
+ const [, navigate] = wouterUseLocation();
180
+ return navigate;
181
+ }
182
+
183
+ /**
184
+ * Hook para acessar parâmetros da rota (funciona em SSR e cliente)
185
+ *
186
+ * Uso:
187
+ * ```tsx
188
+ * // Rota: /teste/:pathAppliedId/avaliacao/:assessmentRatingIndex
189
+ * const params = useParams();
190
+ * console.log(params.pathAppliedId, params.assessmentRatingIndex);
191
+ * ```
192
+ */
193
+ export function useParams<
194
+ T extends Record<string, string> = Record<string, string>,
195
+ >(): T {
196
+ // Servidor: lê do AsyncLocalStorage
197
+ if (typeof window === "undefined") {
198
+ const storage = (globalThis as any).__veloServerData;
199
+ const serverData = storage?.getStore?.();
200
+ return (serverData?.__params as T) ?? ({} as T);
201
+ }
202
+
203
+ // Cliente: sempre usa wouter para ter params atualizados durante navegação SPA
204
+ return wouterUseParams() as T;
205
+ }
206
+
207
+ /**
208
+ * Hook para acessar query string da rota (funciona em SSR e cliente)
209
+ *
210
+ * Uso:
211
+ * ```tsx
212
+ * // URL: /teste?foo=bar&baz=123
213
+ * const query = useQuery();
214
+ * console.log(query.foo, query.baz);
215
+ * ```
216
+ */
217
+ export function useQuery<
218
+ T extends Record<string, string> = Record<string, string>,
219
+ >(): T {
220
+ // Servidor: lê do AsyncLocalStorage
221
+ if (typeof window === "undefined") {
222
+ const storage = (globalThis as any).__veloServerData;
223
+ const serverData = storage?.getStore?.();
224
+ return (serverData?.__query as T) ?? ({} as T);
225
+ }
226
+
227
+ // Cliente: lê do __PAGE_DATA__ ou parse da URL
228
+ const pageData = (window as any).__PAGE_DATA__;
229
+ if (pageData?.__query) {
230
+ return pageData.__query as T;
231
+ }
232
+
233
+ // Fallback: parse da URL atual
234
+ const searchParams = new URLSearchParams(window.location.search);
235
+ const query: Record<string, string> = {};
236
+ searchParams.forEach((value, key) => {
237
+ query[key] = value;
238
+ });
239
+ return query as T;
240
+ }
241
+
242
+ /**
243
+ * Hook para acessar o pathname completo da URL (funciona em SSR e cliente)
244
+ * Diferente do useLocation do wouter que retorna path relativo ao contexto nest,
245
+ * este hook sempre retorna o pathname absoluto.
246
+ *
247
+ * Uso:
248
+ * ```tsx
249
+ * const pathname = usePathname();
250
+ * // Em /admin/colaboradores sempre retorna "/admin/colaboradores"
251
+ * // (não "/" como useLocation retornaria dentro do contexto /admin)
252
+ * ```
253
+ */
254
+ export function usePathname(): string {
255
+ // Servidor: lê do AsyncLocalStorage
256
+ if (typeof window === "undefined") {
257
+ const storage = (globalThis as any).__veloServerData;
258
+ const serverData = storage?.getStore?.();
259
+ return (serverData?.__pathname as string) ?? "/";
260
+ }
261
+
262
+ // Cliente: usa useLocation do wouter para reatividade,
263
+ // mas retorna window.location.pathname para o path absoluto
264
+ wouterUseLocation(); // trigger re-render on navigation
265
+ return window.location.pathname;
266
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Types
2
+ export type {
3
+ AppRoutes,
4
+ RouteNode,
5
+ RouteModule,
6
+ LoaderArgs,
7
+ ActionArgs,
8
+ Metadata,
9
+ } from "./types.js";
10
+
11
+ // Config
12
+ export type { VeloConfig } from "./config.js";
13
+ export { defineConfig } from "./config.js";
14
+
15
+ // Components
16
+ export { Scripts, Link } from "./components.js";
17
+
18
+ // Re-export Hono types
19
+ export type { Context, MiddlewareHandler, Next } from "hono";
package/src/init.ts ADDED
@@ -0,0 +1,177 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const PACKAGE_NAME = "@mauroandre/velojs";
5
+
6
+ const templates: Record<string, string> = {
7
+ "package.json": `{
8
+ "name": "my-velojs-app",
9
+ "version": "0.1.0",
10
+ "type": "module",
11
+ "scripts": {
12
+ "dev": "velojs dev",
13
+ "build": "velojs build",
14
+ "start": "velojs start"
15
+ },
16
+ "dependencies": {
17
+ "${PACKAGE_NAME}": "^0.1.0"
18
+ }
19
+ }
20
+ `,
21
+
22
+ "vite.config.ts": `import { veloPlugin } from "${PACKAGE_NAME}/vite";
23
+
24
+ export default { plugins: [veloPlugin()] };
25
+ `,
26
+
27
+ "tsconfig.json": `{
28
+ "compilerOptions": {
29
+ "module": "esnext",
30
+ "moduleResolution": "bundler",
31
+ "target": "esnext",
32
+ "lib": ["esnext", "dom"],
33
+ "strict": true,
34
+ "noUncheckedIndexedAccess": true,
35
+ "verbatimModuleSyntax": true,
36
+ "isolatedModules": true,
37
+ "skipLibCheck": true,
38
+ "esModuleInterop": true,
39
+ "noEmit": true,
40
+ "jsx": "react-jsx",
41
+ "jsxImportSource": "preact"
42
+ },
43
+ "include": ["app/**/*"],
44
+ "exclude": ["node_modules", "dist"]
45
+ }
46
+ `,
47
+
48
+ "app/routes.tsx": `import type { AppRoutes } from "${PACKAGE_NAME}";
49
+
50
+ import * as Root from "./client-root.js";
51
+ import * as Home from "./pages/Home.js";
52
+
53
+ export default [
54
+ {
55
+ module: Root,
56
+ isRoot: true,
57
+ children: [
58
+ { path: "/", module: Home },
59
+ ],
60
+ },
61
+ ] satisfies AppRoutes;
62
+ `,
63
+
64
+ "app/server.tsx": `// Server initialization
65
+ // Use this file to set up server-side logic:
66
+ // import { addRoutes, onServer } from "${PACKAGE_NAME}/server";
67
+ `,
68
+
69
+ "app/client.tsx": `import "./styles/global.css";
70
+ `,
71
+
72
+ "app/client-root.tsx": `import { Scripts } from "${PACKAGE_NAME}";
73
+
74
+ interface RootProps {
75
+ children: preact.ComponentChildren;
76
+ }
77
+
78
+ export const Component = ({ children }: RootProps) => {
79
+ return (
80
+ <html lang="en">
81
+ <head>
82
+ <meta charSet="utf-8" />
83
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
84
+ <title>VeloJS App</title>
85
+ <Scripts />
86
+ </head>
87
+ <body>{children}</body>
88
+ </html>
89
+ );
90
+ };
91
+ `,
92
+
93
+ "app/styles/global.css": `*,
94
+ *::before,
95
+ *::after {
96
+ box-sizing: border-box;
97
+ margin: 0;
98
+ padding: 0;
99
+ }
100
+
101
+ body {
102
+ font-family: system-ui, -apple-system, sans-serif;
103
+ line-height: 1.6;
104
+ color: #1a1a1a;
105
+ background: #fafafa;
106
+ }
107
+
108
+ a {
109
+ color: #0066cc;
110
+ text-decoration: none;
111
+ }
112
+
113
+ a:hover {
114
+ text-decoration: underline;
115
+ }
116
+ `,
117
+
118
+ "app/pages/Home.tsx": `export const metadata = { title: "Home" };
119
+
120
+ export const Component = () => {
121
+ return (
122
+ <main style={{ maxWidth: 640, margin: "80px auto", padding: "0 20px" }}>
123
+ <h1>Welcome to VeloJS</h1>
124
+ <p>Edit <code>app/pages/Home.tsx</code> to get started.</p>
125
+ </main>
126
+ );
127
+ };
128
+ `,
129
+ };
130
+
131
+ export async function runInit(dirName?: string): Promise<void> {
132
+ const targetDir = dirName
133
+ ? path.resolve(process.cwd(), dirName)
134
+ : process.cwd();
135
+
136
+ const dirBaseName = path.basename(targetDir);
137
+
138
+ // Check if directory exists and is non-empty
139
+ if (fs.existsSync(targetDir)) {
140
+ const entries = fs.readdirSync(targetDir).filter(
141
+ (e) => !e.startsWith(".") && e !== "node_modules"
142
+ );
143
+ if (entries.length > 0) {
144
+ console.error(
145
+ `Error: Directory "${dirBaseName}" is not empty. Use an empty directory.`
146
+ );
147
+ process.exit(1);
148
+ }
149
+ } else {
150
+ fs.mkdirSync(targetDir, { recursive: true });
151
+ }
152
+
153
+ // Write all template files
154
+ for (const [filePath, content] of Object.entries(templates)) {
155
+ const fullPath = path.join(targetDir, filePath);
156
+ const dir = path.dirname(fullPath);
157
+
158
+ if (!fs.existsSync(dir)) {
159
+ fs.mkdirSync(dir, { recursive: true });
160
+ }
161
+
162
+ // Replace app name in package.json
163
+ const finalContent = filePath === "package.json"
164
+ ? content.replace("my-velojs-app", dirBaseName)
165
+ : content;
166
+
167
+ fs.writeFileSync(fullPath, finalContent);
168
+ }
169
+
170
+ console.log(`\nProject created in ${dirName ? dirBaseName : "current directory"}!\n`);
171
+ console.log("Next steps:\n");
172
+ if (dirName) {
173
+ console.log(` cd ${dirBaseName}`);
174
+ }
175
+ console.log(" npm install");
176
+ console.log(" npx velojs dev\n");
177
+ }