| 1 | import * as Nextjs from "@alchemy.run/frontend-frameworks/nextjs/neon"; |
| 2 | import * as Vocs from "@alchemy.run/frontend-frameworks/vocs/neon"; |
| 3 | import { expect } from "alchemy-test"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Path from "effect/Path"; |
| 6 | import * as Schedule from "effect/Schedule"; |
| 7 | import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as HttpClient from "effect/http/HttpClient"; |
| 9 | import { functionTextSamples } from "../FunctionRollout.ts"; |
| 10 | |
| 11 | const 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. */ |
| 33 | export 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 | |
| 78 | export 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 | |
| 99 | export 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 | |
| 112 | export 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 | }); |