raiko.js 0.0.0-stage → 1.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.es.md +508 -0
- package/README.md +507 -2
- package/README.pt-BR.md +508 -0
- package/bin/raiko.mjs +149 -0
- package/dist/adapters/node-ws.d.ts +40 -0
- package/dist/adapters/node.d.ts +6 -0
- package/dist/adapters/node.js +298 -0
- package/dist/adapters/serve.d.ts +12 -0
- package/dist/admin/adapters.d.ts +35 -0
- package/dist/admin/index.d.ts +50 -0
- package/dist/admin/index.js +214 -0
- package/dist/app-router/dev-error.d.ts +15 -0
- package/dist/app-router/image.d.ts +1 -0
- package/dist/app-router/index.d.ts +40 -0
- package/dist/app-router/index.js +489 -0
- package/dist/app-router/metadata.d.ts +51 -0
- package/dist/app-router/scan.d.ts +20 -0
- package/dist/app.d.ts +230 -0
- package/dist/auth/index.d.ts +65 -0
- package/dist/auth/index.js +334 -0
- package/dist/auth/jwt.d.ts +13 -0
- package/dist/auth/providers.d.ts +43 -0
- package/dist/auth/totp.d.ts +35 -0
- package/dist/body.d.ts +10 -0
- package/dist/chunk-4NOVAJBA.js +499 -0
- package/dist/chunk-6VHSSIDV.js +186 -0
- package/dist/chunk-FU36MTH6.js +83 -0
- package/dist/chunk-H6BWMS4P.js +87 -0
- package/dist/chunk-IJNNQKTG.js +626 -0
- package/dist/chunk-LAFHS4ZI.js +45 -0
- package/dist/chunk-MGKXQINB.js +45 -0
- package/dist/chunk-SNAWC6GY.js +123 -0
- package/dist/chunk-U626USOW.js +72 -0
- package/dist/chunk-VC57IKQN.js +268 -0
- package/dist/chunk-Y3YIWI5S.js +217 -0
- package/dist/chunk-Z52Z3E6A.js +122 -0
- package/dist/client/index.d.ts +91 -0
- package/dist/client/index.js +228 -0
- package/dist/client/socket.d.ts +46 -0
- package/dist/components/index.d.ts +50 -0
- package/dist/components/index.js +4 -0
- package/dist/context-7E3DE2UC.js +3 -0
- package/dist/context.d.ts +47 -0
- package/dist/cookie.d.ts +39 -0
- package/dist/error.d.ts +34 -0
- package/dist/i18n/index.d.ts +36 -0
- package/dist/i18n/index.js +47 -0
- package/dist/image-FBZPWI2W.js +41 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +1 -0
- package/dist/jsx/jsx-dev-runtime.js +1 -0
- package/dist/jsx/jsx-runtime.d.ts +1 -0
- package/dist/jsx/jsx-runtime.js +1 -0
- package/dist/jsx/runtime.d.ts +75 -0
- package/dist/mail/index.d.ts +88 -0
- package/dist/mail/index.js +147 -0
- package/dist/p2p/client.d.ts +26 -0
- package/dist/p2p/index.d.ts +1 -0
- package/dist/p2p/index.js +39 -0
- package/dist/p2p/server.d.ts +16 -0
- package/dist/p2p-client.d.ts +2 -0
- package/dist/p2p-client.js +88 -0
- package/dist/pipeline.d.ts +36 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.js +254 -0
- package/dist/plugins/openapi.d.ts +27 -0
- package/dist/plugins/ops.d.ts +60 -0
- package/dist/plugins/security.d.ts +67 -0
- package/dist/plugins/static.d.ts +16 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.js +72 -0
- package/dist/response-FIW5C377.js +5 -0
- package/dist/response.d.ts +23 -0
- package/dist/router.d.ts +31 -0
- package/dist/schema.d.ts +149 -0
- package/dist/serve-GGEVATIS.js +101 -0
- package/dist/server/request-store.d.ts +48 -0
- package/dist/server/request-store.js +2 -0
- package/dist/static-4LGL4QSH.js +8 -0
- package/dist/types.d.ts +112 -0
- package/dist/upgrade-HCGQKTSH.js +8 -0
- package/dist/vite/index.d.ts +19 -0
- package/dist/vite/index.js +41 -0
- package/dist/vue/index.d.ts +29 -0
- package/dist/vue/index.js +55 -0
- package/dist/ws/connection.d.ts +37 -0
- package/dist/ws/crypto.d.ts +24 -0
- package/dist/ws/hub.d.ts +11 -0
- package/dist/ws/types.d.ts +47 -0
- package/dist/ws/upgrade.d.ts +9 -0
- package/package.json +171 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandre Kohler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.es.md
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<a href="https://kwy404.github.io/Raiko.js/">
|
|
4
|
+
<picture>
|
|
5
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/kwy404/Raiko.js@main/assets/banner-dark.svg">
|
|
6
|
+
<img src="https://cdn.jsdelivr.net/gh/kwy404/Raiko.js@main/assets/banner.svg" alt="Raiko.js: backend and frontend in one file tree, every runtime" width="820">
|
|
7
|
+
</picture>
|
|
8
|
+
</a>
|
|
9
|
+
|
|
10
|
+
### Backend y frontend en un solo árbol de archivos. En cualquier runtime.
|
|
11
|
+
|
|
12
|
+
**Un framework de TypeScript con un app router al estilo de Next.js, server components, una API tipada, WebSockets cifrados, autenticación con 2FA, un panel de administración y envío de correos. Funciona en Bun, Node, Deno, Cloudflare Workers y Vercel.**
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/raiko.js)
|
|
15
|
+
[](https://github.com/kwy404/Raiko.js/actions/workflows/ci.yml)
|
|
16
|
+
[](https://www.npmjs.com/package/raiko.js)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](https://kwy404.github.io/Raiko.js/docs/)
|
|
19
|
+
|
|
20
|
+
**[Sitio web](https://kwy404.github.io/Raiko.js/) · [Documentación](https://kwy404.github.io/Raiko.js/docs/) · [Benchmarks](#benchmarks) · [Comparación](#cómo-se-compara) · [GitHub](https://github.com/kwy404/Raiko.js) · [Read in English](README.md) · [Leia em Português](README.pt-BR.md)**
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
## Empieza con un solo comando
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx create-raiko@latest my-app
|
|
28
|
+
# or
|
|
29
|
+
bun create raiko my-app
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
El instalador te pregunta qué necesitas y genera un proyecto que ya funciona: frontend (Raiko Pages con Voodoo.js, React, Vue, Svelte, Solid, Preact o Angular), CSS (Monsoon, Tailwind, StyleX, UnoCSS), íconos, base de datos (Kastor, Prisma o Drizzle sobre SQLite o PostgreSQL), inicio de sesión (correo y contraseña, GitHub, Google, Discord, GitLab), autenticación de dos factores, proveedor de correo, traducciones, roles de administración, un banner de cookies, una app instalable y un destino de despliegue. Cada combinación se instala, se verifica con el chequeo de tipos, se compila y se arranca en CI.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd my-app
|
|
36
|
+
npm run dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## El recorrido de 30 segundos
|
|
40
|
+
|
|
41
|
+
Una API con validación, tipos y documentación OpenAPI a partir de un solo esquema:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Raiko, t } from 'raiko.js'
|
|
45
|
+
|
|
46
|
+
const app = new Raiko()
|
|
47
|
+
.get('/', 'Hello')
|
|
48
|
+
.get('/users/:id', ({ params }) => ({ id: params.id }))
|
|
49
|
+
.post('/users', ({ body, status }) => status(201, { id: 1, ...body }), {
|
|
50
|
+
body: t.Object({ email: t.String({ format: 'email' }), name: t.String({ minLength: 2 }) }),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
app.listen(3000)
|
|
54
|
+
export default app
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Una página es un server component asíncrono. Lee la base de datos y no envía nada de JavaScript a menos que lo pidas:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// app/blog/[slug]/page.tsx
|
|
61
|
+
import { notFound } from 'raiko.js/server'
|
|
62
|
+
import { db } from '../../../db'
|
|
63
|
+
|
|
64
|
+
export const revalidate = 60
|
|
65
|
+
export async function generateMetadata({ params }) {
|
|
66
|
+
return { title: params.slug }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default async function Post({ params }) {
|
|
70
|
+
const post = await db.posts.findUnique({ where: { slug: params.slug } })
|
|
71
|
+
if (!post) notFound()
|
|
72
|
+
return <article><h1>{post.title}</h1><p>{post.body}</p></article>
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
El cliente conoce cada ruta, parámetro, body y respuesta:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { client } from 'raiko.js/client'
|
|
80
|
+
import type { App } from './server'
|
|
81
|
+
|
|
82
|
+
const api = client<App>('https://api.example.com')
|
|
83
|
+
const { data, error } = await api.post('/users', { body: { email: 'ana@raiko.dev', name: 'Ana' } })
|
|
84
|
+
// ^ { id: number; email: string; name: string } error.status is 422 with the exact field
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Contenido
|
|
88
|
+
|
|
89
|
+
- [Por qué Raiko](#por-qué-raiko)
|
|
90
|
+
- [Rutas y handlers](#rutas-y-handlers)
|
|
91
|
+
- [Validación](#validación)
|
|
92
|
+
- [Hooks del ciclo de vida](#hooks-del-ciclo-de-vida)
|
|
93
|
+
- [Estado, decoradores y plugins](#estado-decoradores-y-plugins)
|
|
94
|
+
- [Respuestas y streaming](#respuestas-y-streaming)
|
|
95
|
+
- [App router](#app-router)
|
|
96
|
+
- [Páginas protegidas](#páginas-protegidas)
|
|
97
|
+
- [Componentes](#componentes)
|
|
98
|
+
- [Cliente tipado y hooks](#cliente-tipado-y-hooks)
|
|
99
|
+
- [WebSockets](#websockets)
|
|
100
|
+
- [Auth y 2FA](#auth-y-2fa)
|
|
101
|
+
- [Panel de administración](#panel-de-administración)
|
|
102
|
+
- [Correo electrónico](#correo-electrónico)
|
|
103
|
+
- [Traducciones](#traducciones)
|
|
104
|
+
- [Plugins incluidos](#plugins-incluidos)
|
|
105
|
+
- [Errores que te dicen cómo arreglarlos](#errores-que-te-dicen-cómo-arreglarlos)
|
|
106
|
+
- [Cualquier frontend](#cualquier-frontend)
|
|
107
|
+
- [Despliegue](#despliegue)
|
|
108
|
+
- [Seguridad](#seguridad)
|
|
109
|
+
- [Benchmarks](#benchmarks)
|
|
110
|
+
- [Cómo se compara](#cómo-se-compara)
|
|
111
|
+
- [Problemas de Next.js que resuelve](#problemas-de-nextjs-que-resuelve)
|
|
112
|
+
- [Pruebas](#pruebas)
|
|
113
|
+
|
|
114
|
+
## Por qué Raiko
|
|
115
|
+
|
|
116
|
+
| | |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| **App router como en Next.js** | Archivos `page`, `layout`, `loading`, `error`, `not-found`, `forbidden`, `unauthorized`, `route` y `socket`, metadata, form actions e ISR. |
|
|
119
|
+
| **Server components sin React** | TSX asíncrono renderizado a un string o a un stream con `<Suspense>`. Cero JavaScript en el cliente por defecto. |
|
|
120
|
+
| **Interactividad sin build** | Pon `v-data` y `v-on:click` en tu TSX y Voodoo.js se agrega solo a esa página. |
|
|
121
|
+
| **Tipos del servidor al cliente** | Params, query, body y cada status de respuesta llegan a `client<App>()`. Sin generación de código. |
|
|
122
|
+
| **WebSockets en cualquier runtime** | Bun, Node (con su propia implementación de RFC 6455, sin dependencias), Deno y Workers, con salas y validación. |
|
|
123
|
+
| **Mensajes cifrados** | `encrypt: true` agrega intercambio de claves ECDH y AES-GCM, con una clave por dirección y protección contra replay. |
|
|
124
|
+
| **P2P** | Señalización WebRTC y un cliente mesh para data channels y medios. |
|
|
125
|
+
| **Auth** | OAuth con PKCE (GitHub, Google, Discord, GitLab, cualquier OAuth 2), credenciales con PBKDF2, 2FA con TOTP, sesiones JWT firmadas. |
|
|
126
|
+
| **Panel de administración** | CRUD, búsqueda, ordenamiento y paginación con permisos por rol y por acción, para Kastor, Prisma y Drizzle. |
|
|
127
|
+
| **Correo electrónico** | Plantillas en TSX con vista previa en vivo, enviadas por Resend, Postmark, SendGrid o SMTP. |
|
|
128
|
+
| **Errores al estilo Rust** | Cada error tiene un código, el archivo y la línea, el fragmento de código fuente y una línea `help:`. |
|
|
129
|
+
| **Rápido** | Rutas rápidas compiladas para cada ruta y el router nativo de Bun. Mira los [benchmarks](#benchmarks). |
|
|
130
|
+
| **Probado para romperse** | 107 tests, más de 3,4 millones de verificaciones de propiedades generadas, dos auditorías de seguridad, 11 plantillas probadas de punta a punta. |
|
|
131
|
+
|
|
132
|
+
## Rutas y handlers
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
new Raiko()
|
|
136
|
+
.get('/', 'static text') // constant: served by Bun's native router, no JS per request
|
|
137
|
+
.get('/users/:id', ({ params }) => params.id) // params are typed from the path
|
|
138
|
+
.get('/files/*', ({ params }) => params['*']) // wildcard
|
|
139
|
+
.get('/archive/:year?', ({ params }) => params.year ?? 'all')
|
|
140
|
+
.group('/v1', (v1) => v1.get('/health', 'ok'))
|
|
141
|
+
.all('/echo', ({ request }) => request.method)
|
|
142
|
+
.route('PROPFIND', '/dav', () => 'custom method')
|
|
143
|
+
.mount('/legacy', otherFetchHandler) // mount Hono, another app, any fetch handler
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
El router es un árbol radix con un hash map al frente para las rutas estáticas, con backtracking para que gane la ruta más específica, y con `HEAD`, `OPTIONS` y `405` automáticos con el header `Allow`.
|
|
147
|
+
|
|
148
|
+
## Validación
|
|
149
|
+
|
|
150
|
+
`t` construye esquemas con inferencia completa, convierte los strings que llegan de params, query y formularios, y reporta la ruta exacta de cada problema:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { t } from 'raiko.js'
|
|
154
|
+
|
|
155
|
+
const User = t.Object({
|
|
156
|
+
email: t.String({ format: 'email' }),
|
|
157
|
+
age: t.Optional(t.Integer({ minimum: 13 })),
|
|
158
|
+
tags: t.Array(t.String(), { maxItems: 10 }),
|
|
159
|
+
role: t.Enum(['user', 'admin']),
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Zod, Valibot y ArkType también funcionan, gracias a Standard Schema: pásalos en cualquier lugar donde iría un esquema `t`.
|
|
164
|
+
|
|
165
|
+
Cuando la validación falla, la respuesta es un `422` con problem details según RFC 9457:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "status": 422, "detail": "body.email: Expected a valid email", "errors": [{ "path": ["email"], "on": "body" }] }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Hooks del ciclo de vida
|
|
172
|
+
|
|
173
|
+
`onRequest`, `onParse`, `onTransform`, `derive`, validación, `resolve`, `onBeforeHandle`, handler, `onAfterHandle`, `mapResponse`, `onAfterResponse` y `onError`. Un hook se aplica a las rutas registradas después de él. Pasa `'global'` para que también se aplique a la app que use tu plugin.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
app
|
|
177
|
+
.derive(({ headers }) => ({ token: headers.authorization?.slice(7) }))
|
|
178
|
+
.onBeforeHandle(({ token }) => (token ? undefined : new Response('Unauthorized', { status: 401 })))
|
|
179
|
+
.get('/me', ({ token }) => ({ token }))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Estado, decoradores y plugins
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const db = new Raiko({ name: 'db' }).decorate('db', createDb()).state('hits', 0)
|
|
186
|
+
|
|
187
|
+
const app = new Raiko()
|
|
188
|
+
.use(db) // deduplicated by name
|
|
189
|
+
.get('/hits', ({ store }) => ++store.hits)
|
|
190
|
+
.get('/users', ({ db }) => db.users.findMany())
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Respuestas y streaming
|
|
194
|
+
|
|
195
|
+
Devuelve lo que quieras: los objetos se convierten en JSON, los strings en texto, el JSX en HTML, los generadores en Server Sent Events, y `Blob`, `File`, `ReadableStream` y `Response` pasan tal cual. `status(201, body)` define el código y tipa el cliente según cada status.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { sse } from 'raiko.js'
|
|
199
|
+
|
|
200
|
+
app.get('/ticks', async function* () {
|
|
201
|
+
for (let i = 0; i < 5; i++) yield sse({ i }, { event: 'tick' })
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## App router
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
app/
|
|
209
|
+
layout.tsx root layout, metadata
|
|
210
|
+
page.tsx /
|
|
211
|
+
loading.tsx streamed fallback while pages load
|
|
212
|
+
error.tsx error boundary
|
|
213
|
+
not-found.tsx 404
|
|
214
|
+
forbidden.tsx 403
|
|
215
|
+
blog/[slug]/page.tsx /blog/:slug
|
|
216
|
+
docs/[...path]/page.tsx
|
|
217
|
+
(marketing)/about/page.tsx route group, not in the URL
|
|
218
|
+
api/users/route.ts export GET, POST...
|
|
219
|
+
chat/socket.ts WebSocket
|
|
220
|
+
middleware.ts
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { Raiko } from 'raiko.js'
|
|
225
|
+
import { appRouter } from 'raiko.js/app'
|
|
226
|
+
|
|
227
|
+
const app = new Raiko().use(await appRouter({ dir: './app' }))
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Las páginas reciben `params` y `searchParams`, exportan `metadata` o `generateMetadata`, `revalidate` para el renderizado en caché y `actions` para formularios (`<Form action="like">` envía a `?/like`). Dentro de cualquier componente: `headers()`, `cookies()`, `usePathname()`, `useParams()`, `useSearchParams()`, `currentUser()`, `notFound()`, `redirect()`, `forbidden()`, `unauthorized()`.
|
|
231
|
+
|
|
232
|
+
`sitemap.xml` y `robots.txt` se generan a partir de tus páginas. En desarrollo, cada página tiene un overlay de errores, un badge de DevTools (ruta, params, tiempo de renderizado, caché) y un verificador de accesibilidad.
|
|
233
|
+
|
|
234
|
+
## Páginas protegidas
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
// app/dashboard/page.tsx
|
|
238
|
+
export const auth = true // signed in users only
|
|
239
|
+
export default function Dashboard() { ... }
|
|
240
|
+
|
|
241
|
+
// app/admin/layout.tsx
|
|
242
|
+
export const roles = ['admin'] // every page under app/admin
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
appRouter({ getUser: async (ctx) => (await auth.getSession(ctx.request))?.user })
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Los usuarios sin sesión van a `/login?callbackUrl=...` (o a tu `unauthorized.tsx`). Los usuarios sin el rol reciben `forbidden.tsx` con status 403.
|
|
250
|
+
|
|
251
|
+
## Componentes
|
|
252
|
+
|
|
253
|
+
`<Link>` con prefetch, `<Image>` con `srcset`, carga diferida y redimensionamiento con sharp cuando está instalado, `<Script>` con estrategias de carga, `<Head>`, `<JsonLd>`, `<SkipLink>` y `<Form>`.
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import { Image, Link, JsonLd } from 'raiko.js/components'
|
|
257
|
+
|
|
258
|
+
<Image src="/hero.png" alt="Product screenshot" width={1200} height={630} priority />
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Cliente tipado y hooks
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const api = client<App>('', { retry: 2, timeout: 10_000, headers: () => ({ authorization: `Bearer ${token}` }) })
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Hooks de React y Vue para queries, mutations y sockets:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { useQuery, useSocket } from 'raiko.js/react' // or 'raiko.js/vue'
|
|
271
|
+
|
|
272
|
+
const users = useQuery(() => api.get('/users'), { key: 'users' })
|
|
273
|
+
const chat = useSocket(() => api.ws('/chat'), (msg) => setMessages((m) => [...m, msg]))
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## WebSockets
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
app.ws('/chat/:room', {
|
|
280
|
+
body: t.Object({ text: t.String({ maxLength: 500 }) }),
|
|
281
|
+
encrypt: true,
|
|
282
|
+
open: (ws) => ws.subscribe(ws.data.params.room),
|
|
283
|
+
message: (ws, msg) => ws.publish(ws.data.params.room, { from: ws.id, text: msg.text }),
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
El cliente del navegador se reconecta con backoff, encola los mensajes mientras no hay conexión y se encarga del intercambio de claves. Con `encrypt: true`, las claves se acuerdan con ECDH P-256, cada dirección obtiene su propia clave AES-GCM 256 derivada con HKDF y los nonces son contadores de frames, así que se rechazan los frames repetidos, reordenados o reflejados. Todo esto corre sobre TLS (`wss://`): mantiene el contenido de los mensajes fuera del alcance de proxies y logs, pero no del propio navegador del usuario.
|
|
288
|
+
|
|
289
|
+
P2P con WebRTC:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { signaling } from 'raiko.js/p2p'
|
|
293
|
+
app.use(signaling({ path: '/p2p/:room' }))
|
|
294
|
+
|
|
295
|
+
// browser
|
|
296
|
+
import { joinRoom } from 'raiko.js/p2p/client'
|
|
297
|
+
const room = joinRoom(api.ws('/p2p/lobby'))
|
|
298
|
+
room.on('message', (from, data) => console.log(from, data))
|
|
299
|
+
room.send({ hi: true })
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Auth y 2FA
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { raikoAuth, GitHub, Google, Credentials, verifyPassword } from 'raiko.js/auth'
|
|
306
|
+
|
|
307
|
+
export const auth = raikoAuth({
|
|
308
|
+
secret: process.env.AUTH_SECRET!,
|
|
309
|
+
providers: [
|
|
310
|
+
GitHub({ clientId, clientSecret }),
|
|
311
|
+
Credentials({ authorize: async ({ email, password }) => /* your lookup */ null }),
|
|
312
|
+
],
|
|
313
|
+
twoFactor: { getSecret: (user) => user.totpSecret },
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
app.use(auth.plugin).get('/me', ({ auth }) => auth?.user, { beforeHandle: auth.protect })
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
OAuth usa PKCE y un state firmado. Las contraseñas usan PBKDF2 SHA-256 con 210.000 iteraciones. TOTP funciona con Google Authenticator, Authy y 1Password, permite cinco intentos por inicio de sesión y rechaza un código ya usado. `generateTotpSecret()`, `totpUri()` y `recoveryCodes()` te ayudan a construir la pantalla de configuración.
|
|
320
|
+
|
|
321
|
+
## Panel de administración
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { admin, modelAdapter } from 'raiko.js/admin'
|
|
325
|
+
|
|
326
|
+
app.use(admin({
|
|
327
|
+
auth: ({ auth }) => auth?.user.role ?? null,
|
|
328
|
+
resources: {
|
|
329
|
+
users: {
|
|
330
|
+
adapter: modelAdapter(db.users, { searchable: ['email'] }),
|
|
331
|
+
fields: ['id', 'email', 'name', 'role'],
|
|
332
|
+
hidden: ['passwordHash'],
|
|
333
|
+
permissions: { admin: '*', support: ['read', 'update'], viewer: ['list'] },
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
}))
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Cada acción (`list`, `read`, `create`, `update`, `delete`) se verifica por separado, así que un rol de soporte puede corregir un usuario sin poder listarlo ni eliminarlo. `modelAdapter` funciona con Kastor y Prisma, y `drizzleAdapter` con Drizzle.
|
|
340
|
+
|
|
341
|
+
## Correo electrónico
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
// emails/welcome.tsx
|
|
345
|
+
/** @jsxImportSource raiko.js */
|
|
346
|
+
export default function Welcome({ name }: { name: string }) {
|
|
347
|
+
return <h1>Welcome, {name}</h1>
|
|
348
|
+
}
|
|
349
|
+
Welcome.subject = (p) => `Welcome, ${p.name}`
|
|
350
|
+
Welcome.preview = { name: 'Ana' }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { createMailer, resend } from 'raiko.js/mail'
|
|
355
|
+
|
|
356
|
+
const mailer = createMailer({ from: 'hi@my.app', transport: resend({ apiKey }), templates: { welcome: Welcome } })
|
|
357
|
+
await mailer.send({ to: 'ana@x.io', template: 'welcome', props: { name: 'Ana' } })
|
|
358
|
+
app.use(mailer.preview()) // /_raiko/mail lists and renders every template
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Para cada correo se genera una versión en texto plano. Las plantillas también pueden devolver HTML de React Email, Vue o cualquier string.
|
|
362
|
+
|
|
363
|
+
## Traducciones
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { createI18n } from 'raiko.js/i18n'
|
|
367
|
+
|
|
368
|
+
const i18n = createI18n({ en, 'pt-BR': ptBR }, { fallback: 'en', cookie: 'locale' })
|
|
369
|
+
i18n.t('pt-BR', 'cart.items', { count: 3 }) // "3 itens", plurals with Intl.PluralRules
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Las claves se tipan a partir de tu diccionario de respaldo. La detección del idioma lee el prefijo de la URL, una cookie y `Accept-Language`.
|
|
373
|
+
|
|
374
|
+
## Plugins incluidos
|
|
375
|
+
|
|
376
|
+
Desde `raiko.js/plugins`: `cors`, `secureHeaders`, `rateLimit`, `csrf`, `logger`, `requestId`, `compress`, `etag`, `responseCache`, `health`, `staticFiles` y `openapi` (OpenAPI 3.1 con una página de documentación en `/docs`).
|
|
377
|
+
|
|
378
|
+
## Errores que te dicen cómo arreglarlos
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
error[RK0104]: two routes use different param names at the same position
|
|
382
|
+
--> app/blog/[slug]/page.tsx
|
|
383
|
+
|
|
|
384
|
+
3 | app/blog/[id]/page.tsx
|
|
385
|
+
| ^^^^ already declared as [slug]
|
|
386
|
+
|
|
|
387
|
+
= help: rename the folder to [slug], or move one of the pages
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Cada error con código (`RK0100` en adelante) tiene su página en la [referencia de errores](https://kwy404.github.io/Raiko.js/docs/errors.html).
|
|
391
|
+
|
|
392
|
+
## Cualquier frontend
|
|
393
|
+
|
|
394
|
+
Las Raiko Pages se renderizan en el servidor. Para una single page app, el plugin de Vite ejecuta el backend dentro del servidor de desarrollo de Vite, así React, Vue, Svelte, Solid, Preact o Angular comparten un solo puerto con la API:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
// vite.config.ts
|
|
398
|
+
import react from '@vitejs/plugin-react'
|
|
399
|
+
import { raiko } from 'raiko.js/vite'
|
|
400
|
+
|
|
401
|
+
export default { plugins: [react(), raiko({ entry: './server/index.ts' })] }
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Los builds de producción salen minificados y ofuscados, sin source maps. La ofuscación hace más lenta la lectura del código, pero no la impide: los secretos y las reglas se quedan en el servidor.
|
|
405
|
+
|
|
406
|
+
## Despliegue
|
|
407
|
+
|
|
408
|
+
| Destino | Cómo |
|
|
409
|
+
| --- | --- |
|
|
410
|
+
| Bun, Node, Deno | `app.listen(3000)`, o `raiko build` y luego `raiko start` |
|
|
411
|
+
| Varios núcleos | `app.listen(3000, { cluster: true })`: un proceso por núcleo en el mismo puerto |
|
|
412
|
+
| Vercel | `export default { fetch: app.fetch }` en `api/index.ts` (el instalador lo escribe) |
|
|
413
|
+
| Cloudflare Workers | `export default { fetch: app.fetch }`, `wrangler deploy` |
|
|
414
|
+
| Docker | el instalador escribe un `Dockerfile` multi stage que no corre como root |
|
|
415
|
+
|
|
416
|
+
`raiko deploy` elige el destino según tu proyecto.
|
|
417
|
+
|
|
418
|
+
## Seguridad
|
|
419
|
+
|
|
420
|
+
Valores por defecto que resisten ataques, cada uno respaldado por un test:
|
|
421
|
+
|
|
422
|
+
- El HTML se escapa en todas partes, y los nombres de atributos y etiquetas se validan.
|
|
423
|
+
- El parseo de query, cookies y formularios usa objetos con prototipo null (sin prototype pollution).
|
|
424
|
+
- Los archivos estáticos y el endpoint de imágenes rechazan path traversal.
|
|
425
|
+
- Las cookies se pueden firmar, y las firmas tienen una única codificación canónica.
|
|
426
|
+
- `cors({ credentials: true })` se niega a funcionar sin una lista explícita de orígenes.
|
|
427
|
+
- Las redirecciones después del inicio de sesión y de las form actions se quedan en tu sitio.
|
|
428
|
+
- Los límites de body se aplican a las subidas chunked y multipart.
|
|
429
|
+
- Los errores de validación de respuestas nunca envían datos del servidor al cliente.
|
|
430
|
+
- La caché nunca le sirve a un usuario la respuesta de otro.
|
|
431
|
+
- El rate limiter no confía en `X-Forwarded-For` a menos que se lo indiques.
|
|
432
|
+
|
|
433
|
+
Dos auditorías independientes encontraron 21 problemas antes de esta versión. Todos están corregidos, y cada uno tiene un test que reproduce el ataque. Reporta una vulnerabilidad a través de [SECURITY.md](SECURITY.md).
|
|
434
|
+
|
|
435
|
+
## Benchmarks
|
|
436
|
+
|
|
437
|
+
Requests por segundo, más es mejor. Las mismas tres rutas en cada framework, sobre Bun, por HTTP real con autocannon (64 conexiones, pipelining 4), mediana de tres rondas.
|
|
438
|
+
|
|
439
|
+
<!-- bench:start -->
|
|
440
|
+
| Server rendered page | Raiko.js (Bun) | Raiko.js (Node) | Next.js 15 (Node) |
|
|
441
|
+
| --- | ---: | ---: | ---: |
|
|
442
|
+
| req/s | 8,910 | 3,737 | 155 |
|
|
443
|
+
|
|
444
|
+
| Route | Raiko.js | Elysia 1.4 | Hono 4.13 |
|
|
445
|
+
| --- | ---: | ---: | ---: |
|
|
446
|
+
| text | 99,040 | 83,192 | 63,920 |
|
|
447
|
+
| params + json | 66,943 | 75,723 | 60,477 |
|
|
448
|
+
| json body | 51,136 | 53,168 | 47,029 |
|
|
449
|
+
|
|
450
|
+
AMD EPYC 9J45, 1 vCPU, 1.9 GB RAM, Linux 6.8, Bun 1.4.4, Node 22.23. autocannon -c 64 -p 4, median of 3 rounds.
|
|
451
|
+
<!-- bench:end -->
|
|
452
|
+
|
|
453
|
+
Notas honestas: la máquina tiene una sola vCPU, así que el generador de carga y el servidor la comparten y los números quedan comprimidos. En las rutas estáticas, Raiko y Elysia responden desde el router nativo de Bun sin ejecutar JavaScript, por lo que empatan en el techo. Ejecuta `bun bench/http.ts` en tu propio hardware.
|
|
454
|
+
|
|
455
|
+
## Cómo se compara
|
|
456
|
+
|
|
457
|
+
| | Raiko.js | Elysia | Next.js | Hono |
|
|
458
|
+
| --- | --- | --- | --- | --- |
|
|
459
|
+
| Runtimes | Bun, Node, Deno, Workers, Vercel | Bun primero | Node, Vercel | Todos |
|
|
460
|
+
| App router, layouts, loading, error | Sí | No | Sí | No |
|
|
461
|
+
| Server components | Sí, sin necesidad de React | No | Sí | Solo JSX |
|
|
462
|
+
| Cliente tipado | Sí | Sí (Eden) | Server actions | Sí (RPC) |
|
|
463
|
+
| WebSockets en cualquier runtime | Sí | Bun | No | Según el adaptador |
|
|
464
|
+
| Mensajes de WebSocket cifrados | Sí | No | No | No |
|
|
465
|
+
| Auth con OAuth y 2FA | Incluido | Plugins | NextAuth | Middleware |
|
|
466
|
+
| Panel de administración con permisos | Incluido | No | No | No |
|
|
467
|
+
| Plantillas de correo con vista previa | Incluido | No | No | No |
|
|
468
|
+
| Tamaño del ecosistema | Nuevo | En crecimiento | Enorme | Grande |
|
|
469
|
+
|
|
470
|
+
## Problemas de Next.js que resuelve
|
|
471
|
+
|
|
472
|
+
- **Un solo proceso para las páginas y la API.** Sin backend aparte, sin tipos duplicados.
|
|
473
|
+
- **No necesitas React en el servidor.** Las páginas son funciones asíncronas comunes que devuelven TSX.
|
|
474
|
+
- **WebSockets de verdad.** Next.js no tiene servidor de WebSockets, y Raiko tiene uno en cada runtime.
|
|
475
|
+
- **Caché predecible.** Solo se guardan en caché las páginas que exportan `revalidate`, nunca las requests personalizadas.
|
|
476
|
+
- **Funciona en cualquier lado.** La misma app se despliega en Bun, Node, Deno, Workers y Vercel sin un proyecto adaptador.
|
|
477
|
+
- **Errores con los que puedes hacer algo.** Errores con código y su solución, no un stack trace minificado.
|
|
478
|
+
- **Auth, administración y correo incluidos.** Lo que todo producto necesita no son librerías separadas que tengas que conectar.
|
|
479
|
+
|
|
480
|
+
## Pruebas
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
const res = await app.request('/users', { json: { email: 'a@b.co', name: 'Ana' } })
|
|
484
|
+
expect(res.status).toBe(201)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Sin servidor, sin puerto: `app.request()` y `app.fetch()` ejecutan el ciclo de vida completo en memoria.
|
|
488
|
+
|
|
489
|
+
## Más proyectos de kwy404
|
|
490
|
+
|
|
491
|
+
| Proyecto | Qué hace |
|
|
492
|
+
| --- | --- |
|
|
493
|
+
| [Voodoo.js](https://github.com/kwy404/Voodoo.js) | JSX en HTML plano sin paso de build, usado para la interactividad en Raiko Pages |
|
|
494
|
+
| [Monsoon](https://github.com/kwy404/monsoon) | CSS utility first sin paso de build, un tema a partir de un solo color |
|
|
495
|
+
| [glyphfox](https://github.com/kwy404/glyphfox) | Íconos transformables para cualquier framework |
|
|
496
|
+
| [Kastor](https://github.com/kwy404/kastor) | El ORM de TypeScript para casi cualquier base de datos |
|
|
497
|
+
| [a11ymedic](https://github.com/kwy404/a11ymedic) | Verificador de accesibilidad que explica cómo arreglar cada problema |
|
|
498
|
+
| [cookieowl](https://github.com/kwy404/cookieowl) | Consentimiento de cookies para LGPD y GDPR que de verdad bloquea los trackers |
|
|
499
|
+
| [installkite](https://github.com/kwy404/installkite) | Haz que cualquier sitio web sea instalable y funcione sin conexión |
|
|
500
|
+
| [edgegoat](https://github.com/kwy404/edgegoat) | Entradas de prueba para casos límite en JavaScript |
|
|
501
|
+
|
|
502
|
+
## Autor
|
|
503
|
+
|
|
504
|
+
Creado por **Alexandre Kohler** ([GitHub](https://github.com/kwy404)).
|
|
505
|
+
|
|
506
|
+
## Licencia
|
|
507
|
+
|
|
508
|
+
[MIT](LICENSE)
|