1import { providers } from "@/Neon/Providers.ts";
2import { WebsiteArtifact } from "@/Neon/Website/Artifact.ts";
3import { deployWebsite } from "@/Neon/Website/FrameworkSite.ts";
4import { Astro } from "@/Neon/Website/Astro.ts";
5import { Nextjs } from "@/Neon/Website/Nextjs.ts";
6import * as Test from "@/Test/Alchemy.ts";
7import { getProject, getProjectBranchFunction } from "@distilled.cloud/neon";
8import { describe, expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Path from "effect/Path";
12import * as HttpClient from "effect/http/HttpClient";
13import { functionRolloutTimeout } from "../FunctionRollout.ts";
14import { exampleRoot, updatedBodyContaining } from "./Fixture.ts";
15
16const { test } = Test.make({ providers: providers() });
17
18describe.sequential(
19 "Neon Website feasibility",
20 { tags: ["provider:neon", "provider:neon:website", "live"] },
21 () => {
22 // Live deploy + rollout polling takes minutes; skip under --fast.
23 test.provider.skipIf(!!process.env.FAST)(
24 "static Fetch files deploy, update, no-op, HEAD, and destroy",
25 (stack) =>
26 Effect.gen(function* () {
27 yield* stack.destroy();
28 const fs = yield* FileSystem.FileSystem;
29 const path = yield* Path.Path;
30 const root = yield* fs.makeTempDirectoryScoped({
31 prefix: "neon-static-feasibility-",
32 });
33 const distDir = path.join(root, "dist");
34 yield* fs.makeDirectory(distDir);
35 yield* fs.writeFileString(
36 path.join(distDir, "index.html"),
37 "<h1>Neon static feasibility</h1>",
38 );
39 yield* fs.writeFileString(
40 path.join(distDir, "asset.json"),
41 '{"ok":true}',
42 );
43 const deploy = stack.deploy(
44 Effect.gen(function* () {
45 const artifact = yield* WebsiteArtifact("Artifact", {
46 root,
47 distDir,
48 static: { notFoundHandling: "spa" },
49 });
50 return yield* deployWebsite({}, artifact);
51 }),
52 );
53 const site = yield* deploy;
54 expect(site.function).toBeDefined();
55 const fn = site.function!;
56 expect(
57 yield* HttpClient.get(`${site.url}/deep/link`).pipe(
58 Effect.flatMap((res) => res.text),
59 ),
60 ).toContain("Neon static feasibility");
61 const head = yield* HttpClient.head(`${site.url}/asset.json`);
62 expect(head.status).toBe(200);
63 expect(yield* head.text).toBe("");
64 const observed = yield* getProjectBranchFunction({
65 project_id: fn.projectId,
66 branch_id: fn.branchId,
67 slug: fn.slug,
68 });
69 expect(observed).toBeDefined();
70 const unchanged = yield* deploy;
71 expect(unchanged.function!.activeDeploymentId).toBe(
72 fn.activeDeploymentId,
73 );
74 yield* fs.writeFileString(
75 path.join(distDir, "index.html"),
76 "<h1>Neon static updated</h1>",
77 );
78 const updated = yield* deploy;
79 expect(updated.function!.functionId).toBe(fn.functionId);
80 expect(updated.function!.activeDeploymentId).not.toBe(
81 fn.activeDeploymentId,
82 );
83 expect(updated.url).toBe(site.url);
84 yield* updatedBodyContaining(
85 String(updated.url),
86 "Neon static updated",
87 );
88 yield* stack.destroy();
89 expect(
90 yield* getProject({ project_id: fn.projectId }).pipe(
91 Effect.as(false),
92 Effect.catchTag("NotFound", () => Effect.succeed(true)),
93 ),
94 ).toBe(true);
95 }).pipe(Effect.scoped),
96 { timeout: functionRolloutTimeout },
97 );
98
99 for (const [slug, website] of [
100 ["astro", Astro],
101 ["nextjs", Nextjs],
102 ] as const) {
103 test.provider.skipIf(!!process.env.FAST)(
104 `${slug} real framework deploy feasibility`,
105 (stack) =>
106 Effect.gen(function* () {
107 yield* stack.destroy();
108 const rootDir = yield* exampleRoot(slug);
109 const site = yield* stack.deploy(
110 website("Web", {
111 rootDir,
112 memo: slug === "nextjs" ? false : undefined,
113 env: {
114 GREETING: `Hello from ${slug === "astro" ? "Astro" : "Next.js"} on Neon!`,
115 },
116 }),
117 );
118 const response = yield* HttpClient.get(String(site.url));
119 expect(response.status).toBe(200);
120 expect(yield* response.text).toContain("Neon");
121 expect(
122 (yield* HttpClient.get(`${site.url}/example.json`)).status,
123 ).toBe(200);
124 expect(
125 (yield* HttpClient.get(`${site.url}/api/hello?name=Alchemy`))
126 .status,
127 ).toBe(200);
128 const fn = site.function!;
129 yield* getProjectBranchFunction({
130 project_id: fn.projectId,
131 branch_id: fn.branchId,
132 slug: fn.slug,
133 });
134 yield* stack.destroy();
135 expect(
136 yield* getProject({ project_id: fn.projectId }).pipe(
137 Effect.as(false),
138 Effect.catchTag("NotFound", () => Effect.succeed(true)),
139 ),
140 ).toBe(true);
141 }),
142 { timeout: 120_000 },
143 );
144 }
145 },
146);