1import * as Alchemy from "@/index.ts";
2import { providers } from "@/Neon/Providers.ts";
3import { Vite } from "@/Neon/Website/Vite.ts";
4import * as Test from "@/Test/Alchemy.ts";
5import { getProject, getProjectBranchFunction } from "@distilled.cloud/neon";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import * as Redacted from "effect/Redacted";
11import * as HttpClient from "effect/http/HttpClient";
12import { bodyContaining, exampleRoot } from "./Fixture.ts";
13
14const { test } = Test.make({ providers: providers(), dev: true });
15
16test.provider(
17 "Vite hot reloads files and restarts for environment changes",
18 (stack) =>
19 Effect.gen(function* () {
20 yield* stack.destroy();
21 const fs = yield* FileSystem.FileSystem;
22 const path = yield* Path.Path;
23 const rootDir = yield* exampleRoot("vite");
24 const asset = path.join(rootDir, "public", "example.json");
25 const original = yield* fs.readFileString(asset);
26 yield* Effect.addFinalizer(() =>
27 fs.writeFileString(asset, original).pipe(Effect.orDie),
28 );
29 const deploy = (value: string) =>
30 stack.deploy(Vite("Web", { rootDir, env: { VITE_REVISION: value } }));
31 const site = yield* deploy("first");
32 yield* bodyContaining(`${site.url}/src/App.tsx`, "Vite");
33 yield* fs.writeFileString(asset, '{"framework":"hot-reloaded"}');
34 yield* bodyContaining(`${site.url}/example.json`, "hot-reloaded");
35 const restarted = yield* deploy("second");
36 expect(restarted.function).toBeUndefined();
37 yield* bodyContaining(`${restarted.url}/src/App.tsx`, "second");
38 yield* stack.destroy();
39 }).pipe(Effect.scoped),
40 {
41 tags: ["provider:neon", "provider:neon:website", "local"],
42 timeout: 120_000,
43 exclusive: true,
44 },
45);
46
47test.provider(
48 "remote opts into a real Function and destroys its stamped live resources",
49 (stack) =>
50 Effect.gen(function* () {
51 yield* stack.destroy();
52 const rootDir = yield* exampleRoot("vite");
53 const site = yield* stack.deploy(
54 Vite("Web", {
55 rootDir,
56 env: {
57 VITE_REVISION: "public-build-marker",
58 SERVER_ONLY_MARKER: Redacted.make("private-runtime-marker"),
59 },
60 }).pipe(Alchemy.remote()),
61 );
62 expect(site.url).toMatch(/^https:\/\//);
63 const fn = site.function!;
64 expect(fn.functionId.startsWith("dev:")).toBe(false);
65 const observed = yield* getProjectBranchFunction({
66 project_id: fn.projectId,
67 branch_id: fn.branchId,
68 slug: fn.slug,
69 });
70 expect(observed.function.id).toBe(fn.functionId);
71 const html = yield* bodyContaining(
72 `${site.url}/deep/link`,
73 "Vite on Neon",
74 );
75 const scripts = [...html.matchAll(/<script\b[^>]*\bsrc="([^"]+)"/g)].map(
76 (match) => match[1]!,
77 );
78 expect(scripts.length).toBeGreaterThan(0);
79 let javascript = "";
80 for (const script of scripts) {
81 javascript += yield* HttpClient.get(
82 new URL(script, String(site.url)).href,
83 ).pipe(Effect.flatMap((response) => response.text));
84 }
85 expect(javascript).toContain("public-build-marker");
86 expect(html + javascript).not.toContain("private-runtime-marker");
87 yield* stack.destroy();
88 expect(
89 yield* getProject({ project_id: fn.projectId }).pipe(
90 Effect.as(false),
91 Effect.catchTag("NotFound", () => Effect.succeed(true)),
92 ),
93 ).toBe(true);
94 }),
95 {
96 tags: ["provider:neon", "provider:neon:website", "live"],
97 timeout: 120_000,
98 exclusive: true,
99 },
100);