| 1 | import * as Command from "@/Command/index.ts"; |
| 2 | import { providers } from "@/Neon/Providers.ts"; |
| 3 | import { stageWebsiteArtifact } from "@/Neon/Website/Artifact.ts"; |
| 4 | import * as Test from "@/Test/Alchemy.ts"; |
| 5 | import { Server } from "@/Website/Server.ts"; |
| 6 | import { describe, expect } from "alchemy-test"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Path from "effect/Path"; |
| 9 | import * as Stream from "effect/Stream"; |
| 10 | import * as ChildProcess from "effect/process/ChildProcess"; |
| 11 | import { exampleRoot } from "./Fixture.ts"; |
| 12 | import { frameworks } from "./Frameworks.ts"; |
| 13 | |
| 14 | const { test } = Test.make({ providers: providers() }); |
| 15 | |
| 16 | describe.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 | ); |