1import { describe, expect, it } from "vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as ChildProcess from "effect/process/ChildProcess";
7import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
8import { loadPrerenderPairs, seedStoreFromPrerender } from "../cache/seed.ts";
9import {
10 LAMBDA_ADAPTER_FILE_NAME,
11 SERVE_ENTRY_NAME,
12 makeAwsTarget,
13 makeLambdaEntrySource,
14 target,
15} from "../aws.ts";
16
17describe("makeAwsTarget", () => {
18 it(
19 "renders from an isolated Lambda directory without project dependencies",
20 () =>
21 Effect.runPromise(
22 Effect.scoped(
23 Effect.gen(function* () {
24 const fs = yield* FileSystem.FileSystem;
25 const path = yield* Path.Path;
26 const root = yield* path.fromFileUrl(
27 new URL(
28 "../../../../../examples/aws-website-vinext/",
29 import.meta.url,
30 ),
31 );
32 const configPath = path.join(root, "vite.config.ts");
33 const configBefore = yield* fs.readFileString(configPath);
34 expect(configBefore).not.toContain(
35 "@alchemy.run/frontend-frameworks",
36 );
37 const built = yield* makeAwsTarget({ streaming: false }).build!({
38 root,
39 framework: "vinext",
40 });
41 expect(yield* fs.readFileString(configPath)).toBe(configBefore);
42 expect(
43 yield* fs.readFileString(
44 path.join(built.distDirectory!, "server/index.js"),
45 ),
46 ).toContain("CACHE_BUCKET_NAME");
47 const directory = yield* fs.makeTempDirectoryScoped({
48 prefix: "vinext-lambda-",
49 });
50 yield* fs.copy(
51 path.join(built.distDirectory!, "server"),
52 directory,
53 );
54 const prerender = yield* Effect.promise(() =>
55 loadPrerenderPairs(directory),
56 );
57 expect(prerender.routeCount).toBeGreaterThan(0);
58 expect(
59 prerender.pairs.some((pair) => pair.key.includes("/isr")),
60 ).toBe(true);
61 const values = new Map<string, string>();
62 const store = {
63 getText: (key: string) =>
64 Effect.runPromise(Effect.sync(() => values.get(key))),
65 putText: (key: string, value: string) =>
66 Effect.runPromise(
67 Effect.sync(() => {
68 values.set(key, value);
69 }),
70 ),
71 delete: (key: string) =>
72 Effect.runPromise(
73 Effect.sync(() => {
74 values.delete(key);
75 }),
76 ),
77 };
78 yield* Effect.promise(() =>
79 seedStoreFromPrerender(store, directory),
80 );
81 const key = prerender.pairs[0]!.key;
82 values.set(key, "refreshed at runtime");
83 yield* Effect.promise(() =>
84 seedStoreFromPrerender(store, directory),
85 );
86 expect(values.get(key)).toBe("refreshed at runtime");
87 const spawner = yield* ChildProcessSpawner;
88 const node = yield* Effect.sync(() => process.execPath);
89 const child = yield* spawner.spawn(
90 ChildProcess.make(
91 node,
92 [
93 "--input-type=module",
94 "--eval",
95 `
96 import assert from "node:assert/strict";
97 import { handler } from "./serve-aws-lambda.mjs";
98 for (const route of ["/", "/isr", "/api/hello"]) {
99 const response = await handler({ rawPath: route, rawQueryString: "name=Alchemy", requestContext: { domainName: "localhost", http: { method: "GET" } } });
100 assert.equal(response.statusCode, 200);
101 assert.ok(response.body.length > 0);
102 if (route === "/") assert.match(response.body, /vinext on AWS/);
103 if (route === "/api/hello") assert.equal(JSON.parse(response.body).greeting, "isolated Lambda");
104 }
105 `,
106 ],
107 {
108 cwd: directory,
109 env: { GREETING: "isolated Lambda" },
110 stdout: "inherit",
111 stderr: "inherit",
112 },
113 ),
114 );
115 expect(yield* child.exitCode).toBe(0);
116 }),
117 ).pipe(Effect.provide(NodeServices.layer)),
118 ),
119 120_000,
120 );
121 it("declares the aws platform and a wholesale vinext build (not OpenNext)", () => {
122 const aws = makeAwsTarget();
123 expect(aws.platform).toBe("aws");
124 expect(aws.build).toBeTypeOf("function");
125 expect(aws.bundle?.conditions).toContain("node");
126 expect(aws.bundle?.external).toContain("@aws-sdk/");
127 expect(aws.bundle?.external ?? []).not.toContain("cloudflare:");
128 });
129
130 it("wraps the App Router fetch handler as a streaming Lambda handler", () => {
131 const source = makeLambdaEntrySource(true);
132 expect(SERVE_ENTRY_NAME).toBe("server/serve-aws-lambda.mjs");
133 expect(LAMBDA_ADAPTER_FILE_NAME).toBe("vinext-aws-lambda.mjs");
134 expect(source).toContain('import rsc from "./index.js"');
135 expect(source).toContain("toLambdaHandler");
136 expect(source).toContain("export const handler");
137 expect(source).toContain('hostRuntime: "node"');
138 expect(source).not.toContain("toBufferedLambdaHandler");
139 expect(source).not.toContain("opennext");
140 expect(source).not.toContain("vinext/server/fetch-handler");
141 expect(source).not.toContain("startProdServer");
142 });
143
144 it("can emit the buffered Lambda wrapper", () => {
145 const source = makeLambdaEntrySource(false);
146 expect(source).toContain("toBufferedLambdaHandler");
147 expect(source).not.toContain("toLambdaHandler");
148 });
149
150 it("exposes the named `target` module export as the factory", () => {
151 expect(target).toBe(makeAwsTarget);
152 });
153});