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