Contenidos
Creando una herramienta para hacer preview a sitios estaticos
Cuando estás desarrollando una aplicación web en React, Vue, Django… bueno, básicamente cualquier tipo de app web, ya sea backend o frontend, siempre necesitas configurar un servidor de desarrollo, una forma de servir todos los archivos dentro de tu proyecto para poder previsualizar lo que estás trabajando.
Las librerías modernas ya traen esta funcionalidad incorporada, incluso algunos IDEs como VSCode tienen extensiones que te ayudan con esto.
Pero, ¿qué pasa si usas algo como Vim, o simplemente estás desarrollando un sitio estático con algunos archivos HTML y JS? Entonces, todo lo demás parece demasiado.
Yo desarrollo muchos sitios estáticos, y a veces las cosas pueden volverse un poco locas, y tengo que estar subiendo nuevas versiones de esos sitios a sus respectivos servidores.
A veces no hay tiempo para abrir un IDE para previsualizarlos, y esto puede llevarme a cometer errores y subir la versión o los archivos equivocados a producción.
Puede ser un desastre.
Así que desarrollé una herramienta para poder previsualizar cualquier carpeta desde cualquier navegador de internet siempre que tenga la terminal abierta usando Deno.
La idea
El objetivo era simple: crear un servidor de archivos estáticos que:
- Sirva cualquier carpeta de archivos HTML.
- Mapee automáticamente los archivos a rutas según la estructura de carpetas.
- Abra la carpeta servida en el navegador por defecto.
- Requiera una configuración y dependencias mínimas.
El código
1. Punto de entrada
El punto de entrada de la aplicación analiza los argumentos de la línea de comandos e inicia el servidor.
import { parseArgs } from "jsr:@std/cli/parse-args";
import Parse from "./parse.ts";
import Directory from "./directory.ts";
import Serve from "./serve.ts";
const PATH = "path";
const PORT = "port";
async function main(): Promise<null | boolean> {
const { path, port } = parseArgs(Deno.args, { string: [PATH, PORT] });
console.clear();
const [parsed_path, parsed_port] = [Parse.path(path), Parse.port(port)];
const directory = new Directory();
await directory.read(parsed_path);
Serve.dir(parsed_path, directory.endpoints, parsed_port);
console.log(`Folder served at http://localhost:${parsed_port}`);
return true;
}
main();
Esto es lo que sucede:
- La función “parseArgs” extrae los argumentos “—path” y “—port”.
- Se proporcionan valores por defecto usando la utilidad “Parse”.
- La clase “Directory” escanea la carpeta y mapea las rutas.
- La clase “Serve” inicia el servidor HTTP.
2. Parseo de Argumentos
Esta utilidad asegura que los argumentos “path” y “port” sean válidos
export default class Parse {
static port(str?: string | undefined): number {
return str !== undefined ? Number(str) : 9999;
}
static path(srt?: string | undefined): string {
if (srt === undefined) {
return Deno.cwd();
}
return srt;
}
}
- Si no se proporciona “—path”, se usa el directorio actual por defecto.
- Si no se proporciona “—port”, se usa “9999” por defecto.
3. Recorrido de Directorios
Esta clase escanea la carpeta de forma recursiva y mapea los archivos “.html” a rutas.
export default class Directory {
endpoints: { [key: string]: Array<string> } = {};
regex = new RegExp(/.html/);
async read(path: string) {
await this.get_endpoints(path, "/");
}
async get_endpoints(path_name: string, folder_name: string): Promise<void> {
const endpoint_arr: Array<string> = [];
for await (const file of Deno.readDir(path_name)) {
if (file.isDirectory) {
await this.get_endpoints(
`${path_name}/${file.name}`,
`${folder_name}${file.name}/`,
);
} else if (file.name.match(this.regex)) {
let endpoint = file.name.split(this.regex)[0];
if (endpoint === "404") {
endpoint = "(.*)";
}
endpoint_arr.push(endpoint);
}
}
endpoint_arr.sort((b, a) => a.localeCompare(b));
if (endpoint_arr.length > 0) this.endpoints[folder_name] = endpoint_arr;
}
}
- El método “read” inicia el escaneo de la carpeta.
- El método “get_endpoints”:
- Recorre directorios de forma recursiva.
- Mapea archivos “.html” a rutas.
- Manejo especial para “404.html” (ruta catch-all).
La lógica detrás de mi convención de nombres es que todo tenga sentido desde fuera de la clase, por ejemplo:
Serve.html();
Directory.read();
Serve.dir();
En mi opinión, esto es simplemente limpio.
4. Sirviendo Archivos
Esta clase inicializa el servidor HTTP y registra las rutas.
import { Application, Context } from "@oak/oak";
import { Route, RouteFactory } from "./route.ts";
export default class Serve {
static regex = new RegExp(/\/+/g);
static dir(
path: string,
endpoints: { [key: string]: Array<string> },
port: number,
) {
const app = new Application();
const r = new Route();
const entries = Object.entries(endpoints);
this.loop(entries, (item) => {
const [inner_path, routes] = item;
this.loop(routes, (route) => {
const route_data = new RouteFactory(route, path, inner_path, port);
r.append_to(route_data.route_name, route_data.full_name);
});
});
app.use(r.get().routes());
app.use(async (context, next) => {
try {
await context.send({ root: path });
} catch {
next();
}
});
app.listen({ port });
}
static loop<T>(arr: T[], func: (t: T) => void) {
let i = 0;
while (i < arr.length) {
func(arr[i]);
i++;
}
}
static async html(ctx: Context, full_name: string) {
try {
const decoder = new TextDecoder("utf-8");
const file = await Deno.readFile(full_name);
const text = decoder.decode(file);
ctx.response.body = text;
ctx.response.type = "text/html";
ctx.response.status = 200;
} catch (err) {
console.log("error loading file");
console.log(err);
}
}
}
- El método “dir”:
- Registra rutas dinámicamente según la estructura del directorio.
- Sirve archivos estáticos directamente si no hay coincidencia de ruta.
- El método “html” lee y sirve archivos HTML.
5. Abriendo el Navegador
Esta clase abre la carpeta servida en el navegador predeterminado.
export default class CommandFactory {
public command: Deno.Command;
constructor(os_name: string, port: number) {
const url = `http://localhost:${port}/`;
const args_arr = [url];
let command_name = "";
switch (os_name) {
case "windows":
command_name = "cmd";
args_arr.push("/c", "start");
break;
case "darwin":
command_name = "open";
break;
default:
command_name = "xdg-open";
break;
}
this.command = new Deno.Command(command_name, { args: args_arr });
}
execute() {
this.command.spawn();
}
}
- Detecta el sistema operativo.
- Construye el comando apropiado para abrir el navegador.
¿Por qué esta herramienta?
Esta herramienta es perfecta para:
- Previsualizar rápidamente sitios estáticos sin configurar un entorno de desarrollo completo.
- Flujos de trabajo rápidos.
- Evitar errores al subir archivos a producción.
¡Pruébala y simplifica tu flujo de trabajo de desarrollo de sitios estáticos!
Échale un vistazo en GithubQuizás te interese:
Construyendo funciones auxiliares reutilizables de PostgreSQL en Deno con deno-postgres
Boilerplate code is life.
Interface vs Type – Una guía comprensiva
La vida esta llena de decisiones, escoge con sabiduría