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 +141 -0
- package/package.json +47 -0
- package/vueloFramework/app.ts +17 -0
- package/vueloFramework/autoImport.ts +155 -0
- package/vueloFramework/client.js +5 -0
- package/vueloFramework/createVueloApp.ts +14 -0
- package/vueloFramework/defaults/App.vue +39 -0
- package/vueloFramework/defaults/index.html +16 -0
- package/vueloFramework/hydrateClientComponents.js +267 -0
- package/vueloFramework/interfaces/vueloConfig.ts +5 -0
- package/vueloFramework/router.ts +7 -0
- package/vueloFramework/server.ts +23 -0
- package/vueloFramework/servers/bun.ts +131 -0
- package/vueloFramework/utils/compile.ts +177 -0
- package/vueloFramework/utils/template.ts +28 -0
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,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,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;
|