1import { isDeployTarget } from "../../core/index.ts";
2import { NODE_BUNDLE_CONDITIONS } from "../../core/NodeServe.ts";
3import { spawn } from "node:child_process";
4import { once } from "node:events";
5import * as fs from "node:fs/promises";
6import * as net from "node:net";
7import * as path from "node:path";
8import { describe, expect, it, vi } from "vitest";
9import { makeProject, run } from "../../core/test/helpers.ts";
10import { makeNodeAdapter, makeNodeTarget, target } from "../node.ts";
11
12describe("makeNodeTarget", () => {
13 it("is a DeployTarget for the node platform with a finish pass", () => {
14 const node = makeNodeTarget({
15 adapter: { notFoundHandling: "single-page-application" },
16 });
17 expect(isDeployTarget(node)).toBe(true);
18 expect(node.platform).toBe("node");
19 expect(node.bundle?.conditions).toEqual([...NODE_BUNDLE_CONDITIONS]);
20 expect(node.bundle?.external ?? []).not.toContain("cloudflare:");
21 expect(node.bundle?.external ?? []).not.toContain("@aws-sdk/");
22 expect(node.build).toBeTypeOf("function");
23 expect(node.finish).toBeTypeOf("function");
24 });
25
26 it("produces the in-memory kit adapter from the adapter hook", () => {
27 const adapter = makeNodeAdapter();
28 expect(adapter.name).toBe(
29 "@alchemy.run/frontend-frameworks/sveltekit/node",
30 );
31 expect(adapter.result.current).toBeUndefined();
32 expect(typeof adapter.adapt).toBe("function");
33 });
34
35 it("exposes the named `target` module export as the factory", () => {
36 expect(target).toBe(makeNodeTarget);
37 });
38
39 it("serves SSR and static assets from a relocated dist directory", async () => {
40 const root = await makeProject({
41 "entry.js": 'export const handler = () => new Response("SSR home");',
42 ".svelte-kit/node/robots.txt": "User-agent: *",
43 ".svelte-kit/node/about.html": "Prerendered about",
44 ".svelte-kit/node/docs/index.html": "Prerendered docs",
45 ".svelte-kit/node/_app/immutable/app.css": "body { color: red; }",
46 "dist/client/stale.txt": "Removed in this build",
47 });
48 const output = await run(
49 makeNodeTarget().finish!(
50 {
51 distDirectory: path.join(root, "dist"),
52 clientDirectory: path.join(root, ".svelte-kit/node"),
53 serverModules: [],
54 externalWorkspaces: new Set(),
55 },
56 { root, entry: path.join(root, "entry.js") },
57 ),
58 );
59 expect(output.clientDirectory).toBe(path.join(root, "dist/client"));
60 await expect(
61 fs.stat(path.join(root, "dist/client/stale.txt")),
62 ).rejects.toThrow();
63
64 const deployed = await makeProject({});
65 await fs.cp(path.join(root, "dist"), deployed, { recursive: true });
66 await fs.rm(root, { recursive: true, force: true });
67
68 const reservation = net.createServer();
69 reservation.listen(0, "127.0.0.1");
70 await once(reservation, "listening");
71 const port = (reservation.address() as net.AddressInfo).port;
72 await new Promise<void>((resolve, reject) =>
73 reservation.close((error) => (error ? reject(error) : resolve())),
74 );
75 const child = spawn(
76 process.execPath,
77 [path.join(deployed, output.serverModules![0]!.name)],
78 {
79 cwd: deployed,
80 env: { ...process.env, PORT: String(port) },
81 stdio: "ignore",
82 },
83 );
84 const exited = once(child, "exit");
85 const base = `http://127.0.0.1:${port}`;
86 try {
87 await vi.waitFor(
88 async () => {
89 expect((await fetch(`${base}/health`)).status).toBe(200);
90 },
91 { timeout: 5_000, interval: 50 },
92 );
93 for (const [url, body] of [
94 ["/", "SSR home"],
95 ["/about", "Prerendered about"],
96 ["/docs/", "Prerendered docs"],
97 ["/robots.txt", "User-agent: *"],
98 ["/_app/immutable/app.css", "body { color: red; }"],
99 ]) {
100 const response = await fetch(`${base}${url}`);
101 expect(response.status).toBe(200);
102 expect(await response.text()).toBe(body);
103 }
104 } finally {
105 child.kill();
106 await exited;
107 }
108 });
109});