1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwayApi } from "@distilled.cloud/railway";
3import * as Railway from "@/Railway";
4import { suitePartition } from "../suiteProject.ts";
5import * as Test from "@/Test/Alchemy";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as pathe from "pathe";
11import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
12import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
13import { prepareNextjsFixture } from "../../Cloudflare/Website/TypeScriptCompat.ts";
14
15const { test } = Test.make({ providers: Railway.providers() });
16
17const logLevel = Effect.provideService(
18 MinimumLogLevel,
19 process.env.DEBUG ? "Debug" : "Info",
20);
21
22const fixtureDir = pathe.resolve(
23 import.meta.dirname,
24 "../../AWS/Website/fixtures/nextjs-app",
25);
26const fixtureEntries = [
27 ".gitignore",
28 "package.json",
29 "next.config.ts",
30 "tsconfig.json",
31 "app",
32 "public",
33];
34
35const readService = Query.fn((id: string) => ({
36 deletedAt: RailwayApi.service({ id }).deletedAt,
37}));
38
39const waitUntilGone = (serviceId: string) =>
40 readService(serviceId).pipe(
41 Effect.map((service) =>
42 service.deletedAt != null ? ("gone" as const) : ("found" as const),
43 ),
44 Effect.catchTag("RailwayNotFound", () => Effect.succeed("gone" as const)),
45 Effect.repeat({
46 schedule: Schedule.spaced("1 second"),
47 until: (status) => status === "gone",
48 times: 10,
49 }),
50 );
51
52test.provider(
53 "Nextjs: deploy, GET /, destroy, gone",
54 (stack) =>
55 Effect.gen(function* () {
56 yield* stack.destroy();
57
58 // Clone OUTSIDE the repo (OS temp dir): an in-workspace clone makes
59 // Next treat the alchemy monorepo as the workspace root and look up
60 // the root's typescript (catalog:build = tsgo, which has no JS compiler).
61 const rootDir = yield* cloneFixture(fixtureDir, {
62 prefix: "alchemy-nextjs-railway-",
63 entries: fixtureEntries,
64 });
65 yield* prepareNextjsFixture(rootDir);
66
67 const deployed = yield* stack.deploy(
68 Effect.gen(function* () {
69 const { project, environment } = yield* suitePartition;
70 const site = yield* Railway.Website.Nextjs("Web", {
71 project,
72 environment,
73 rootDir,
74 memo: {
75 include: [
76 "app/**",
77 "public/**",
78 "package.json",
79 "next.config.ts",
80 "tsconfig.json",
81 ],
82 },
83 });
84 return { site };
85 }),
86 );
87
88 const url = deployed.site.url;
89 expect(url).toBeDefined();
90 expect(url).toMatch(/^https:\/\//);
91 expect(deployed.site.service).toBeDefined();
92 expect(deployed.site.project).toBeDefined();
93
94 yield* expectUrlContains(`${url!}/`, "NEXTJS_AWS_PAGE_MARKER", {
95 timeout: "90 seconds",
96 label: "home page",
97 });
98 yield* expectUrlContains(
99 `${url!}/api/hello?echo=roundtrip`,
100 "NEXTJS_AWS_API_MARKER",
101 {
102 timeout: "30 seconds",
103 label: "api route",
104 },
105 );
106 yield* expectUrlContains(`${url!}/static`, "NEXTJS_AWS_STATIC_MARKER", {
107 timeout: "30 seconds",
108 label: "extra route",
109 });
110
111 const serviceId = deployed.site.service!.serviceId;
112 yield* stack.destroy();
113 const gone = yield* waitUntilGone(serviceId);
114 expect(gone).toEqual("gone");
115 }).pipe(logLevel),
116 {
117 tags: [
118 "provider:railway",
119 "provider:railway:project",
120 "provider:railway:projectenvironment",
121 "provider:railway:service",
122 "provider:railway:website",
123 "live",
124 ],
125 timeout: 120_000,
126 },
127);