1import { Function } from "@/Neon/Function.ts";
2import { Project } from "@/Neon/Project.ts";
3import { providers } from "@/Neon/Providers.ts";
4import { packageWebsiteArtifact } from "@/Neon/Website/Artifact.ts";
5import * as Test from "@/Test/Alchemy.ts";
6import { getProject } from "@distilled.cloud/neon";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Path from "effect/Path";
11import * as HttpClient from "effect/http/HttpClient";
12import {
13 bodyContaining,
14 buildPortableExample,
15 exampleRoot,
16} from "./Fixture.ts";
17
18const { test } = Test.make({ providers: providers() });
19
20for (const slug of ["nextjs", "vocs"] as const)
21 test.provider.skipIf(!process.env.NEON_WEBSITE_FRESH)(
22 `fresh ${slug} portable artifact serves production requests`,
23 (stack) =>
24 Effect.gen(function* () {
25 yield* stack.destroy();
26 const fs = yield* FileSystem.FileSystem;
27 const path = yield* Path.Path;
28 const root = yield* exampleRoot(slug);
29 const next = slug === "nextjs";
30 const pixel = path.join(root, "public/neon-artifact-pixel.png");
31 if (next) {
32 const original = (yield* fs.exists(pixel))
33 ? yield* fs.readFile(pixel)
34 : undefined;
35 yield* Effect.addFinalizer(() =>
36 (original
37 ? fs.writeFile(pixel, original)
38 : fs.remove(pixel, { force: true })
39 ).pipe(Effect.orDie),
40 );
41 yield* fs.writeFile(
42 pixel,
43 yield* Effect.sync(() =>
44 Buffer.from(
45 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=",
46 "base64",
47 ),
48 ),
49 );
50 }
51 const artifact = yield* packageWebsiteArtifact(
52 yield* buildPortableExample(slug, root),
53 );
54 const directory = yield* fs.makeTempDirectoryScoped();
55 const zip = path.join(directory, "site.zip");
56 yield* fs.writeFile(zip, artifact.archive);
57 const site = yield* stack.deploy(
58 Effect.gen(function* () {
59 const project = yield* Project("Project", {
60 region: "aws-us-east-2",
61 });
62 return yield* Function("Site", {
63 project,
64 artifact: { zip },
65 env: { NODE_ENV: "production" },
66 });
67 }),
68 );
69 const url = site.url.replace(/\/$/, "");
70 yield* Effect.logInfo(`Fresh ${slug} artifact: ${url}`);
71 const html = yield* bodyContaining(`${url}/`, "Neon");
72 for (const method of ["GET", "HEAD"] as const) {
73 const response = yield* method === "HEAD"
74 ? HttpClient.head(`${url}/example.json`)
75 : HttpClient.get(`${url}/example.json`);
76 expect(response.status).toBe(200);
77 const body = yield* response.text;
78 if (method === "HEAD") expect(body).toBe("");
79 else expect(body).toContain("framework");
80 }
81 const assets = [
82 ...html.matchAll(/(?:src|href)="([^" ]+\.(?:js|css)(?:\?[^" ]*)?)"/g),
83 ]
84 .map((match) => match[1]!)
85 .filter((url) => url.startsWith("/"));
86 expect(assets.length).toBeGreaterThan(0);
87 for (const asset of assets.slice(0, 3)) {
88 const response = yield* HttpClient.get(`${url}${asset}`);
89 expect(response.status).toBe(200);
90 expect((yield* response.arrayBuffer).byteLength).toBeGreaterThan(0);
91 }
92 if (next) {
93 const image = yield* HttpClient.get(
94 `${url}/_next/image?url=%2Fneon-artifact-pixel.png&w=64&q=75`,
95 { headers: { accept: "image/webp" } },
96 );
97 expect(image.status).toBe(200);
98 expect(image.headers["content-type"]).toContain("image/webp");
99 expect((yield* image.arrayBuffer).byteLength).toBeGreaterThan(0);
100 const stream = yield* HttpClient.get(`${url}/api/stream`);
101 expect(stream.status).toBe(200);
102 expect(stream.headers["content-type"]).toContain("text/event-stream");
103 expect(yield* stream.text).toBe("data: first\n\ndata: second\n\n");
104 } else yield* bodyContaining(`${url}/guide`, "Guide");
105 yield* stack.destroy();
106 expect(
107 yield* getProject({ project_id: site.projectId }).pipe(
108 Effect.as(false),
109 Effect.catchTag("NotFound", () => Effect.succeed(true)),
110 ),
111 ).toBe(true);
112 }).pipe(Effect.scoped),
113 {
114 tags: [
115 "provider:neon",
116 "provider:neon:function",
117 "provider:neon:project",
118 "provider:neon:website",
119 "live",
120 ],
121 timeout: 120_000,
122 },
123 );