Angular SSR with Cloudflare Pages and Bun
How to setup Angular SSR with Cloudflare Pages and Bun
Introduction
Building an Angular app for WinterCG runtimes (Cloudflare / Deno Deploy) it is not an obvious task.
Angular 19 introduced the new SSR build flag experimentalPlatform
that makes the it much easier (experimental zoneless is also a big step forward).
Create an Angular Application
ng new my-app \
--minimal \
--strict \
--routing \
--style=css \
--ssr \
--server-routing=false \
--experimental-zoneless
Create a worker
Install packages
npm i --save-dev wrangler itty-router
wrangler
is the Cloudflare Workers CLI, it allows us to run our worker locallyitty-router
is a lightweight router for Workers
Create the worker
src/worker.ts import { renderApplication } from "@angular/platform-server";
import { default as bootstrap } from "./main.server";
// Api imports
import type { IRequest } from "itty-router";
import { AutoRouter, error } from "itty-router";
// Declare the environment type
declare interface Env extends Record<string, any> {
ASSETS: { fetch: (url: URL) => Promise<Response> };
// ... declare more env variables here
}
async function render(url: URL, env: Env) {
const indexUrl = new URL("/index.csr.html", url);
const indexHtml = await env.ASSETS.fetch(indexUrl);
const document = await indexHtml.text();
const content = await renderApplication(bootstrap, {
document,
url: url.pathname + url.search,
});
return new Response(content, {
status: 200,
headers: {
"content-type": "text/html",
"cache-control": "public, max-age=0, s-maxage=0",
},
});
}
const router = AutoRouter<IRequest, [Env]>()
.get("/api/hello", () => ({ message: "Hello, World!" }))
.all("/api/*", () => error(404))
.get("*", (request, env) => {
const url = new URL(request.url);
// If the URL has a file extension, we assume it's a static file
if (url.pathname.includes(".")) {
return env.ASSETS.fetch(url);
} else {
return render(url, env);
}
})
.all("*", () => error(404));
export const fetch = (request: Request, env: Env) => {
return router.fetch(request, env).catch((err: Error) => {
console.error("[SSR Error]", err);
return new Response("Internal Server Error", { status: 500 });
});
};
// Export for Cloudflare Pages
export default { fetch };
Edit angular.json
configuration
...
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
- "outputPath": "dist/my-app",
+ "outputPath": "dist",
"server": "src/main.server.ts",
- "prerender": true, /* For some reason, prerender doesn't works with MacOs */
+ "prerender": false,
"ssr": {
"entry": "src/server.ts"
},
},
"configurations": {
+ "worker": {
+ "tsConfig": "tsconfig.worker.json",
+ "ssr": {
+ "entry": "src/worker.ts",
+ "experimentalPlatform": "neutral"
+ }
+ },
"production": {
...
Add tsconfig.worker.ts
Copy tsconfig.app.json
to tsconfig.worker.json
and edit it:
...
"files": [
"src/main.ts",
"src/main.server.ts",
- "src/server.ts"
+ "src/worker.ts"
],
...
Build the worker
ng build --configuration worker
Bundle the worker
esbuild --bundle dist/server/server.mjs \ --outfile=dist/browser/_worker.js \ --platform=browser \ --target=es2022 \ --format=esm
This will create a bundled _worker.js
file in the dist/browser
directory, this is what Cloudflare Pages expects to find.
Run the worker locally
npx wrangler pages dev dist/browser --port 4200 --compatibility-date=2024-12-05
Create a Bun adapter
We can create a Bun adapter to run the worker locally with Bun.
Install Bun types
npm i --save-dev @types/bun
Create the Bun adapter
bun.ts /// <reference types="@types/bun" />
// @ts-ignore - This is a workaround for the missing types
import { fetch as handleRequest } from "./dist/server/server.mjs";
console.log("[Bun] Starting server on port 4200");
const env = {
...process.env,
ASSETS: {
async fetch({ pathname }: URL) {
const browserDir = import.meta.dir + "/dist/browser";
const file = Bun.file(browserDir + pathname);
if (file) {
try {
const headers = { "content-type": file.type };
return new Response(await file.text(), { headers });
} catch (err) {
return new Response("Internal Server Error", { status: 500 });
}
} else {
return new Response("Not Found", { status: 404 });
}
},
},
};
Bun.serve({
port: 4200,
fetch(req) {
console.log("[Bun] Request", req.url);
return handleRequest(req, env);
},
});
Run the worker with Bun
bun run bun.ts
We don't even need to bundle the worker as with wrangler dev
because Bun will handle the worker's bundling and serving!