| 1 | import { isDeployTarget } from "../../core/index.ts"; |
| 2 | import { NODE_BUNDLE_CONDITIONS } from "../../core/NodeServe.ts"; |
| 3 | import { spawn } from "node:child_process"; |
| 4 | import { once } from "node:events"; |
| 5 | import * as fs from "node:fs/promises"; |
| 6 | import * as net from "node:net"; |
| 7 | import * as path from "node:path"; |
| 8 | import { describe, expect, it, vi } from "vitest"; |
| 9 | import { makeProject, run } from "../../core/test/helpers.ts"; |
| 10 | import { makeNodeAdapter, makeNodeTarget, target } from "../node.ts"; |
| 11 | |
| 12 | describe("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 | }); |