1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import { describe, expect } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as pathe from "pathe";
8import { prepareNextjsFixture } from "../../Cloudflare/Website/TypeScriptCompat.ts";
9import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
10import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
11
12// `dev: true` runs local providers behind the RPC sidecar proxy by default,
13// matching the process topology of the real `alchemy dev` command.
14const { test } = Test.make({ providers: AWS.providers(), dev: true });
15
16const fixtureDir = pathe.resolve(import.meta.dirname, "fixtures", "nextjs-app");
17
18const fixtureEntries = [
19 ".gitignore",
20 "package.json",
21 "next.config.ts",
22 "tsconfig.json",
23 "app",
24 "public",
25];
26
27describe(
28 "AWS.Website.Nextjs local",
29 { tags: ["provider:aws", "provider:aws:website", "local"] },
30 () => {
31 test.provider(
32 "dev runs Next's own dev server with no cloud resources",
33 (stack) =>
34 Effect.gen(function* () {
35 const fs = yield* FileSystem.FileSystem;
36 const path = yield* Path.Path;
37
38 yield* stack.destroy();
39
40 // Clone OUTSIDE the repo (OS temp dir): an in-workspace clone makes
41 // OpenNext detect the workspace as a monorepo and trace the server
42 // bundle against bun's isolated-install symlink store, which drops
43 // transitive deps (e.g. @swc/helpers) from the shipped node_modules.
44 const rootDir = yield* cloneFixture(fixtureDir, {
45 prefix: "alchemy-nextjs-aws-local-",
46 entries: fixtureEntries,
47 });
48 yield* prepareNextjsFixture(rootDir);
49
50 const deployed = yield* stack.deploy(
51 Effect.gen(function* () {
52 const site = yield* AWS.Website.Nextjs("NextjsSite", {
53 rootDir,
54 });
55 return { site };
56 }),
57 );
58
59 // The site is the framework's own dev server: a localhost URL and
60 // no cloud rows at all (proof no AWS call ran).
61 const url = deployed.site.url! as string;
62 expect(url).toMatch(
63 /^http:\/\/(localhost|127\.0\.0\.1|\[[0-9a-fA-F:]+\])/,
64 );
65 expect(deployed.site.distribution).toBeUndefined();
66 expect(deployed.site.server).toBeUndefined();
67 expect(deployed.site.bucket).toBeUndefined();
68 expect(deployed.site.revalidationQueue).toBeUndefined();
69 expect(deployed.site.tagCacheTable).toBeUndefined();
70
71 // SSR page served by the next dev server (native HMR toolchain).
72 yield* expectUrlContains(`${url}/`, "NEXTJS_AWS_PAGE_MARKER", {
73 timeout: "180 seconds",
74 label: "dev SSR home page",
75 });
76 // App Router API route through the dev server.
77 yield* expectUrlContains(
78 `${url}/api/hello?echo=dev`,
79 "NEXTJS_AWS_API_MARKER",
80 { label: "API route (dev)" },
81 );
82 yield* expectUrlContains(`${url}/api/hello?echo=dev`, "dev", {
83 label: "API route query echo (dev)",
84 });
85
86 // ── HMR: edit the App Router route in place. The stack is NOT
87 // re-applied — next dev must recompile the route and serve it
88 // through the same URL ───────────────────────────────────────────
89 const routePath = path.join(rootDir, "app/api/hello/route.ts");
90 const route = yield* fs.readFileString(routePath);
91 yield* fs.writeFileString(
92 routePath,
93 route.replace("NEXTJS_AWS_API_MARKER", "NEXTJS_AWS_API_MARKER_V2"),
94 );
95 yield* expectUrlContains(
96 `${url}/api/hello?echo=dev`,
97 "NEXTJS_AWS_API_MARKER_V2",
98 { timeout: "120 seconds", label: "API route after HMR edit" },
99 );
100 // The route still round-trips its query after the recompile.
101 yield* expectUrlContains(
102 `${url}/api/hello?echo=post-hmr`,
103 "post-hmr",
104 {
105 label: "API route query echo after HMR edit",
106 },
107 );
108
109 yield* stack.destroy();
110 }),
111 { timeout: 600_000 },
112 );
113 },
114);