vuelo-framework 1.0.0

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/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # 🚀 Vuelo
2
+
3
+ **Vuelo** es un framework ágil y ligero que combina la magia de Vue con la
4
+ velocidad impresionante de Bun. Diseñado para aquellos desarrolladores que
5
+ quieren crear aplicaciones web interactivas y de alto rendimiento, Vuelo
6
+ transforma la codificación en una experiencia placentera, ¡como un vuelo suave
7
+ hacia el éxito!
8
+
9
+ ## ¿Por qué Vuelo?
10
+
11
+ Porque tu código merece ser elevado a otro nivel. Aquí cada componente es un
12
+ pasajero feliz en un vuelo directo hacia la productividad.
13
+
14
+ 1. **Amo Vue**: La reactividad de Vue es un superpoder que te permite crear
15
+ interfaces interactivas sin sudar la gota gorda. Su sistema de componentes es
16
+ tan intuitivo que te sentirás como un maestro de ceremonias en un espectáculo
17
+ de fuegos artificiales.
18
+
19
+ 2. **Velocidad del servidor de Bun**: Hablemos de velocidad: el servidor de Bun
20
+ es **fast as f*ck**. Maneja miles de solicitudes por segundo como un ninja,
21
+ todo mientras te mantiene en la zona de desarrollo.
22
+
23
+ 3. **Simplicidad**: Vuelo combina lo mejor de Vue y la velocidad de Bun,
24
+ permitiéndote enfocarte en lo que realmente importa: construir algo increíble
25
+ sin complicaciones innecesarias.
26
+
27
+ 4. **Estático desde 0**: Desde el principio, todas las páginas y componentes
28
+ (excepto las futuras islas) son completamente estáticos. Tu servidor envía
29
+ únicamente HTML al cliente, asegurando que tu aplicación despegue con
30
+ velocidad y eficiencia.
31
+
32
+ 5. **Autoimportación de rutas**: En Vuelo, la carpeta `pages` es tu puerta de
33
+ embarque. Cada archivo en `pages` se convierte en una ruta automáticamente,
34
+ así que solo colócalo y ¡bam!, ¡listo para volar!
35
+
36
+ ## Funcionalidad de Islas
37
+
38
+ Vuelo admite la funcionalidad de islas, que permite que componentes individuales
39
+ se carguen de manera interactiva. Esta funcionalidad se activa mediante los
40
+ atributos `data-hydrate` y `data-hydrate-event`.
41
+
42
+ - **data-hydrate="Counter"**: Este atributo debe coincidir con el componente que
43
+ se encargará de hidratarlo.
44
+ - **data-hydrate-event="load"**: Este atributo indica el evento que se utilizará
45
+ para hidratar el componente.
46
+
47
+ Si no se incluyen estos atributos, el componente se renderizará de forma plana y
48
+ no aprovechará la funcionalidad de hidratación. Además, los componentes deben
49
+ ubicarse en la carpeta `islands` para que sean servidos y detectados por el
50
+ hidratador.
51
+
52
+ Es importante mencionar que los componentes deben ser de Vue 2, ya que son
53
+ montados por Vue en el front, en el contenedor donde estaba el componente
54
+ anterior.
55
+
56
+ Aquí tienes ejemplos de uso:
57
+
58
+ ```html
59
+ <Counter></Counter>
60
+ <Counter data-hydrate="Counter" data-hydrate-event="load"></Counter>
61
+ <Counter data-hydrate="Counter" data-hydrate-event="click"></Counter>
62
+ <Counter data-hydrate="Counter" data-hydrate-event="mouseover"></Counter>
63
+ ```
64
+
65
+ ## Instalación como Paquete NPM
66
+
67
+ Para usar Vuelo Framework en tu proyecto, instálalo con tu gestor de paquetes preferido:
68
+
69
+ ```bash
70
+ # Con npm
71
+ npm install vuelo-framework
72
+
73
+ # Con yarn
74
+ yarn add vuelo-framework
75
+
76
+ # Con pnpm
77
+ pnpm add vuelo-framework
78
+
79
+ # Con bun
80
+ bun add vuelo-framework
81
+ ```
82
+
83
+ ## Uso Rápido
84
+
85
+ Después de instalar, crea un archivo `index.ts` en la raíz de tu proyecto:
86
+
87
+ ```typescript
88
+ import { vuelo } from "vuelo-framework";
89
+
90
+ const vueloServer = await vuelo({
91
+ mode: 'SSRIslands',
92
+ port: 3000
93
+ });
94
+
95
+ console.log("Vuelo running on " + vueloServer.url);
96
+ ```
97
+
98
+ Y crea la estructura de carpetas:
99
+
100
+ ```
101
+ tu-proyecto/
102
+ ├── index.ts
103
+ ├── package.json
104
+ ├── vite.config.ts
105
+ ├── tsconfig.json
106
+ └── src/
107
+ ├── pages/ # Tus páginas aquí
108
+ │ └── index.vue
109
+ └── islands/ # Componentes interactivos aquí
110
+ └── counter.vue
111
+ ```
112
+
113
+ ## Desarrollo Local
114
+
115
+ Si estás desarrollando el framework mismo, instala las dependencias:
116
+
117
+ ```bash
118
+ bun install
119
+ ```
120
+
121
+ Y ejecuta el proyecto de ejemplo:
122
+
123
+ ```bash
124
+ bun run index.ts
125
+ ```
126
+
127
+ ## Carpeta `pages`
128
+
129
+ La carpeta `pages` es donde la magia sucede. Cada archivo que pongas aquí es
130
+ como un pasajero que llega justo a tiempo para abordar. No te preocupes por las
131
+ importaciones; simplemente añade tu archivo y deja que Vuelo haga el trabajo
132
+ pesado.
133
+
134
+ ## Origen del Proyecto
135
+
136
+ Este proyecto fue creado usando `bun init` en bun v1.1.29. [Bun](https://bun.sh)
137
+ es un runtime de JavaScript que hace que tu código vuele más rápido que un jet
138
+ privado.
139
+
140
+ ¡Gracias por elegir Vuelo! Esperamos que disfrutes del viaje tanto como nosotros
141
+ disfrutamos construirlo. ¡Feliz codificación! ✈️
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "vuelo-framework",
3
+ "version": "1.0.0",
4
+ "description": "Framework Vue.js con SSR e Islands Architecture",
5
+ "type": "module",
6
+ "main": "./vueloFramework/server.ts",
7
+ "types": "./vueloFramework/server.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./vueloFramework/server.ts",
11
+ "types": "./vueloFramework/server.ts"
12
+ },
13
+ "./defaults/*": "./vueloFramework/defaults/*"
14
+ },
15
+ "files": [
16
+ "vueloFramework/**/*",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "vue",
22
+ "ssr",
23
+ "islands",
24
+ "framework",
25
+ "vite"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "prepublishOnly": "echo 'Asegúrate de haber compilado el proyecto antes de publicar'"
31
+ },
32
+ "peerDependencies": {
33
+ "typescript": "^5.0.0",
34
+ "vue": "^3.5.0"
35
+ },
36
+ "dependencies": {
37
+ "@vitejs/plugin-vue": "^5.1.4",
38
+ "@vue/compiler-sfc": "^3.5.12",
39
+ "autoprefixer": "^10.4.20",
40
+ "postcss": "^8.4.47",
41
+ "vite": "^5.4.9",
42
+ "vue": "^3.5.12"
43
+ },
44
+ "devDependencies": {
45
+ "@types/bun": "latest"
46
+ }
47
+ }
@@ -0,0 +1,17 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import { createSSRApp } from "vue";
3
+
4
+ export async function createApp(vite: ViteDevServer, component: any) {
5
+ let module;
6
+ try {
7
+ module = await vite.ssrLoadModule("src/App.vue");
8
+ } catch (error) {
9
+ console.log("App.vue no found con src using default");
10
+ module = await vite.ssrLoadModule("vueloFramework/defaults/App.vue");
11
+ }
12
+ const app = createSSRApp(module.default);
13
+ if (component) {
14
+ app.component("RouteView", component.default);
15
+ }
16
+ return {app};
17
+ }
@@ -0,0 +1,155 @@
1
+ import { readdir } from "fs/promises";
2
+ import path from "path";
3
+
4
+ export async function getImports() {
5
+ // Asegurarse de resolver desde el directorio del proyecto (cwd)
6
+ const projectRoot = process.cwd();
7
+ const pagesDir = path.resolve(projectRoot, "src/pages");
8
+ const islandsDir = path.resolve(projectRoot, "src/islands");
9
+ const componentsDir = path.resolve(projectRoot, "src/components");
10
+
11
+ const pages = await getFilesIfExists(pagesDir, ".vue");
12
+ const islands = await getFilesIfExists(islandsDir, ".vue");
13
+ const components = await getFilesIfExists(componentsDir, ".vue");
14
+
15
+ const importMap = generateImportMap(pages, islands, components);
16
+ return importMap;
17
+ }
18
+
19
+ async function getFilesIfExists(dir: string, ext: string) {
20
+ try {
21
+ const files = await readdir(dir, { withFileTypes: true });
22
+ const results: string[] = [];
23
+
24
+ for (const file of files) {
25
+ if (file.isDirectory()) {
26
+ const subDirResults = await getFilesIfExists(
27
+ path.join(dir, file.name),
28
+ ext,
29
+ );
30
+ results.push(...subDirResults);
31
+ } else if (file.isFile() && file.name.endsWith(ext)) {
32
+ results.push(path.join(dir, file.name));
33
+ }
34
+ }
35
+
36
+ return results;
37
+ } catch (error) {
38
+ // Si el directorio no existe, simplemente retorna un array vacío
39
+ return [];
40
+ }
41
+ }
42
+
43
+ async function getFiles(dir: string, ext: string) {
44
+ const files = await readdir(dir, { withFileTypes: true });
45
+ const results: string[] = [];
46
+
47
+ for (const file of files) {
48
+ if (file.isDirectory()) {
49
+ const subDirResults = await getFiles(path.join(dir, file.name), ext);
50
+ results.push(...subDirResults);
51
+ } else if (file.isFile() && file.name.endsWith(ext)) {
52
+ results.push(path.join(dir, file.name));
53
+ }
54
+ }
55
+
56
+ return results;
57
+ }
58
+
59
+ function generateImportMap(
60
+ pages: string[],
61
+ islands: string[],
62
+ components: string[],
63
+ ) {
64
+ const pageImports = pages.map((page) => {
65
+ const importName = path.basename(page, path.extname(page));
66
+ return `import * as $${importName} from "${page}";`;
67
+ });
68
+
69
+ const islandImports = islands.map((island) => {
70
+ const importName = path.basename(island, path.extname(island));
71
+ return `import * as $${importName} from "${island}";`;
72
+ });
73
+
74
+ const componentsImports = components.map((island) => {
75
+ const importName = path.basename(island, path.extname(island));
76
+ return `import * as $${importName} from "${island}";`;
77
+ });
78
+
79
+ const manifest = {
80
+ pages: pages.map((page) => {
81
+ const name = path.basename(page, ".vue");
82
+ let route = page.replace(/^.*\/pages\//, ""); // Remover la ruta hasta 'pages/'
83
+ route = route.replace(/index\.vue$/, ""); // Eliminar 'index.vue'
84
+ route = route.replace(/\.vue$/, ""); // Eliminar '.vue' de otros archivos
85
+ route = route.endsWith("/") ? route.slice(0, -1) : route; // Eliminar barra final si existe
86
+ route = `/${route}`; // Asegurar que empiece con '/'
87
+
88
+ // Calcular la ruta relativa al directorio actual
89
+ const relativePath = path.relative(__dirname, page);
90
+
91
+ return {
92
+ name,
93
+ path: relativePath, // Aquí se usa la ruta relativa
94
+ route,
95
+ };
96
+ }),
97
+ islands: islands.map((island) => {
98
+ const name = path.basename(island, ".vue");
99
+ // island ya debería ser una ruta absoluta (viene de path.resolve + path.join)
100
+ // Pero por si acaso, asegurémonos de que sea absoluta
101
+ let absolutePath = island;
102
+
103
+ if (!path.isAbsolute(island)) {
104
+ absolutePath = path.resolve(island);
105
+ }
106
+ // Si por alguna razón la ruta empieza con /src/ (ruta absoluta incorrecta),
107
+ // corregirla resolviéndola desde el directorio del proyecto
108
+ if (absolutePath.startsWith("/src/")) {
109
+ absolutePath = path.resolve(process.cwd(), absolutePath.slice(1));
110
+ }
111
+
112
+ return {
113
+ name,
114
+ path: absolutePath, // Usar la ruta absoluta
115
+ };
116
+ }),
117
+ components: components.map((component) =>
118
+ `${path.basename(component, ".vue")}`
119
+ ),
120
+ baseUrl: import.meta.url,
121
+ };
122
+
123
+ return {
124
+ imports: [...pageImports, ...islandImports, ...componentsImports],
125
+ manifest,
126
+ };
127
+ }
128
+
129
+ export async function pagesComponents(vite: any, pages: any[]) {
130
+ const routes = pages;
131
+ const components: { route: any; promise: Promise<any> }[] = []; // Almacena el objeto con la ruta y la promesa
132
+
133
+ // Recopilar todas las promesas de los componentes junto con su ruta
134
+ routes.forEach((componentRoute) => {
135
+ components.push({
136
+ route: componentRoute,
137
+ promise: vite.ssrLoadModule(componentRoute.path.replace("..", "")),
138
+ });
139
+ });
140
+
141
+ // Esperar a que todas las promesas se resuelvan
142
+ const results = await Promise.all(components.map((c) => c.promise));
143
+
144
+ // Combinar los resultados con sus rutas
145
+ const componentsToImport = components.map((c, index) => ({
146
+ route: c.route,
147
+ component: { default: results[index].default }, // Asocia el componente resuelto con su ruta
148
+ }));
149
+
150
+ return componentsToImport;
151
+ }
152
+
153
+ export async function islandsComponents(vite: any, islands: any) {
154
+ return islands;
155
+ }
@@ -0,0 +1,5 @@
1
+ import App from "./defaults/App.vue"
2
+
3
+ const app = createSSRApp(App)
4
+
5
+ app.mount('#app')
@@ -0,0 +1,14 @@
1
+ import { renderToString } from "vue/server-renderer";
2
+ import { createApp } from "./app";
3
+
4
+ export async function createVueloApp(vite: any, component: any) {
5
+ const {app} = await createApp(vite, component);
6
+ // TODO retornar un compoente de error
7
+ try {
8
+ const html = await renderToString(app);
9
+ return html;
10
+ } catch (error) {
11
+ console.error("Error al renderizar la app:", error);
12
+ return "<div>Error rendering the app</div>";
13
+ }
14
+ }
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div class="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6">
3
+ <h1 class="text-4xl font-bold text-green-600 mb-4">Bienvenido a Vuelo Framework</h1>
4
+ <p class="text-lg text-gray-700 mb-6">{{ message }}</p>
5
+ <RouteView class="w-full max-w-4xl bg-white shadow-md rounded-lg p-6"></RouteView>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { ref } from "vue";
11
+ const messages = [
12
+ "¡Bienvenidos a Vuelo Framework! Donde los bugs son solo pasajeros en tránsito.",
13
+ "Con Vuelo, tus componentes no solo funcionan, ¡sino que también hacen acrobacias!",
14
+ "¿Por qué Vuelo Framework? Porque incluso tu código merece unas vacaciones.",
15
+ "Vuelo: Donde la única turbulencia es la de tu café.",
16
+ "Usar Vuelo Framework es como tener un piloto automático para tu código.",
17
+ "Con Vuelo, tus errores se convierten en 'nubes de oportunidad'.",
18
+ "¿Vuelo Framework? Más como 'vuelo en primera clase' para tu desarrollo.",
19
+ "En Vuelo, cada componente tiene alas. ¡Despegamos!",
20
+ "Vuelo: Porque a veces el código necesita una escapada rápida.",
21
+ "Con Vuelo, tus componentes no solo son ligeros, ¡son aerodinámicos!",
22
+ "¡Usa Vuelo! Porque tu código merece ser elevado a otro nivel.",
23
+ "En Vuelo, los problemas se desvanecen como maletas perdidas.",
24
+ "¿Vuelo Framework? Más como 'Vuelo y no mires atrás'.",
25
+ "Con Vuelo, cada despliegue es un aterrizaje suave.",
26
+ "En Vuelo, la única 'carga útil' son tus ideas brillantes.",
27
+ "¡Hola, Vuelo Framework! ¿Listo para un viaje sin escalas hacia la productividad?",
28
+ "Con Vuelo, tus componentes nunca se quedan en tierra.",
29
+ "¿Problemas de rendimiento? En Vuelo, solo llevamos equipaje de mano.",
30
+ "Vuelo: La única vez que 'despegar' es una buena noticia para tu código.",
31
+ "Con Vuelo, incluso el código más pesado se siente ligero."
32
+ ];
33
+
34
+ const randomMessage = messages[Math.floor(Math.random() * messages.length)];
35
+ const message = ref(randomMessage);
36
+ </script>
37
+
38
+ <style scoped>
39
+ </style>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Vuelo App</title>
7
+
8
+ <link
9
+ href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
10
+ rel="stylesheet"
11
+ />
12
+ </head>
13
+ <body>
14
+ <div id="app"><!--vuelo-app-html--></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,267 @@
1
+ import {
2
+ createApp,
3
+ } from "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js";
4
+
5
+ export function hydrateComponents() {
6
+ const elements = document.querySelectorAll("[data-hydrate]");
7
+
8
+ console.log(`[DEBUG] Found ${elements.length} elements to hydrate`);
9
+
10
+ elements.forEach(async (element) => {
11
+ const componentName = element.getAttribute("data-hydrate");
12
+ const eventType = element.getAttribute("data-hydrate-event");
13
+
14
+ console.log(`[DEBUG] Setting up hydration for ${componentName} with event ${eventType}`);
15
+
16
+ if (!componentName || !eventType) {
17
+ console.warn(`[WARN] Missing componentName or eventType for element:`, element);
18
+ return;
19
+ }
20
+
21
+ const hydrate = async () => {
22
+ console.log(`[DEBUG] Hydration triggered for ${componentName}`);
23
+ try {
24
+ // Importar dinámicamente el componente compilado como módulo ES
25
+ const moduleUrl = `/api/islands/${componentName.toLowerCase()}`;
26
+
27
+ console.log(`[DEBUG] Fetching component from: ${moduleUrl}`);
28
+
29
+ // Crear un módulo dinámico usando import() con un data URL o fetch + eval
30
+ // Como no podemos usar import() directamente con URLs relativas en todos los navegadores,
31
+ // vamos a usar una técnica alternativa: crear un script module dinámicamente
32
+ const response = await fetch(moduleUrl);
33
+ if (!response.ok) {
34
+ console.error(`[ERROR] Failed to fetch component: ${response.status} ${response.statusText}`);
35
+ throw new Error(`Network response was not ok: ${response.status}`);
36
+ }
37
+
38
+ const compiledCode = await response.text();
39
+
40
+ console.log(`[DEBUG] Fetching component ${componentName}...`);
41
+ console.log(`[DEBUG] Code length: ${compiledCode.length}`);
42
+ console.log(`[DEBUG] First 300 chars:`, compiledCode.substring(0, 300));
43
+
44
+ let Component;
45
+
46
+ // Usar Blob URL para crear un módulo que el navegador pueda importar
47
+ // Esto permite que los imports estáticos funcionen si están correctamente resueltos
48
+ const blob = new Blob([compiledCode], { type: 'application/javascript' });
49
+ const blobUrl = URL.createObjectURL(blob);
50
+
51
+ try {
52
+ // Intentar importar el módulo usando Blob URL
53
+ // Los imports dentro del código deberían resolverse automáticamente
54
+ const module = await import(blobUrl);
55
+
56
+ // Limpiar el Blob URL después de importar
57
+ URL.revokeObjectURL(blobUrl);
58
+
59
+ console.log(`[DEBUG] Module imported:`, module);
60
+ console.log(`[DEBUG] Module keys:`, Object.keys(module));
61
+ console.log(`[DEBUG] Module.default:`, module.default);
62
+
63
+ // Obtener el componente (puede estar en default o en exports)
64
+ Component = module.default || module;
65
+
66
+ if (!Component) {
67
+ // Intentar acceder directamente a las propiedades del módulo
68
+ Component = Object.values(module).find(v => v && typeof v === 'object' && (v.render || v.setup || v.__name));
69
+ console.log(`[DEBUG] Component found in module values:`, Component);
70
+ }
71
+
72
+ // Verificar que el componente tenga las propiedades necesarias
73
+ if (Component) {
74
+ console.log(`[DEBUG] Component structure:`, {
75
+ hasRender: !!Component.render,
76
+ hasSetup: !!Component.setup,
77
+ hasTemplate: !!Component.template,
78
+ __name: Component.__name
79
+ });
80
+ }
81
+ } catch (importError) {
82
+ console.error("Blob URL import failed:", importError);
83
+ console.error("Import error details:", importError.message, importError.stack);
84
+ URL.revokeObjectURL(blobUrl);
85
+
86
+ // Fallback: usar Function constructor para código sin imports complejos
87
+ try {
88
+ console.log(`[DEBUG] Trying Function constructor fallback...`);
89
+ const moduleExports = {};
90
+ const module = { exports: moduleExports };
91
+ const exports = moduleExports;
92
+
93
+ // Ejecutar el código compilado
94
+ const moduleFunction = new Function('module', 'exports', compiledCode);
95
+ moduleFunction(module, exports);
96
+
97
+ Component = module.exports.default || module.exports;
98
+
99
+ console.log(`[DEBUG] Component from Function constructor:`, Component);
100
+ console.log(`[DEBUG] Component structure:`, {
101
+ hasRender: Component?.render ? 'yes' : 'no',
102
+ hasSetup: Component?.setup ? 'yes' : 'no',
103
+ __name: Component?.__name
104
+ });
105
+ } catch (functionError) {
106
+ console.error("Function constructor also failed:", functionError);
107
+ console.error("Function error details:", functionError.message, functionError.stack);
108
+ throw new Error(`Failed to load component ${componentName}: ${functionError.message}`);
109
+ }
110
+ }
111
+
112
+ if (!Component) {
113
+ console.error(`[ERROR] Component ${componentName} not found. Module structure:`, Object.keys(module || {}));
114
+ throw new Error(`Component ${componentName} not found in module exports`);
115
+ }
116
+
117
+ console.log(`[DEBUG] Component found:`, Component);
118
+
119
+ // Verificar que el componente sea válido
120
+ if (typeof Component !== 'object' || !Component) {
121
+ throw new Error(`Invalid component: ${Component}`);
122
+ }
123
+
124
+ // Verificar que el componente tenga render o setup
125
+ if (!Component.render && !Component.setup && !Component.template) {
126
+ console.warn(`[WARN] Component ${componentName} doesn't have render, setup, or template`);
127
+ }
128
+
129
+ // Extraer props de atributos data-*
130
+ const props = {};
131
+ for (let i = 0; i < element.attributes.length; i++) {
132
+ const attr = element.attributes[i];
133
+ if (attr.name.startsWith('data-prop-')) {
134
+ const propName = attr.name.replace('data-prop-', '');
135
+ try {
136
+ // Intentar parsear como JSON, si falla usar el valor directo
137
+ props[propName] = JSON.parse(attr.value);
138
+ } catch (e) {
139
+ props[propName] = attr.value;
140
+ }
141
+ }
142
+ }
143
+
144
+ // Guardar las clases y estilos del elemento original para preservarlos
145
+ const originalClasses = element.className;
146
+ const originalId = element.id;
147
+ const originalStyles = element.getAttribute('style') || '';
148
+
149
+ // Limpiar completamente el contenido del elemento antes de montar
150
+ // Esto asegura que Vue tenga control completo
151
+ element.innerHTML = '';
152
+
153
+ // Remover atributos de hidratación para evitar conflictos
154
+ element.removeAttribute('data-hydrate');
155
+ element.removeAttribute('data-hydrate-event');
156
+
157
+ // Crea una nueva instancia de la aplicación Vue para el cliente
158
+ // Usar createApp en lugar de createSSRApp para el cliente
159
+ const app = createApp(Component, props);
160
+
161
+ // Montar el componente directamente en el elemento
162
+ // Vue reemplazará el contenido del elemento con el componente
163
+ const instance = app.mount(element);
164
+
165
+ // Después de montar, verificar si hay un doble contenedor
166
+ // Si el elemento tiene un solo hijo que es un div sin atributos importantes,
167
+ // extraer su contenido para evitar el doble contenedor
168
+ if (element.children.length === 1) {
169
+ const wrapperDiv = element.firstElementChild;
170
+ if (wrapperDiv &&
171
+ wrapperDiv.tagName === 'DIV' &&
172
+ !wrapperDiv.id &&
173
+ !wrapperDiv.getAttribute('data-hydrate') &&
174
+ !wrapperDiv.getAttribute('data-hydrate-event')) {
175
+ // Contar atributos no importantes (solo class y style son aceptables)
176
+ const importantAttrs = Array.from(wrapperDiv.attributes).filter(attr =>
177
+ attr.name !== 'class' && attr.name !== 'style'
178
+ );
179
+
180
+ // Si solo tiene class/style o ningún atributo, podemos aplanarlo
181
+ if (importantAttrs.length === 0) {
182
+ // Preservar las clases del wrapper si las tiene
183
+ if (wrapperDiv.className && !originalClasses) {
184
+ element.className = wrapperDiv.className;
185
+ } else if (wrapperDiv.className && originalClasses) {
186
+ element.className = originalClasses + ' ' + wrapperDiv.className;
187
+ }
188
+
189
+ // Mover todos los hijos del div wrapper al elemento original
190
+ const fragment = document.createDocumentFragment();
191
+ while (wrapperDiv.firstChild) {
192
+ fragment.appendChild(wrapperDiv.firstChild);
193
+ }
194
+ element.removeChild(wrapperDiv);
195
+ element.appendChild(fragment);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Restaurar clases, id y estilos si es necesario (solo si no se restauraron arriba)
201
+ if (originalClasses && !element.className.includes(originalClasses)) {
202
+ element.className = originalClasses + (element.className ? ' ' + element.className : '');
203
+ }
204
+ if (originalId) {
205
+ element.id = originalId;
206
+ }
207
+ if (originalStyles) {
208
+ const currentStyle = element.getAttribute('style') || '';
209
+ element.setAttribute('style', originalStyles + (currentStyle ? '; ' + currentStyle : ''));
210
+ }
211
+
212
+ console.log("hydrated:", componentName);
213
+ console.log(`[DEBUG] Component mounted, instance:`, instance);
214
+
215
+ // Verificar que el componente se montó correctamente
216
+ if (!instance) {
217
+ console.warn(`[WARN] Component ${componentName} mounted but instance is null`);
218
+ }
219
+ } catch (error) {
220
+ console.error("Error hydrating component:", error);
221
+ }
222
+ };
223
+
224
+ // Variable para rastrear si ya se hidrató este elemento
225
+ let isHydrated = false;
226
+
227
+ const doHydrate = async () => {
228
+ if (isHydrated) {
229
+ console.log(`[DEBUG] Component ${componentName} already hydrated, skipping`);
230
+ return;
231
+ }
232
+ isHydrated = true;
233
+ await hydrate();
234
+ };
235
+
236
+ switch (eventType) {
237
+ case "load":
238
+ // Para load, verificar si la página ya cargó
239
+ if (document.readyState === 'complete') {
240
+ // Si ya cargó, hidratar inmediatamente
241
+ doHydrate();
242
+ } else {
243
+ // Si no, esperar al evento load
244
+ window.addEventListener("load", doHydrate, { once: true });
245
+ }
246
+ break;
247
+ case "click":
248
+ // Para click, prevenir la propagación y solo hidratar una vez
249
+ element.addEventListener("click", (e) => {
250
+ e.preventDefault();
251
+ e.stopPropagation();
252
+ doHydrate();
253
+ }, { once: true });
254
+ break;
255
+ case "mouseover":
256
+ element.addEventListener("mouseover", doHydrate, { once: true });
257
+ break;
258
+ default:
259
+ console.warn(`Unknown event type: ${eventType}`);
260
+ break;
261
+ }
262
+ });
263
+ }
264
+
265
+ document.addEventListener("DOMContentLoaded", () => {
266
+ hydrateComponents();
267
+ });
@@ -0,0 +1,5 @@
1
+ export interface VueloConfig {
2
+ port?: number;
3
+ flavor?: "bun" | "deno" | "node";
4
+ mode?: "SSR" | "SSRIslands" | "static";
5
+ }
@@ -0,0 +1,7 @@
1
+ export function resolveRouteComponent(components: any[], url: URL) {
2
+ const route = components.find((c) => c.route.route === url.pathname); // Encuentra la ruta correspondiente
3
+ if (!route) {
4
+ return null; // Retornar null si no se encuentra la ruta
5
+ }
6
+ return route.component; // Retornar el componente si se encontró la ruta
7
+ }
@@ -0,0 +1,23 @@
1
+ import { createServer } from "vite";
2
+ import { getImports, islandsComponents, pagesComponents } from "./autoImport";
3
+ import { type VueloConfig } from "./interfaces/vueloConfig";
4
+ import BunServer from "./servers/bun";
5
+
6
+ export async function vuelo(config: VueloConfig = {}) {
7
+ const defaultConfig: VueloConfig = {
8
+ port: 9876,
9
+ flavor: "bun",
10
+ mode: "SSR",
11
+ };
12
+ const finalConfig = { ...defaultConfig, ...config };
13
+ const vite = await createServer({
14
+ server: { middlewareMode: true },
15
+ });
16
+ const autoImports = await getImports();
17
+ const components = {
18
+ pages: await pagesComponents(vite, autoImports.manifest.pages),
19
+ islands: await islandsComponents(vite, autoImports.manifest.islands),
20
+ };
21
+ const server = BunServer(vite, finalConfig, components);
22
+ return server;
23
+ }
@@ -0,0 +1,131 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import { createVueloApp } from "../createVueloApp";
3
+ import { resolveRouteComponent } from "../router";
4
+ import getTemplate from "../utils/template";
5
+ import type { VueloConfig } from "../interfaces/vueloConfig";
6
+ import fs from "fs"; // Asegúrate de importar el módulo fs para trabajar con el sistema de archivos
7
+ import path from "path"; // Asegúrate de importar el módulo path para manejar rutas
8
+ import { compileVueComponent } from "../utils/compile";
9
+
10
+ export default function BunServer(
11
+ vite: ViteDevServer,
12
+ config: VueloConfig,
13
+ components: any,
14
+ ) {
15
+ return Bun.serve({
16
+ async fetch(req) {
17
+ const url = new URL(req.url);
18
+
19
+ // Manejar requests de /@fs/ para imports de archivos del sistema
20
+ if (url.pathname.startsWith("/@fs/")) {
21
+ try {
22
+ const filePath = url.pathname.replace("/@fs/", "");
23
+
24
+ // Si es un archivo Vue, compilarlo usando nuestra función
25
+ if (filePath.endsWith(".vue")) {
26
+ const compiledCode = await compileVueComponent(vite, filePath);
27
+ return new Response(compiledCode, {
28
+ headers: { "Content-Type": "application/javascript" },
29
+ });
30
+ }
31
+
32
+ // Para otros archivos, leer y servir directamente
33
+ const fileContent = await fs.promises.readFile(filePath, "utf-8");
34
+ return new Response(fileContent, {
35
+ headers: { "Content-Type": "application/javascript" },
36
+ });
37
+ } catch (error) {
38
+ console.error("Error serving /@fs/ file:", error);
39
+ return new Response("Not Found", { status: 404 });
40
+ }
41
+ }
42
+
43
+ // Servir hydrateClientComponents.js de forma estática
44
+ if (url.pathname === "/vuelo/hydrateClientComponents.js") {
45
+ const filePath = path.join(__dirname, "../hydrateClientComponents.js"); // Ajusta la ruta al archivo
46
+ try {
47
+ // Leer el archivo de forma asíncrona
48
+ const fileContent = await fs.promises.readFile(filePath, "utf-8");
49
+ return new Response(fileContent, {
50
+ headers: { "Content-Type": "application/javascript" }, // Especifica el tipo MIME para JavaScript
51
+ });
52
+ } catch (error) {
53
+ console.error("Error reading hydrateClientComponents.js:", error);
54
+ return new Response("Internal Server Error", { status: 500 });
55
+ }
56
+ }
57
+ // manejar rutas de islas - servir componentes compilados como módulos ES
58
+ if (url.pathname.startsWith("/api/islands/")) {
59
+ for (const island of components.islands) {
60
+ const name = "/api/islands/" + island.name.toLowerCase();
61
+ // Resolver la ruta correctamente
62
+ let absolutePath = island.path;
63
+
64
+ // Si la ruta empieza con /src/, es una ruta absoluta incorrecta
65
+ // Necesitamos resolverla desde el directorio del proyecto
66
+ if (absolutePath.startsWith("/src/")) {
67
+ absolutePath = path.resolve(process.cwd(), absolutePath.slice(1));
68
+ } else if (!path.isAbsolute(absolutePath)) {
69
+ // Si es relativa, resolverla desde el directorio del proyecto
70
+ absolutePath = path.resolve(process.cwd(), absolutePath);
71
+ }
72
+
73
+ if (url.pathname === name) {
74
+ try {
75
+ // Verificar que el archivo existe
76
+ if (!fs.existsSync(absolutePath)) {
77
+ console.error(`File not found: ${absolutePath} (original: ${island.path})`);
78
+ return new Response("Component file not found", { status: 404 });
79
+ }
80
+
81
+ // Compilar el componente usando Vite
82
+ const compiledCode = await compileVueComponent(vite, absolutePath);
83
+
84
+ return new Response(compiledCode, {
85
+ headers: {
86
+ "Content-Type": "application/javascript",
87
+ },
88
+ });
89
+ } catch (error) {
90
+ console.error("Error compiling component:", error);
91
+ return new Response("Internal Server Error", { status: 500 });
92
+ }
93
+ }
94
+ }
95
+ // Si ninguna ruta de isla coincide, puedes retornar un 404 o un mensaje
96
+ return new Response("Not Found", { status: 404 });
97
+ } else {
98
+ // manejar páginas del frontend
99
+ try {
100
+ let template = getTemplate();
101
+ template = await vite.transformIndexHtml(url.origin, template)
102
+ const rcomponent = resolveRouteComponent(components.pages, url);
103
+ const appHtml = await createVueloApp(vite, rcomponent);
104
+ let script = `
105
+ <script type="module" src="/vuelo/hydrateClientComponents.js"></script>
106
+ `;
107
+ if (config.mode === "SSR") {
108
+ script = `
109
+ <script type="module" src="/vueloFramework/client.js"></script>
110
+ `;
111
+ }
112
+
113
+ const html = template
114
+ .replace(`<!--vuelo-app-html-->`, appHtml)
115
+ .replace(`</body>`, `${script}</body>`);
116
+
117
+ return new Response(html, {
118
+ headers: { "Content-Type": "text/html" },
119
+ });
120
+ } catch (error) {
121
+ console.error("Error during rendering:", error);
122
+ return new Response("Internal Server Error", { status: 500 });
123
+ }
124
+ }
125
+
126
+ // No debería llegar aquí, pero en caso de que sí, puedes retornar un 404 o un mensaje
127
+ return new Response("Not Found", { status: 404 });
128
+ },
129
+ port: config.port,
130
+ });
131
+ }
@@ -0,0 +1,177 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ // Función para compilar un componente Vue usando Vite para el cliente
6
+ export async function compileVueComponent(
7
+ vite: ViteDevServer,
8
+ filePath: string,
9
+ ): Promise<string> {
10
+ // Leer el contenido del archivo
11
+ const fileContent = await fs.promises.readFile(filePath, "utf-8");
12
+
13
+ // Usar el pluginContainer de Vite para transformar el componente SFC
14
+ // Esto transforma <script setup> y otros a código JavaScript válido
15
+ // Necesitamos usar la URL del módulo en formato Vite
16
+ const moduleId = `/@fs/${filePath}`;
17
+
18
+ // Primero resolver el ID del módulo
19
+ const resolved = await vite.pluginContainer.resolveId(moduleId);
20
+ const id = resolved?.id || moduleId;
21
+
22
+ // Transformar el código usando el pluginContainer
23
+ const result = await vite.pluginContainer.transform(fileContent, id, { ssr: false });
24
+
25
+ if (!result || !result.code) {
26
+ throw new Error(`Failed to transform component at ${filePath}`);
27
+ }
28
+
29
+ let code = result.code;
30
+
31
+ // Reemplazar imports de Vite HMR (no necesarios en el cliente)
32
+ code = code.replace(
33
+ /import\s+.*from\s+['"]\/@vite\/client['"];?/g,
34
+ '// HMR removed for client'
35
+ );
36
+ code = code.replace(
37
+ /import\.meta\.hot\s*=.*;?/g,
38
+ '// HMR removed for client'
39
+ );
40
+
41
+ // Reemplazar TODOS los imports de vue desde node_modules/.vite/deps/ con el CDN
42
+ // Esto incluye imports con query strings como ?v=c407c0a3
43
+ code = code.replace(
44
+ /from\s+['"]\/node_modules\/\.vite\/deps\/vue\.js[^'"]*['"]/g,
45
+ 'from "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"'
46
+ );
47
+
48
+ // Reemplazar imports de 'vue' para usar el CDN
49
+ code = code.replace(
50
+ /from\s+['"]vue['"]/g,
51
+ 'from "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"'
52
+ );
53
+
54
+ // Verificar si el código usa funciones de Vue pero no tiene imports válidos
55
+ // Si usa ref, reactive, etc. sin import, necesitamos agregarlo
56
+ const usesRef = /\bref\s*\(/.test(code);
57
+ const hasValidVueImport = /import\s+.*\{[^}]*ref[^}]*\}\s+from\s+['"]https:\/\/unpkg\.com\/vue@3\/dist\/vue\.esm-browser\.prod\.js['"]/.test(code) ||
58
+ /import\s+\*\s+as\s+Vue\s+from\s+['"]https:\/\/unpkg\.com\/vue@3\/dist\/vue\.esm-browser\.prod\.js['"]/.test(code);
59
+
60
+ if (usesRef && !hasValidVueImport) {
61
+ // Agregar import de ref y otras funciones comunes de Vue al inicio
62
+ // Primero, encontrar qué funciones de Vue se están usando
63
+ const vueImports = [];
64
+ if (/\bref\s*\(/.test(code)) vueImports.push('ref');
65
+ if (/\breactive\s*\(/.test(code)) vueImports.push('reactive');
66
+ if (/\bcomputed\s*\(/.test(code)) vueImports.push('computed');
67
+ if (/\bwatch\s*\(/.test(code)) vueImports.push('watch');
68
+
69
+ // Agregar el import al inicio del código
70
+ if (vueImports.length > 0) {
71
+ code = `import { ${vueImports.join(', ')} } from "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js";\n${code}`;
72
+ }
73
+ }
74
+
75
+ // Reemplazar imports relativos con rutas absolutas usando /@fs/
76
+ // Esto permite que Vite resuelva los imports cuando se sirven
77
+ code = code.replace(
78
+ /from\s+['"]\.\/([^'"]+)['"]/g,
79
+ (match, importPath) => {
80
+ const resolvedPath = path.resolve(path.dirname(filePath), importPath);
81
+ return `from "/@fs/${resolvedPath}"`;
82
+ }
83
+ );
84
+
85
+ // También manejar imports de '../'
86
+ code = code.replace(
87
+ /from\s+['"]\.\.\/([^'"]+)['"]/g,
88
+ (match, importPath) => {
89
+ const resolvedPath = path.resolve(path.dirname(filePath), "..", importPath);
90
+ return `from "/@fs/${resolvedPath}"`;
91
+ }
92
+ );
93
+
94
+ // Asegurarse de que el componente se exporte como default
95
+ // El código compilado crea _sfc_main, necesitamos exportarlo
96
+ if (code.includes('const _sfc_main') || code.includes('var _sfc_main')) {
97
+ // Buscar si ya hay un export default
98
+ if (!code.match(/export\s+default\s+_sfc_main/)) {
99
+ // Agregar el export al final del código
100
+ code += '\nexport default _sfc_main;';
101
+ }
102
+ }
103
+
104
+ // Asegurarse de que el código exporte el componente correctamente
105
+ // El código compilado por Vite para SFC con <script setup> ya debería tener export default
106
+ // Pero verificamos que esté presente
107
+ if (!code.includes('export default') && !code.includes('export {')) {
108
+ console.warn(`[WARN] Compiled code for ${filePath} doesn't seem to have exports`);
109
+ }
110
+
111
+ // Eliminar código de HMR que no funciona en el cliente
112
+ code = code.replace(/if\s*\(import\.meta\.hot\)\s*\{[\s\S]*?\}/g, '// HMR removed');
113
+ code = code.replace(/__VUE_HMR_RUNTIME__[^}]*\}/g, '// HMR removed');
114
+ code = code.replace(/typeof\s+[^=]*=\s*mod[\s\S]*?\}/g, '// HMR removed');
115
+ code = code.replace(/_sfc_main\.__hmrId\s*=\s*[^;]+;?/g, '// HMR removed');
116
+
117
+ // Eliminar import de _export_sfc que no funciona en el cliente
118
+ code = code.replace(/import\s+_export_sfc\s+from\s+['"][^'"]*export-helper['"];?/g, '');
119
+
120
+ // Buscar la función render (probablemente _sfc_render)
121
+ const renderMatch = code.match(/function\s+(_sfc_render[^\s(]*)\s*\(/);
122
+ if (renderMatch) {
123
+ const renderFnName = renderMatch[1];
124
+
125
+ // Primero, eliminar cualquier asignación incorrecta que esté dentro del setup
126
+ code = code.replace(/_sfc_main\.render\s*=\s*_sfc_render\s*;?\s*\n/g, '');
127
+
128
+ // Buscar donde termina la definición de _sfc_main (el cierre del objeto, no del setup)
129
+ // El patrón es: const _sfc_main = { setup(...) { ... } } seguido de un salto de línea o import
130
+ // Necesitamos encontrar el cierre del objeto _sfc_main, no el cierre de setup
131
+ const sfcMainEndMatch = code.match(/(const _sfc_main = \{[\s\S]*?setup\([^)]*\)\s*\{[\s\S]*?\}\s*\n\})\s*(?=\n|import|function|const|let|var|export)/);
132
+
133
+ if (sfcMainEndMatch) {
134
+ // Insertar la asignación del render justo después de la definición completa de _sfc_main
135
+ code = code.replace(sfcMainEndMatch[0], sfcMainEndMatch[1] + `\n_sfc_main.render = ${renderFnName};\n`);
136
+ } else {
137
+ // Fallback: buscar el patrón más simple - después del cierre del objeto _sfc_main
138
+ // Buscar: const _sfc_main = { ... } seguido de algo que no sea parte del objeto
139
+ const simpleMatch = code.match(/(const _sfc_main = \{[\s\S]*?\n\})\s*(?=\n|import|function|const|let|var|export)/);
140
+ if (simpleMatch) {
141
+ code = code.replace(simpleMatch[0], simpleMatch[1] + `\n_sfc_main.render = ${renderFnName};\n`);
142
+ }
143
+ }
144
+ }
145
+
146
+ // Reemplazar _export_sfc con asignación directa del render si existe
147
+ const exportSfcMatch = code.match(/export\s+default\s+[^_]*_export_sfc\s*\(\s*_sfc_main\s*,\s*\[\s*\[\s*['"]render['"]\s*,\s*([^,\]]+)\s*\]/);
148
+ if (exportSfcMatch) {
149
+ const renderFnName = exportSfcMatch[1].trim();
150
+ // Asignar el render directamente a _sfc_main si no está ya asignado
151
+ if (!code.includes('_sfc_main.render =')) {
152
+ code = code.replace(/(const _sfc_main = \{[\s\S]*?\n\})\s*/, (match) => {
153
+ return match + `\n_sfc_main.render = ${renderFnName};\n`;
154
+ });
155
+ }
156
+ // Eliminar el export con _export_sfc
157
+ code = code.replace(/export\s+default\s+[^_]*_export_sfc\s*\([^)]+\)/, '');
158
+ }
159
+
160
+ // Eliminar todos los export default existentes
161
+ code = code.replace(/export\s+default\s+[^;]+;?\s*/g, '');
162
+
163
+ // Agregar un solo export default de _sfc_main al final
164
+ if (code.includes('_sfc_main')) {
165
+ code += '\nexport default _sfc_main;';
166
+ }
167
+
168
+ // Log para depuración (mostrar más caracteres para ver el template)
169
+ console.log(`[DEBUG compile] Compiled code preview (first 1000 chars):`, code.substring(0, 1000));
170
+ console.log(`[DEBUG compile] Code length: ${code.length}`);
171
+ console.log(`[DEBUG compile] Has export default: ${code.includes('export default')}`);
172
+ console.log(`[DEBUG compile] Has _sfc_main: ${code.includes('_sfc_main')}`);
173
+ console.log(`[DEBUG compile] Has _sfc_main.render: ${code.includes('_sfc_main.render')}`);
174
+ console.log(`[DEBUG compile] Last 500 chars:`, code.substring(code.length - 500));
175
+
176
+ return code;
177
+ }
@@ -0,0 +1,28 @@
1
+ import { readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { join } from "path";
4
+
5
+ function getTemplate() {
6
+ let template;
7
+
8
+ try {
9
+ // Intenta leer el archivo index.html en la ruta especificada
10
+ template = readFileSync(resolve("index.html"), "utf-8");
11
+ } catch (error) {
12
+ // Si no se encuentra, intenta leerlo desde la carpeta defaults
13
+ try {
14
+ const file = join(__dirname, "../defaults/index.html");
15
+ template = readFileSync(file, "utf-8");
16
+ } catch (error) {
17
+ // Maneja el error si tampoco se encuentra el archivo en defaults
18
+ console.error(
19
+ "No se pudo encontrar el template en ninguna de las rutas especificadas.",
20
+ );
21
+ template = ""; // O alguna otra acción que desees realizar
22
+ }
23
+ }
24
+
25
+ return template;
26
+ }
27
+
28
+ export default getTemplate;