1import * as Command from "@/Command/index.ts";
2import { providers } from "@/Neon/Providers.ts";
3import { stageWebsiteArtifact } from "@/Neon/Website/Artifact.ts";
4import * as Test from "@/Test/Alchemy.ts";
5import { Server } from "@/Website/Server.ts";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Path from "effect/Path";
9import * as Stream from "effect/Stream";
10import * as ChildProcess from "effect/process/ChildProcess";
11import { exampleRoot } from "./Fixture.ts";
12import { frameworks } from "./Frameworks.ts";
13
14const { test } = Test.make({ providers: providers() });
15
16describe.sequential(
17 "Neon Website production Fetch artifacts",
18 { tags: ["provider:neon", "provider:neon:website", "live"] },
19 () => {
20 for (const { slug, name } of frameworks) {
21 // Production builds spawn multi-GiB build children (nuxt/vocs ~3 GiB)
22 // that cannot share a 4 GiB budget with a long-lived suite process.
23 test.provider.skipIf(!!process.env.FAST)(
24 slug,
25 (stack) =>
26 Effect.gen(function* () {
27 yield* stack.destroy();
28 const path = yield* Path.Path;
29 const root = yield* exampleRoot(slug);
30 const framework = slug === "foldkit" ? "vite" : slug;
31 const target = `@alchemy.run/frontend-frameworks/${framework}/neon`;
32 const build = yield* stack.deploy(
33 Effect.gen(function* () {
34 return yield* slug === "static"
35 ? Command.Build("Build", {
36 cwd: root,
37 command: "bun run build",
38 outdir: "dist",
39 memo: false,
40 }).pipe(
41 Effect.map((build) => ({
42 distDir: build.outdir,
43 serverEntry: undefined,
44 })),
45 )
46 : Server("Build", {
47 root,
48 framework:
49 slug === "nextjs" || slug === "vocs"
50 ? target
51 : `@alchemy.run/frontend-frameworks/${framework}`,
52 target,
53 memo: false,
54 env: { GREETING: `Hello from ${name} on Neon!` },
55 options:
56 slug === "astro" ? { astro: { output: "server" } } : {},
57 });
58 }),
59 );
60 expect(build.distDir).toBeDefined();
61 const artifact = yield* stageWebsiteArtifact({
62 root,
63 distDir: build.distDir!,
64 serverEntry: build.serverEntry,
65 static: slug === "static" ? {} : undefined,
66 layout: slug === "nextjs" ? "next" : "output",
67 });
68 const proc = yield* ChildProcess.make("node", [
69 "--input-type=module",
70 "-e",
71 [
72 `const {default: handler} = await import(${JSON.stringify(path.join(artifact.directory, "index.mjs"))});`,
73 'for (const [method, pathname] of [["GET", "/"], ["GET", "/example.json"], ["HEAD", "/example.json"]]) {',
74 'const response = await handler.fetch(new Request("http://localhost" + pathname, { method }));',
75 "const body = await response.text();",
76 'if (response.status !== 200 || (method === "HEAD" && body !== "") || (pathname === "/" && !body.includes("Neon"))) { throw new Error(method + " " + pathname + " returned " + response.status + ": " + body.slice(0, 300)); }',
77 "}",
78 ...(slug === "sveltekit"
79 ? [
80 'for (const method of ["GET", "HEAD"]) {',
81 'const response = await handler.fetch(new Request("http://localhost/about", { method }));',
82 'const body = await response.text(); if (response.status !== 200 || (method === "HEAD" ? body !== "" : !body.includes("prerendered"))) throw new Error(method + " /about returned " + response.status + ": " + body);',
83 "}",
84 ]
85 : []),
86 'console.log("NEON_FETCH_ARTIFACT_OK"); process.exit(0);',
87 ].join("\n"),
88 ]);
89 const [code, stdout, stderr] = yield* Effect.all(
90 [
91 proc.exitCode,
92 proc.stdout.pipe(Stream.decodeText, Stream.mkString),
93 proc.stderr.pipe(Stream.decodeText, Stream.mkString),
94 ],
95 { concurrency: "unbounded" },
96 ).pipe(Effect.timeout("30 seconds"));
97 expect({ code, stderr: code === 0 ? "" : stderr }).toEqual({
98 code: 0,
99 stderr: "",
100 });
101 expect(stdout).toContain("NEON_FETCH_ARTIFACT_OK");
102 yield* stack.destroy();
103 }).pipe(Effect.scoped),
104 { timeout: 120_000 },
105 );
106 }
107 },
108);