viernes, mayo 16, 2025

Firebase Studio y Cloudflare

Es importante usar la IA para avanzar rápido y la nueva herramienta de google Firebase Studio es tentadora. Tiene un modo de prototipo que promete generar una aplicación en minutos.

Sería tonto no probarla, ¿no? ¿NO?

Me encuentro escribiendo cosas para el prototipo, ¿las pongo en inglés? ¿entenderá español? ¿qué puedo generar?

And app with user registration and new users are redirected to a subscription page to select between three plans 'basic', 'pro' and 'enterprise'. Each plan has discount if payed annually (10%). The app uses Stripe to handle payments in the backend. After the user has a valid subscription, the user can visit the homepage where he can create a todo list.

Si, con todo y el error And que puse al inicio... vamos a ver qué genera... 


Increíble, la verdad que es sorprendente lo que puedes hacer en minutos con ésta herramienta.  Obvio tiene muchas limitaciones que estoy esquivando, voy a listar algunas.

  • En modo prototipo, no soporta Angular
Ya con esa limitante, para mi, no vale la pena explorar más hasta que google soporte en su herramienta su propio framework.

Después Firebase Studio te ayuda a publicar la aplicación usando Firebase Hosting, es trivial.  ¿Pero qué pasa si quieres usar Cloudflare? ¿Cómo publicas en Cloudflare?  Bueno de eso quería escribir por que para mi no fue trivial.

En éste artículo sólo voy a compartir como publicar en Clouflare Pages, por que también lo puedes publicar usando Cloudflare Workers, pero yo sólo quiero el contenido estático nada de SSR.

La aplicación generada por Firebase Studio es una Nextjs, 😒 He estado esquivando React por años, incluso incursionando con Vue y Svelte que son maravillosos, pero React no me gusta, yo quiero que mis archivos sean html o js o ts o css o scss o json.

Tengo cero experiencia con Nextjs, y la IA no me sirve igual cuando no sé lo que está haciendo.  Pero eh aquí el resultado:

La configuración en next.config.ts:


import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  output: "export", // Use 'export' for static export
  distDir: "out", // Output directory for the static export
  typescript: {
    ignoreBuildErrors: true,
  },
  eslint: {
    ignoreDuringBuilds: true,
  },
  images: {
    unoptimized: true, // Disable Next.js image optimization
    remotePatterns: [
      {
        protocol: "https",
        hostname: "placehold.co",
        port: "",
        pathname: "/**",
      },
    ],
  },
};

export default nextConfig;

Puntos importantes:

Las imágenes guardadas en /public no son accesibles si usas Image tag, para que sirva tienes que deshabilitar la optimización que hace Nextjs con `images.unoptimezed: true`.  output y distDir son opciones que fui encontrando en la web y no estoy seguro de que distDir sea necesario, pero por ahora funciona y la dejé así.

Bien ahora cada vez que corras npm run build creará un directorio out con el sitio estático compilado y listo para publicar.

Ahora Cloudflare.  Crea un nuevo Page dentro de Cloudflare Compute, yo elegí Use direct upload, pero creo que con Import an existing Git repository lo puedes lograr, no lo probé.

Le pones un nombre, todo minúsculas sin espacios y luego te aparece una opción para subir contenido.  En éste punto puedes subir el contenido de tu carpeta out. Yo me regresé a la lista de Workers & Pages, y confirmé de que existía mi nuevo proyecto.


Y aunque ahí dice assets uploaded, no es cierto no subí nada, lo quiero hacer desde mi consola de trabajo usando wrangler.

Después de validar mi cuenta en la terminal puedes correr éste commando en la raíz del proyecto. 'super-app' es el nombre que le pusimos.

$ npx wrangler pages download config super-app
Need to install the following packages:
wrangler@4.15.2
Ok to proceed? (y)


 ⛅️ wrangler 4.15.2
-------------------

Bajará un archivo wrangler.toml básico que necesita un retoquito.

# Generated by Wrangler on Fri May 16 2025 10:21:40 GMT-0600 (Central Standard Time)
name = "super-app"
compatibility_date = "2025-05-16"
pages_build_output_dir = "./out"

[env]
production = { }

Y ahora está listo para publicar...

npx wrangler pages deploy --branch main

--branch main le dice a Cloudflare que debe de ir hasta arriba en el nombre de dominio, de lo contrario usa el nombre del branch que tengas activo en el repositorio git del proyecto.

Para facilitar el ciclo de desarrollo, le agrego a package.json un script:


