1import { stageWebsiteArtifact } from "@/Neon/Website/Artifact.ts";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as Stream from "effect/Stream";
8import * as ChildProcess from "effect/process/ChildProcess";
9import { buildPortableExample } from "./Fixture.ts";
10
11for (const slug of ["nextjs", "vocs"] as const) {
12 it.live.skipIf(!process.env.NEON_WEBSITE_LINUX)(
13 `${slug} production artifact runs on Linux ARM64 glibc without host dependencies`,
14 () =>
15 Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 const path = yield* Path.Path;
18 const next = slug === "nextjs";
19 const artifact = yield* stageWebsiteArtifact(
20 yield* buildPortableExample(slug),
21 );
22 const probe = [
23 'import assert from "node:assert/strict";',
24 'import fs from "node:fs";',
25 'import path from "node:path";',
26 'import { createRequire } from "node:module";',
27 "try {",
28 'assert.equal(process.platform, "linux"); assert.equal(process.arch, "arm64");',
29 "assert.ok(process.report.getReport().header.glibcVersionRuntime);",
30 ...(next
31 ? [
32 'const entry = [...fs.globSync("files/**/serve-neon.mjs")][0]; fs.writeFileSync(path.join(path.dirname(entry), "public/pixel.png"), Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64"));',
33 ]
34 : []),
35 'const { default: handler } = await import("./index.mjs");',
36 ...(next
37 ? [
38 'const require = createRequire(path.join(process.cwd(), "package.json")); const sharp = createRequire(require.resolve("next/package.json"))("sharp"); assert.ok(sharp.versions.vips);',
39 ]
40 : []),
41 'const request = (url, method = "GET", headers) => handler.fetch(new Request("http://localhost" + url, { method, headers }));',
42 'const home = await request("/"); assert.equal(home.status, 200); const html = await home.text(); assert.match(html, /Neon/);',
43 'for (const method of ["GET", "HEAD"]) { const res = await request("/example.json", method); assert.equal(res.status, 200); const text = await res.text(); if (method === "HEAD") assert.equal(text, ""); else assert.match(text, /framework/); }',
44 'const assets = [...html.matchAll(/(?:src|href)="([^" ]+\\.(?:js|css)(?:\\?[^" ]*)?)"/g)].map(m => m[1]).filter(url => url.startsWith("/")); assert.ok(assets.length > 0);',
45 "for (const url of assets.slice(0, 3)) { const res = await request(url); assert.equal(res.status, 200, url); assert.ok((await res.arrayBuffer()).byteLength > 0); }",
46 ...(next
47 ? [
48 'const image = await request("/_next/image?url=%2Fpixel.png&w=64&q=75", "GET", { accept: "image/webp" }); assert.equal(image.status, 200, await (image.status === 200 ? Promise.resolve("") : image.text())); assert.match(image.headers.get("content-type"), /^image\\/webp/); assert.ok((await image.arrayBuffer()).byteLength > 0);',
49 'const stream = await request("/api/stream"); assert.equal(stream.status, 200); assert.match(stream.headers.get("content-type"), /text\\/event-stream/); const reader = stream.body.getReader(); const first = await reader.read(); assert.match(new TextDecoder().decode(first.value), /data: first/); let rest = ""; for (;;) { const part = await reader.read(); if (part.done) break; rest += new TextDecoder().decode(part.value); } assert.match(rest, /data: second/);',
50 ]
51 : [
52 'const page = await request("/guide"); assert.equal(page.status, 200); assert.match(await page.text(), /Guide/);',
53 ]),
54 'console.log("NEON_LINUX_ARTIFACT_OK"); process.exit(0);',
55 "} catch (error) { console.error(error); process.exit(1); }",
56 ].join("\n");
57 yield* fs.writeFileString(
58 path.join(artifact.directory, "probe.mjs"),
59 probe,
60 );
61 const proc = yield* ChildProcess.make("docker", [
62 "run",
63 "--rm",
64 "--network=none",
65 "--memory=1g",
66 "--memory-swap=1g",
67 "--cpus=2",
68 "--platform=linux/arm64",
69 "--mount",
70 `type=bind,source=${artifact.directory},target=/app`,
71 "--workdir",
72 "/app",
73 "--env",
74 "NODE_ENV=production",
75 "node:24-bookworm-slim",
76 "node",
77 "probe.mjs",
78 ]);
79 const [code, stdout, stderr] = yield* Effect.all(
80 [
81 proc.exitCode,
82 proc.stdout.pipe(Stream.decodeText, Stream.mkString),
83 proc.stderr.pipe(Stream.decodeText, Stream.mkString),
84 ],
85 { concurrency: "unbounded" },
86 ).pipe(Effect.timeout("45 seconds"));
87 expect({ code, stderr: code === 0 ? "" : stderr }).toEqual({
88 code: 0,
89 stderr: "",
90 });
91 expect({
92 completed: stdout.includes("NEON_LINUX_ARTIFACT_OK"),
93 stdout,
94 stderr,
95 }).toMatchObject({ completed: true });
96 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
97 {
98 tags: ["provider:neon", "provider:neon:website", "local"],
99 timeout: 120_000,
100 },
101 );
102}