1import * as Nextjs from "@alchemy.run/frontend-frameworks/nextjs/neon";
2import * as Vocs from "@alchemy.run/frontend-frameworks/vocs/neon";
3import { expect } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import * as Path from "effect/Path";
6import * as Schedule from "effect/Schedule";
7import * as FileSystem from "effect/FileSystem";
8import * as HttpClient from "effect/http/HttpClient";
9import { functionTextSamples } from "../FunctionRollout.ts";
10
11const buildDirectories = new Set([
12 "node_modules",
13 "dist",
14 "build",
15 "serve-neon.mjs",
16 "neon-node-adapter.mjs",
17 ".alchemy",
18 ".next",
19 ".nuxt",
20 ".output",
21 ".svelte-kit",
22 ".astro",
23 ".vite",
24 ".vinxi",
25 ".nitro",
26 ".cache",
27 ".turbo",
28 ".vercel",
29 "tsconfig.tsbuildinfo",
30]);
31
32/** Isolate mutable sources and build output while reusing installed dependencies. */
33export const exampleRoot = Effect.fn(function* (slug: string) {
34 const fs = yield* FileSystem.FileSystem;
35 const path = yield* Path.Path;
36 const source = yield* path.fromFileUrl(
37 new URL(`../../../../../examples/neon-website-${slug}/`, import.meta.url),
38 );
39 // Keep relative tsconfig paths and avoid ignored roots that confuse Tailwind's scanner.
40 const directory = path.dirname(source);
41 yield* fs.makeDirectory(directory, { recursive: true });
42 const root = yield* fs.makeTempDirectoryScoped({
43 directory,
44 prefix: `neon-website-${slug}-`,
45 });
46 for (const name of yield* fs.readDirectory(source)) {
47 if (
48 !buildDirectories.has(name) &&
49 !name.startsWith(".env") &&
50 name !== ".npmrc"
51 ) {
52 yield* fs.copy(path.join(source, name), path.join(root, name));
53 }
54 }
55 const installed = path.join(source, "node_modules");
56 const dependencies = path.join(root, "node_modules");
57 yield* fs.makeDirectory(dependencies);
58 for (const name of yield* fs.readDirectory(installed)) {
59 if (name.startsWith(".") && name !== ".bin") continue;
60 if (name.startsWith("@")) {
61 yield* fs.makeDirectory(path.join(dependencies, name));
62 for (const pkg of yield* fs.readDirectory(path.join(installed, name))) {
63 yield* fs.symlink(
64 yield* fs.realPath(path.join(installed, name, pkg)),
65 path.join(dependencies, name, pkg),
66 );
67 }
68 } else {
69 yield* fs.symlink(
70 yield* fs.realPath(path.join(installed, name)),
71 path.join(dependencies, name),
72 );
73 }
74 }
75 return root;
76});
77
78export const buildPortableExample = Effect.fn(function* (
79 slug: "nextjs" | "vocs",
80 existingRoot?: string,
81) {
82 const path = yield* Path.Path;
83 const root = existingRoot ?? (yield* exampleRoot(slug));
84 const target = slug === "nextjs" ? Nextjs.target() : Vocs.target();
85 const output = yield* target.build({ root, framework: slug });
86 expect(output.distDirectory).toBeDefined();
87 expect(output.serverModules?.[0]).toBeDefined();
88 return {
89 root,
90 distDir: output.distDirectory!,
91 serverEntry: path.join(
92 output.distDirectory!,
93 output.serverModules![0]!.name,
94 ),
95 layout: slug === "nextjs" ? ("next" as const) : ("output" as const),
96 };
97});
98
99export const bodyContaining = Effect.fn(function* (url: string, text: string) {
100 const body = yield* HttpClient.get(url).pipe(
101 Effect.flatMap((response) => response.text),
102 Effect.repeat({
103 schedule: Schedule.spaced("1 second"),
104 times: 8,
105 until: (body) => body.includes(text),
106 }),
107 );
108 expect(body).toContain(text);
109 return body;
110});
111
112export const updatedBodyContaining = Effect.fn(function* (
113 url: string,
114 text: string,
115) {
116 const samples = yield* functionTextSamples(url, (body) =>
117 body.includes(text),
118 );
119 for (const body of samples) expect(body).toContain(text);
120});