1/**
2 * True `alchemy dev` end-to-end for the Nuxt site: spawns the REAL CLI
3 * (mirroring examples/aws-dev/test/dev.test.ts), which runs Nuxt's own
4 * dev server as the local `Website.Server` provider — no Lambda, no
5 * CloudFront, no S3; the only cloud touch is the state store.
6 *
7 * Coverage:
8 * - stack output → `url` is a local dev-server address (port is
9 * whatever the framework bound — never hard-coded)
10 * - SSR env parity → `/` renders the GREETING declared in alchemy.run.ts
11 * - routing → `/about` (prerendered route) serves
12 * - API route → `/api/hello` serves the nitro handler
13 * - static assets → `/robots.txt` from public/
14 * - HOT RELOAD → editing app/pages/index.vue is served by Nuxt's
15 * HMR without a redeploy
16 */
17import { afterAll, expect, test } from "bun:test";
18import { DevCli, fetchOk } from "alchemy-test/DevCli";
19import * as fs from "node:fs";
20import * as path from "node:path";
21
22const root = path.resolve(import.meta.dirname, "..");
23// Isolated stage so this suite never fights integ.test.ts (same stack
24// name) over state rows.
25const STAGE = "dev-cli-test";
26const cli = new DevCli({ root, stage: STAGE });
27
28// Hot-reload surface: the SSR index page. The test rewrites it in place
29// with the CLI running, then restores it.
30const pagePath = path.join(root, "app", "pages", "index.vue");
31const pageSource = fs.readFileSync(pagePath, "utf8");
32const MARKER = "Nuxt on AWS";
33const MARKER_V2 = "Nuxt on AWS [dev-v2]";
34
35afterAll(async () => {
36 // Always leave the repo tree clean, even on a mid-reload failure.
37 fs.writeFileSync(pagePath, pageSource);
38
39 await cli.stop();
40 if (!process.env.NO_DESTROY) cli.destroy();
41}, 180_000);
42
43test(
44 "alchemy dev serves the Nuxt site locally with hot reload",
45 async () => {
46 cli.start();
47
48 const url = await cli.outputUrlWhenReady("url", {
49 tries: 180,
50 delayMs: 1000,
51 });
52
53 // Dev identity: the framework dev server, not CloudFront. The port is
54 // whatever the framework bound — only the URL captured from the CLI's
55 // stdout is authoritative.
56 expect(new URL(url).hostname).toBe("localhost");
57 expect(url).not.toContain("cloudfront.net");
58
59 // SSR env parity: GREETING from alchemy.run.ts reaches the dev server.
60 const home = await (await fetchOk(url)).text();
61 expect(home).toContain("Hello from Nuxt on AWS!");
62 expect(home).toContain(MARKER);
63
64 // Prerendered route serves through the dev server.
65 const about = await fetchOk(new URL("/about", url));
66 expect(about.status).toBe(200);
67
68 // Nitro API route serves through the dev server.
69 const hello = (await (
70 await fetchOk(new URL("/api/hello", url))
71 ).json()) as { greeting: string | null };
72 expect(hello).toEqual({ greeting: "Hello from Nuxt on AWS!" });
73
74 // Static asset from public/.
75 const robots = await (await fetchOk(new URL("/robots.txt", url))).text();
76 expect(robots).toContain("User-agent:");
77
78 // ── HOT RELOAD: rewrite the index page with the CLI still running —
79 // the framework dev server serves the new markup without a deploy ──
80 fs.writeFileSync(pagePath, pageSource.replace(MARKER, MARKER_V2));
81 await cli.pollUntil(
82 "hot-reloaded page (v2 marker)",
83 async () => {
84 try {
85 const res = await fetch(url);
86 if (!res.ok) return undefined;
87 const html = await res.text();
88 return html.includes(MARKER_V2) ? true : undefined;
89 } catch {
90 return undefined; // mid-reload
91 }
92 },
93 { tries: 120, delayMs: 500 },
94 );
95
96 // Restore — the swap back is itself a second hot reload and leaves
97 // the checked-in tree clean.
98 fs.writeFileSync(pagePath, pageSource);
99 await cli.pollUntil(
100 "restored page (v2 marker gone)",
101 async () => {
102 try {
103 const res = await fetch(url);
104 if (!res.ok) return undefined;
105 const html = await res.text();
106 return html.includes(MARKER_V2) ? undefined : true;
107 } catch {
108 return undefined; // mid-reload
109 }
110 },
111 { tries: 120, delayMs: 500 },
112 );
113 },
114 { timeout: 600_000 },
115);