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 { makeAwsTarget } from "../aws.ts";
8import { fromHarnessOptions } from "../index.ts";
9import {
10 DEFAULT_TARGET_SPECIFIER,
11 make,
12 readNitroOutput,
13 SERVER_ENTRY_NAME,
14 type SolidStartTarget,
15} from "../SolidStart.ts";
16
17const runWithNode = <A, E>(
18 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | Scope.Scope>,
19): Promise<A> =>
20 Effect.runPromise(
21 Effect.scoped(effect).pipe(
22 Effect.provide(NodeServices.layer),
23 ) as Effect.Effect<A, E>,
24 );
25
26describe("readNitroOutput", () => {
27 it("maps a nitro .output tree onto the BuildOutput contract, entry first", async () => {
28 const output = await runWithNode(
29 Effect.gen(function* () {
30 const fs = yield* FileSystem.FileSystem;
31 const path = yield* Path.Path;
32 const dir = yield* fs.makeTempDirectoryScoped({
33 prefix: "solidstart-output-",
34 });
35 const serverDir = path.join(dir, "server");
36 const publicDir = path.join(dir, "public");
37 yield* fs.makeDirectory(path.join(serverDir, "chunks"), {
38 recursive: true,
39 });
40 yield* fs.makeDirectory(publicDir, { recursive: true });
41 // alphabetically before index.mjs, to prove entry-first sorting
42 yield* fs.writeFileString(
43 path.join(serverDir, "chunks", "a.mjs"),
44 "export const a = 1;",
45 );
46 yield* fs.writeFileString(
47 path.join(serverDir, "index.mjs"),
48 "export const handler = () => {};",
49 );
50 yield* fs.writeFileString(
51 path.join(publicDir, "robots.txt"),
52 "User-agent: *\n",
53 );
54 return yield* readNitroOutput({ dir, serverDir, publicDir });
55 }),
56 );
57 expect(output.serverModules?.map((module) => module.name)).toEqual([
58 SERVER_ENTRY_NAME,
59 "server/chunks/a.mjs",
60 ]);
61 expect(output.serverModules?.[0]?.hash).toMatch(/^[0-9a-f]{64}$/);
62 expect(output.clientDirectory?.endsWith("public")).toBe(true);
63 expect(output.distDirectory).toBeDefined();
64 expect(output.externalWorkspaces.size).toBe(0);
65 });
66
67 it("fails when the server directory has no index.mjs entry", async () => {
68 const result = await runWithNode(
69 Effect.gen(function* () {
70 const fs = yield* FileSystem.FileSystem;
71 const path = yield* Path.Path;
72 const dir = yield* fs.makeTempDirectoryScoped({
73 prefix: "solidstart-output-",
74 });
75 const serverDir = path.join(dir, "server");
76 yield* fs.makeDirectory(serverDir, { recursive: true });
77 yield* fs.writeFileString(
78 path.join(serverDir, "other.mjs"),
79 "export const x = 1;",
80 );
81 return yield* Effect.result(
82 readNitroOutput({
83 dir,
84 serverDir,
85 publicDir: path.join(dir, "public"),
86 }),
87 );
88 }),
89 );
90 expect(result._tag).toBe("Failure");
91 if (result._tag === "Failure") {
92 expect(String(result.failure)).toContain("server/index.mjs");
93 }
94 });
95});
96
97describe("make", () => {
98 it("defaults to this package's AWS deploy target", () => {
99 expect(DEFAULT_TARGET_SPECIFIER).toBe(
100 "@alchemy.run/frontend-frameworks/solidstart/aws",
101 );
102 });
103
104 it("rejects a caller-supplied nitro preset that fights the deploy target", async () => {
105 const result = await runWithNode(
106 Effect.gen(function* () {
107 const framework = yield* make({
108 root: "/tmp/does-not-matter",
109 // The adapter-only target: no wholesale `build`, so the preset
110 // conflict check runs before anything touches the project.
111 target: adapterOnly(makeAwsTarget()),
112 nitro: { preset: "netlify" },
113 });
114 return yield* Effect.result(
115 framework.build({ root: "/tmp/does-not-matter" }),
116 );
117 }),
118 );
119 expect(result._tag).toBe("Failure");
120 if (result._tag === "Failure") {
121 expect(result.failure._tag).toBe("FrameworkError");
122 expect(result.failure.message).toContain('"netlify"');
123 expect(result.failure.message).toContain('"aws-lambda"');
124 }
125 });
126
127 it("build fails with a descriptive FrameworkError outside a SolidStart project", async () => {
128 // /tmp has no vite install: loading the project's Vite is the first
129 // step after the target resolves and must surface a FrameworkError.
130 const result = await runWithNode(
131 Effect.gen(function* () {
132 const framework = yield* make({
133 root: "/tmp/does-not-matter",
134 target: adapterOnly(makeAwsTarget()),
135 });
136 return yield* Effect.result(
137 framework.build({ root: "/tmp/does-not-matter" }),
138 );
139 }),
140 );
141 expect(result._tag).toBe("Failure");
142 if (result._tag === "Failure") {
143 expect(result.failure._tag).toBe("FrameworkError");
144 expect(result.failure.framework).toBe("solidstart");
145 }
146 });
147
148 it("exposes the framework contract (build + dev)", async () => {
149 const framework = await runWithNode(make({ root: "/tmp/does-not-matter" }));
150 expect(typeof framework.build).toBe("function");
151 expect(typeof framework.dev).toBe("function");
152 });
153});
154
155describe("fromHarnessOptions", () => {
156 it("forwards the harness's nitro overrides", () => {
157 expect(
158 fromHarnessOptions({ solidstart: { nitro: { prerender: {} } } }).nitro,
159 ).toEqual({ prerender: {} });
160 expect(fromHarnessOptions({}).nitro).toBeUndefined();
161 });
162});
163
164/** Drop the wholesale `build` hook so the in-process pipeline runs. */
165const adapterOnly = (target: SolidStartTarget): SolidStartTarget => {
166 const { build: _build, ...rest } = target;
167 return rest;
168};