import { describe, it, expect } from "vitest"; import { env } from "cloudflare:workers"; import { createApp } from "../app"; import type { CreateAppOptions } from "../app"; let testId = 0; /** * Build an app with createApp, load it into the Worker Loader, * call fetch(), and return the Response. */ async function buildAppAndFetch( options: CreateAppOptions, request: Request = new Request("http://app/") ): Promise { const result = await createApp(options); const id = "test-app-" + testId++; const worker = env.LOADER.get(id, () => ({ mainModule: result.mainModule, modules: result.modules, compatibilityDate: result.wranglerConfig?.compatibilityDate ?? "2026-01-01", compatibilityFlags: result.wranglerConfig?.compatibilityFlags })); return worker.getEntrypoint().fetch(request); } // ── Basic asset serving ───────────────────────────────────────────── describe("createApp e2e — static assets", () => { it("serves a static HTML asset at /", async () => { const res = await buildAppAndFetch({ files: { "src/index.ts": [ "export default {", " fetch() { return new Response('api'); }", "};" ].join("\n") }, assets: { "/index.html": "

Hello

" } }); expect(res.status).toBe(200); expect(await res.text()).toBe("

Hello

"); expect(res.headers.get("Content-Type")).toBe("text/html; charset=utf-8"); }); it("serves a JS asset with correct content type", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch() { return new Response('api'); }", "};" ].join("\n") }, assets: { "/index.html": "

Home

", "/app.js": "console.log('hello')" } }, new Request("http://app/app.js") ); expect(res.status).toBe(200); expect(await res.text()).toBe("console.log('hello')"); expect(res.headers.get("Content-Type")).toBe( "application/javascript; charset=utf-8" ); }); it("serves CSS with correct content type", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch() { return new Response('api'); }", "};" ].join("\n") }, assets: { "/styles.css": "body { color: red; }" } }, new Request("http://app/styles.css") ); expect(res.status).toBe(200); expect(await res.text()).toBe("body { color: red; }"); expect(res.headers.get("Content-Type")).toBe("text/css; charset=utf-8"); }); }); // ── Fall-through to server ────────────────────────────────────────── describe("createApp e2e — server fall-through", () => { it("falls through to user Worker for non-asset routes", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch() { return Response.json({ status: 'ok' }); }", "};" ].join("\n") }, assets: { "/index.html": "

Home

" } }, new Request("http://app/api/data") ); expect(res.status).toBe(200); const data = await res.json(); expect(data).toEqual({ status: "ok" }); }); it("falls through for POST requests even to asset paths", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch(req) {", ' return new Response("posted: " + req.method);', " }", "};" ].join("\n") }, assets: { "/index.html": "

Home

" } }, new Request("http://app/index.html", { method: "POST" }) ); expect(res.status).toBe(200); expect(await res.text()).toBe("posted: POST"); }); it("server can read request URL and headers", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch(req) {", " const url = new URL(req.url);", " return Response.json({", " path: url.pathname,", ' auth: req.headers.get("Authorization")', " });", " }", "};" ].join("\n") }, assets: {} }, new Request("http://app/api/users", { headers: { Authorization: "Bearer token123" } }) ); const data = (await res.json()) as { path: string; auth: string }; expect(data.path).toBe("/api/users"); expect(data.auth).toBe("Bearer token123"); }); }); // ── ETag / conditional requests ───────────────────────────────────── describe("createApp e2e — ETag and caching", () => { it("includes ETag in asset responses", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/app.js": "console.log('hello')" } }, new Request("http://app/app.js") ); expect(res.status).toBe(200); const etag = res.headers.get("ETag"); expect(etag).toBeTruthy(); expect(etag!.startsWith('"')).toBe(true); }); it("returns 304 for matching If-None-Match", async () => { const options: CreateAppOptions = { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/app.js": "console.log('hello')" } }; // First request to get ETag const result = await createApp(options); const id = "test-app-etag-" + testId++; const worker = env.LOADER.get(id, () => ({ mainModule: result.mainModule, modules: result.modules, compatibilityDate: "2026-01-01" })); const first = await worker .getEntrypoint() .fetch(new Request("http://app/app.js")); const etag = first.headers.get("ETag")!; // Second request with If-None-Match const second = await worker.getEntrypoint().fetch( new Request("http://app/app.js", { headers: { "If-None-Match": etag } }) ); expect(second.status).toBe(304); }); it("sets Cache-Control must-revalidate for HTML", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "

Home

" } }, new Request("http://app/") ); expect(res.headers.get("Cache-Control")).toBe( "public, max-age=0, must-revalidate" ); }); it("sets Cache-Control immutable for hashed assets", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/app.a1b2c3d4e5f6.js": "console.log('versioned')" } }, new Request("http://app/app.a1b2c3d4e5f6.js") ); expect(res.headers.get("Cache-Control")).toBe( "public, max-age=31536000, immutable" ); }); }); // ── HTML handling ─────────────────────────────────────────────────── describe("createApp e2e — HTML handling", () => { it("serves /about via /about.html (auto-trailing-slash)", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/about.html": "

About

" } }, new Request("http://app/about") ); expect(res.status).toBe(200); expect(await res.text()).toBe("

About

"); }); it("serves /blog/ via /blog/index.html", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/blog/index.html": "

Blog

" } }, new Request("http://app/blog/") ); expect(res.status).toBe(200); expect(await res.text()).toBe("

Blog

"); }); }); // ── SPA fallback ──────────────────────────────────────────────────── describe("createApp e2e — SPA fallback", () => { it("serves /index.html for unknown routes with SPA config", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "
" }, assetConfig: { not_found_handling: "single-page-application" } }, new Request("http://app/dashboard/settings", { headers: { Accept: "text/html,application/xhtml+xml" } }) ); expect(res.status).toBe(200); expect(await res.text()).toBe("
"); }); it("falls through to server for non-HTML requests (API calls)", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "
" }, assetConfig: { not_found_handling: "single-page-application" } }, new Request("http://app/api/counter") ); expect(res.status).toBe(200); expect(await res.text()).toBe("api"); }); it("still serves exact assets over SPA fallback", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "

Home

", "/app.js": "console.log('app')" }, assetConfig: { not_found_handling: "single-page-application" } }, new Request("http://app/app.js") ); expect(res.status).toBe(200); expect(await res.text()).toBe("console.log('app')"); }); }); // ── Client bundling ───────────────────────────────────────────────── describe("createApp e2e — client bundling", () => { it("bundles a client entry and serves it as an asset", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch() { return new Response('server'); }", "};" ].join("\n"), "src/client.ts": [ 'const msg: string = "hello from client";', "console.log(msg);" ].join("\n") }, client: "src/client.ts", assets: { "/index.html": '' } }, new Request("http://app/client.js") ); expect(res.status).toBe(200); const text = await res.text(); // The bundled output should contain the string from the source expect(text).toContain("hello from client"); expect(res.headers.get("Content-Type")).toBe( "application/javascript; charset=utf-8" ); }); it("serves the HTML page that references the client bundle", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": [ "export default {", " fetch() { return new Response('server'); }", "};" ].join("\n"), "src/client.ts": 'console.log("app");' }, client: "src/client.ts", assets: { "/index.html": '' } }, new Request("http://app/") ); expect(res.status).toBe(200); expect(await res.text()).toBe( '' ); }); }); // ── Multiple assets ───────────────────────────────────────────────── describe("createApp e2e — multiple assets", () => { it("serves multiple different asset types", async () => { const options: CreateAppOptions = { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "

Home

", "/app.js": "console.log('app')", "/styles.css": "body { margin: 0; }", "/data.json": '{"key":"value"}', "/robots.txt": "User-agent: *" } }; const result = await createApp(options); const id = "test-app-multi-" + testId++; const worker = env.LOADER.get(id, () => ({ mainModule: result.mainModule, modules: result.modules, compatibilityDate: "2026-01-01" })); const ep = worker.getEntrypoint(); const html = await ep.fetch(new Request("http://app/")); expect(html.status).toBe(200); expect(html.headers.get("Content-Type")).toBe("text/html; charset=utf-8"); const js = await ep.fetch(new Request("http://app/app.js")); expect(js.status).toBe(200); expect(await js.text()).toBe("console.log('app')"); const css = await ep.fetch(new Request("http://app/styles.css")); expect(css.status).toBe(200); expect(await css.text()).toBe("body { margin: 0; }"); const json = await ep.fetch(new Request("http://app/data.json")); expect(json.status).toBe(200); expect(await json.text()).toBe('{"key":"value"}'); const txt = await ep.fetch(new Request("http://app/robots.txt")); expect(txt.status).toBe(200); expect(await txt.text()).toBe("User-agent: *"); // API route falls through const api = await ep.fetch(new Request("http://app/api/data")); expect(api.status).toBe(200); expect(await api.text()).toBe("api"); }); }); // ── Error cases ───────────────────────────────────────────────────── describe("createApp error cases", () => { it("throws when server entry is not found", async () => { await expect( createApp({ files: { "src/other.ts": "export const x = 1;" }, server: "src/index.ts" }) ).rejects.toThrow('Server entry point "src/index.ts" not found'); }); it("throws when client entry is not found", async () => { await expect( createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, client: "src/client.ts" }) ).rejects.toThrow('Client entry point "src/client.ts" not found'); }); }); // ── Output structure ──────────────────────────────────────────────── describe("createApp output structure", () => { it("returns __app-wrapper.js as mainModule", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); expect(result.mainModule).toBe("__app-wrapper.js"); }); it("includes asset manifest in modules", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

", "/app.js": "console.log('hi')" } }); expect(result.modules["__asset-manifest.json"]).toBeDefined(); }); it("includes asset modules with __assets/ prefix", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

", "/app.js": "console.log('hi')" } }); expect(result.modules["__assets/index.html"]).toBeDefined(); expect(result.modules["__assets/app.js"]).toBeDefined(); }); it("populates assetMap", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); expect(result.assetManifest.size).toBe(1); expect(result.assetManifest.get("/index.html")).toBeDefined(); }); it("reports clientBundles when client entry is provided", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };", "src/client.ts": "console.log('hi')" }, client: "src/client.ts", assets: {} }); expect(result.clientBundles).toBeDefined(); expect(result.clientBundles).toContain("/client.js"); }); }); // ── 404-page handling ──────────────────────────────────────────────── describe("createApp e2e — 404-page not-found handling", () => { it("serves 404.html with status 404", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "

