1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import type * as Scope from "effect/Scope";
6import { describe, expect, it } from "vitest";
7import {
8 NODE_BUNDLE_CONDITIONS,
9 NODE_SERVE_ENTRY_FILE_NAME,
10} from "../../core/NodeServe.ts";
11import { SERVER_ENTRY_FILE_NAME, makeNodeTarget, target } from "../node.ts";
12import { DEFAULT_SERVER_BUILD_FILE } from "../ReactRouter.ts";
13
14const runWithNode = <A, E>(
15 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | Scope.Scope>,
16): Promise<A> =>
17 Effect.runPromise(
18 Effect.scoped(effect).pipe(
19 Effect.provide(NodeServices.layer),
20 ) as Effect.Effect<A, E>,
21 );
22
23describe("makeNodeTarget", () => {
24 it("declares the node platform, Node bundle conditions, and a finish pass", () => {
25 const node = makeNodeTarget();
26 expect(node.platform).toBe("node");
27 expect(node.serverEntryFileName).toBe(DEFAULT_SERVER_BUILD_FILE);
28 expect(SERVER_ENTRY_FILE_NAME).toBe("index.js");
29 expect(node.bundle?.conditions).toEqual([...NODE_BUNDLE_CONDITIONS]);
30 expect(node.bundle?.external ?? []).not.toContain("cloudflare:");
31 expect(node.bundle?.external ?? []).not.toContain("@aws-sdk/");
32 expect(node.bundle?.conditions ?? []).not.toContain("workerd");
33 expect(node.finish).toBeTypeOf("function");
34 expect(node.build).toBeTypeOf("function");
35 });
36
37 it("carries the buildDirectory override on its config", () => {
38 expect(
39 makeNodeTarget({ buildDirectory: "dist" }).config.buildDirectory,
40 ).toBe("dist");
41 });
42
43 it("exposes the named `target` module export as the factory", () => {
44 expect(target).toBe(makeNodeTarget);
45 });
46});
47
48describe("finish", () => {
49 it("emits a Node serve entry around the fetch handler, not a Lambda adapter", async () => {
50 const output = await runWithNode(
51 Effect.gen(function* () {
52 const fs = yield* FileSystem.FileSystem;
53 const path = yield* Path.Path;
54 const dir = yield* fs.makeTempDirectoryScoped({
55 prefix: "react-router-node-finish-",
56 });
57 const serverDir = path.join(dir, "server");
58 const clientDir = path.join(dir, "client");
59 yield* fs.makeDirectory(serverDir, { recursive: true });
60 yield* fs.makeDirectory(clientDir, { recursive: true });
61 yield* fs.writeFileString(
62 path.join(serverDir, SERVER_ENTRY_FILE_NAME),
63 "export default { fetch() {} };",
64 );
65 const node = makeNodeTarget();
66 return yield* node.finish!(
67 {
68 distDirectory: dir,
69 clientDirectory: clientDir,
70 serverModules: [],
71 externalWorkspaces: new Set<string>(),
72 },
73 {
74 root: dir,
75 framework: "react-router",
76 entry: path.join(serverDir, SERVER_ENTRY_FILE_NAME),
77 },
78 );
79 }),
80 );
81 const names = output.serverModules?.map((module) => module.name) ?? [];
82 expect(names[0]).toBe(`server/${NODE_SERVE_ENTRY_FILE_NAME}`);
83 const serve = output.serverModules?.[0];
84 const source = String(serve?.content);
85 expect(source).toContain("/health");
86 expect(source).toContain("process.env.PORT");
87 expect(source).toContain(`from "./${SERVER_ENTRY_FILE_NAME}"`);
88 expect(source).toContain("fetchHandler");
89 expect(source).not.toContain("aws-lambda");
90 expect(source).not.toContain("streamifyResponse");
91 expect(source).not.toContain("workerd");
92 expect(source).not.toContain("@aws-sdk/");
93 });
94
95 it("fails when the framework produced no on-disk entry", async () => {
96 const error = await runWithNode(
97 Effect.gen(function* () {
98 const node = makeNodeTarget();
99 return yield* node.finish!(
100 {
101 distDirectory: "/tmp",
102 clientDirectory: "/tmp/client",
103 serverModules: [],
104 externalWorkspaces: new Set<string>(),
105 },
106 { root: "/tmp", framework: "react-router" },
107 ).pipe(Effect.flip);
108 }),
109 );
110 expect(error._tag).toBe("DeployTargetError");
111 expect(error.message).toContain("no on-disk server entry");
112 });
113});