@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/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
|
+
}
|