1import {
2 Framework,
3 FrameworkError,
4 loadProjectModule,
5} from "@alchemy.run/frontend-frameworks/core";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import { Cwd } from "./Cwd.ts";
9import * as Options from "./Options.ts";
10import * as Vite from "./Vite.ts";
11
12/**
13 * The built-in Vite implementation of framework-core's `Framework` service —
14 * the default when `e2e.config.ts` names no framework. Framework packages
15 * (waku/astro/sveltekit/nextjs) provide their own layers with the same shape.
16 */
17export const makeViteFramework = (
18 options: Options.Options,
19): Options.Options.FrameworkLayer =>
20 Layer.effect(
21 Framework,
22 Effect.gen(function* () {
23 const vite = yield* Vite.Vite;
24 const { worker } = Options.resolveCloudflareOptions(options);
25 const wrapError = (error: Vite.ViteError) =>
26 new FrameworkError({
27 framework: "vite",
28 message: error.message,
29 cause: error.cause,
30 });
31 return Framework.of({
32 build: (buildOptions) =>
33 vite
34 .build(
35 worker,
36 buildOptions?.root !== undefined
37 ? { root: buildOptions.root }
38 : undefined,
39 )
40 .pipe(Effect.mapError(wrapError)),
41 dev: (devOptions) =>
42 vite
43 .dev(worker, {
44 ...(devOptions?.root !== undefined
45 ? { root: devOptions.root }
46 : undefined),
47 ...(devOptions?.port !== undefined
48 ? { server: { port: devOptions.port } }
49 : undefined),
50 })
51 .pipe(
52 Effect.map(({ url }) => ({ url })),
53 Effect.mapError(wrapError),
54 ),
55 });
56 }),
57 ).pipe(Layer.provide(Vite.ViteLive));
58
59/** The exports a framework package may provide (see {@link resolve}). */
60interface FrameworkModule {
61 readonly default?: unknown;
62 readonly framework?: unknown;
63}
64
65const invalidExport = (specifier: string) =>
66 new FrameworkError({
67 framework: specifier,
68 message:
69 `"${specifier}" does not provide a usable framework export: expected a default export ` +
70 "(or named export `framework`) that is a `(options) => Layer<Framework>` factory " +
71 "or a `Layer<Framework>`",
72 });
73
74const toFrameworkLayer: (
75 candidate: unknown,
76 options: Options.Options,
77 specifier: string,
78) => Effect.Effect<
79 Options.Options.FrameworkLayer,
80 FrameworkError | unknown,
81 Options.Options.FrameworkServices
82> = Effect.fn(function* (
83 candidate: unknown,
84 options: Options.Options,
85 specifier: string,
86) {
87 const resolved = Effect.isEffect(candidate)
88 ? yield* candidate as Effect.Effect<
89 unknown,
90 unknown,
91 Options.Options.FrameworkServices
92 >
93 : candidate;
94 if (Layer.isLayer(resolved)) {
95 return resolved as Options.Options.FrameworkLayer;
96 }
97 if (typeof resolved === "function") {
98 return yield* toFrameworkLayer(
99 (resolved as Options.Options.FrameworkFactory)(options),
100 options,
101 specifier,
102 );
103 }
104 return yield* Effect.fail(invalidExport(specifier));
105});
106
107/**
108 * Resolve the `Framework` Layer for a fixture from its options:
109 *
110 * - no `framework` — the built-in Vite implementation over `options.vite`
111 * - a string — imported from the fixture's own `node_modules` (via
112 * framework-core's `loadProjectModule`, so the fixture's installed copy of
113 * the framework is the one driven); its default export (or named export
114 * `framework`) is applied to the options
115 * - a factory — applied to the options
116 * - a Layer — used as-is
117 */
118export const resolve = Effect.fn(function* (options: Options.Options) {
119 const input = options.framework;
120 if (input === undefined) {
121 return makeViteFramework(options);
122 }
123 if (typeof input === "string") {
124 const cwd = yield* Cwd;
125 const module_ = yield* loadProjectModule<FrameworkModule>(cwd, input).pipe(
126 Effect.mapError(
127 (error) =>
128 new FrameworkError({
129 framework: input,
130 message: error.message,
131 cause: error.cause,
132 }),
133 ),
134 );
135 const candidate = module_.default ?? module_.framework;
136 if (candidate === undefined) {
137 return yield* Effect.fail(invalidExport(input));
138 }
139 return yield* toFrameworkLayer(candidate, options, input);
140 }
141 return yield* toFrameworkLayer(
142 input,
143 options,
144 "e2e.config.ts framework option",
145 );
146});
147
148/**
149 * The `Framework` Layer for the current fixture: loads `e2e.config.ts` and
150 * resolves its `framework` option (defaulting to the built-in Vite
151 * implementation). Everything downstream (`Cli`, `Server`) dispatches through
152 * the `Framework` service and never talks to Vite directly.
153 */
154export const layer: Layer.Layer<
155 Framework,
156 unknown,
157 Options.Options.FrameworkServices
158> = Layer.unwrap(
159 Effect.gen(function* () {
160 const options = yield* Options.load();
161 return yield* resolve(options);
162 }),
163);