{
  "name": "nextn",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack -p 9002",
    "genkit:dev": "genkit start -- tsx src/ai/dev.ts",
    "genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "deploy": "next build && npx wrangler pages deploy --branch main",
    "typecheck": "tsc --noEmit"
  },
...

Entonces puedes simplemente ejecutar npm run deploy y te lo compila y te lo publica.

viernes, mayo 02, 2025

Angular, proxy y CORS

La eterna batalla contra CORS: Un recordatorio para mi yo del futuro

De vez en cuando, sobre todo en nuevos proyectos, me tropiezo con la misma piedra. Y hoy, después de perder medio día entre búsquedas en Google, posts antiguos en Stack Overflow, y consultas a Perplexity, ChatGPT 4o-mini, Claude y Deepseek, he decidido documentar la solución para Juan Pablo del futuro.

El problema recurrente

Usualmente, en mis proyectos con Angular (v18), configuro mis variables de ambiente en environment.ts y environment.prod.ts. Típicamente como algo así:

export const environment = {
  production: false,
  protocol: 'http',
  baseUrl: 'http://localhost:5000/',
};

Y luego tengo un servicio que puede hacer una petición al backend así:

  // Use baseUrl from environment and append the specific API path segment
  private apiUrl = environment.baseUrl + 'api/v1'; // Ensure this points to your Go backend

  // Fetch subscription status from API and update the BehaviorSubject
  loadSubscriptionStatus(): Observable<SubscriptionStatus> {
    const headers = this.getAuthHeaders();
    return this.http.get<SubscriptionStatus>(`${this.apiUrl}/subscriptions/current`, { headers })
      .pipe(
        tap(status => this.subscriptionStatusSubject.next(status)), // Update state on success
        catchError(err => {
          this.subscriptionStatusSubject.next(null); // Clear state on error
          return this.handleError(err); // Propagate error
        })
      );
  }

El dolor de cabeza familiar

Y por supuesto, la primera vez el navegador me responde con un error:

Access to XMLHttpRequest at 'http://localhost:5000/api/v1/subscriptions/current' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

Me duele la cabeza cada vez que veo ese error... y recuerdo: "¡Ah, tengo que configurar un proxy!". Mientras desarrollo es buena idea, ya en producción no será necesario. Y entonces busco un proyecto anterior y copio el archivo proxy.conf.json y lo acomodo en el angular.json para que se lea cada vez que corro ng serve.

{
    "/api": {
        "target": "http://localhost:5000",
        "secure": false,
        "logLevel": "debug",
        "changeOrigin": false
    }
}

Pero el error persiste y pierdo la mitad del día entre búsquedas, todos dan consejos razonables, todos los pruebo cada vez con menos entusiasmo, y me digo: "esto ya lo viví cientos de veces".

La solución (otra vez)

Lo acabo de resolver, y ahora lo voy a documentar para Juan Pablo del futuro.

Toda solución parece evidente cuando la encuentras. Explorando vi algo que podría ser la falla...

Mi baseURL está apuntando al backend, y tengo un proxy que también lo hace. Uno de los dos está mal, de lo contrario no necesitaría el proxy.

Así que, modificando el baseUrl a http://localhost:4200 —el puerto original de Angular— se solucionó el problema.

Cabe mencionar que también configuré el servicio para soportar CORS:

	// Register CORS middleware to allow Angular frontend requests
	pb.OnBeforeServe().Add(func(e *core.ServeEvent) error {
		app.Log.Info("CORS middleware ---")
		e.Router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
			AllowOrigins:     []string{"http://localhost:4200", "http://localhost:8090"},
			AllowHeaders:     []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
			AllowMethods:     []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete, http.MethodOptions},
			AllowCredentials: true,
			// Ensure OPTIONS requests are handled correctly
			MaxAge: 86400, // Optional: cache preflight results for 24 hours
		}))

		return nil
	})

Esto ya lo había implementado antes eh, no creas que no estaba cuando el navegador se rehusaba a la conexión pero lo pongo ahora por si alguien pensó que me faltaba.

Lección aprendida (otra vez)

La moraleja es simple: cuando uses un proxy para desarrollo, asegúrate de que tu baseUrl apunte al servidor de desarrollo de Angular (localhost:4200), no al backend directamente. El proxy se encargará de redirigir las peticiones al backend apropiadamente.

export const environment = {
  production: false,
  protocol: 'http',
  baseUrl: 'http://localhost:4200/',  // <-- aquí fíjate!
};

Espero que este recordatorio me ahorre tiempo la próxima vez que me encuentre maldiciendo al error de CORS.