Home

", "/404.html": "

Not Found

" }, assetConfig: { not_found_handling: "404-page" } }, new Request("http://app/nonexistent", { headers: { Accept: "text/html" } }) ); expect(res.status).toBe(404); expect(await res.text()).toBe("

Not Found

"); }); it("serves nested 404.html walking up directories", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "

Home

", "/blog/404.html": "

Blog Not Found

" }, assetConfig: { not_found_handling: "404-page" } }, new Request("http://app/blog/missing-post", { headers: { Accept: "text/html" } }) ); expect(res.status).toBe(404); expect(await res.text()).toBe("

Blog Not Found

"); }); it("falls through to server when no 404.html exists", async () => { const res = await buildAppAndFetch( { files: { "src/index.ts": "export default { fetch() { return new Response('api'); } };" }, assets: { "/index.html": "

Home

" }, assetConfig: { not_found_handling: "404-page" } }, new Request("http://app/nonexistent") ); expect(res.status).toBe(200); expect(await res.text()).toBe("api"); }); }); // ── Wrapper code structure ────────────────────────────────────────── describe("createApp — wrapper code structure", () => { it("module wrapper imports handleAssetRequest from runtime module", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); const wrapper = result.modules["__app-wrapper.js"] as string; expect(wrapper).toContain( 'import { handleAssetRequest, createMemoryStorage } from "./__asset-runtime.js"' ); expect(wrapper).toContain( "await handleAssetRequest(request, manifest, storage, ASSET_CONFIG)" ); }); it("DO wrapper imports handleAssetRequest from runtime module", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" }, durableObject: true }); const wrapper = result.modules["__app-wrapper.js"] as string; expect(wrapper).toContain( 'import { handleAssetRequest, createMemoryStorage } from "./__asset-runtime.js"' ); expect(wrapper).toContain( "await handleAssetRequest(request, manifest, storage, ASSET_CONFIG)" ); }); it("includes __asset-runtime.js in output modules", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); const runtime = result.modules["__asset-runtime.js"]; expect(typeof runtime).toBe("string"); expect(runtime as string).toContain("handleAssetRequest"); expect(runtime as string).toContain("createMemoryStorage"); }); it("wrapper initializes manifest Map and storage at module level", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); const wrapper = result.modules["__app-wrapper.js"] as string; expect(wrapper).toContain("new Map(Object.entries(manifestJson))"); expect(wrapper).toContain("createMemoryStorage(ASSET_CONTENT)"); }); it("module and DO wrappers share the same init block", async () => { const moduleResult = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); const doResult = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" }, durableObject: true }); const moduleWrapper = moduleResult.modules["__app-wrapper.js"] as string; const doWrapper = doResult.modules["__app-wrapper.js"] as string; // Extract the init block (ASSET_CONFIG through storage creation) const extractInit = (code: string) => { const start = code.indexOf("const ASSET_CONFIG"); const end = code.indexOf("createMemoryStorage(ASSET_CONTENT)") + "createMemoryStorage(ASSET_CONTENT);".length; return code.slice(start, end).trim(); }; expect(extractInit(moduleWrapper)).toBe(extractInit(doWrapper)); }); }); // ── Durable Object wrapper ────────────────────────────────────────── describe("createApp — durableObject option", () => { it("sets durableObjectClassName to 'App' with durableObject: true", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" }, durableObject: true }); expect(result.durableObjectClassName).toBe("App"); }); it("uses custom className from durableObject option", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" }, durableObject: { className: "MyApp" } }); expect(result.durableObjectClassName).toBe("MyApp"); }); it("does not set durableObjectClassName without the option", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" } }); expect(result.durableObjectClassName).toBeUndefined(); }); it("wrapper imports DurableObject from cloudflare:workers", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" }, durableObject: true }); const wrapper = result.modules["__app-wrapper.js"]; expect(typeof wrapper).toBe("string"); expect(wrapper as string).toContain( 'import { DurableObject } from "cloudflare:workers"' ); }); it("wrapper exports named class with correct name", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: {}, durableObject: { className: "Counter" } }); const wrapper = result.modules["__app-wrapper.js"] as string; expect(wrapper).toContain("export class Counter extends BaseClass"); }); it("wrapper uses runtime handleAssetRequest and super.fetch fallback", async () => { const result = await createApp({ files: { "src/index.ts": "export default { fetch() { return new Response('ok'); } };" }, assets: { "/index.html": "

Hi

" }, durableObject: true }); const wrapper = result.modules["__app-wrapper.js"] as string; expect(wrapper).toContain( "handleAssetRequest(request, manifest, storage, ASSET_CONFIG)" ); expect(wrapper).toContain("super.fetch(request)"); }); });