1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Layer from "effect/Layer";
4import * as Path from "effect/Path";
5import {
6 finishNeonOutput,
7 makeNeonServeEntrySource,
8 makeNeonTarget,
9} from "../core/NeonServe.ts";
10import { toOutputFile } from "../core/BuildOutput.ts";
11import { DeployTargetError } from "../core/DeployTarget.ts";
12import { pinNodeServeModule } from "../core/NodeServe.ts";
13import type { FrameworkBuildOptions } from "../core/Framework.ts";
14import {
15 make as makeNode,
16 makeNodeTarget,
17 NEXT_PRODUCTION_APP_SOURCE,
18 type NextjsNodeOptions,
19} from "./node.ts";
20
21/** Next.js production Node handler adapted to Neon's Fetch interface. */
22export const target = (config?: Parameters<typeof makeNodeTarget>[0]) => {
23 const node = makeNodeTarget(config);
24 return {
25 ...makeNeonTarget(node),
26 build: (context: Parameters<NonNullable<typeof node.build>>[0]) =>
27 Effect.gen(function* () {
28 const fs = yield* FileSystem.FileSystem;
29 const path = yield* Path.Path;
30 const output = yield* node.build!(context).pipe(
31 Effect.flatMap(finishNeonOutput),
32 );
33 const nodeServe = {
34 ...output.nodeServe,
35 handler: {
36 kind: "fetch" as const,
37 imports: [
38 'import requestMeta from "next/dist/server/request-meta.js";',
39 'import { toFetchHandler } from "./neon-node-adapter.mjs";',
40 NEXT_PRODUCTION_APP_SOURCE,
41 "const nextHandler = app.getRequestHandler();",
42 "const fetchNext = request => {",
43 " const url = new URL(request.url);",
44 " return toFetchHandler((req, res) => {",
45 " const protocol = url.protocol.slice(0, -1);",
46 " req.headers.host = url.host;",
47 ' req.headers["x-forwarded-host"] = url.host;',
48 ' req.headers["x-forwarded-proto"] = protocol;',
49 ' req.headers["x-forwarded-port"] = url.port || (protocol === "https" ? "443" : "80");',
50 " const meta = requestMeta.getRequestMeta(req);",
51 " // Fetch supplies the initial URL; Next's router and renderer must not replace it with a listening address.",
52 " Object.defineProperties(meta, {",
53 " initURL: { enumerable: true, get: () => request.url, set() {} },",
54 " initProtocol: { enumerable: true, get: () => protocol, set() {} },",
55 " });",
56 " requestMeta.setRequestMeta(req, meta);",
57 " Promise.resolve(nextHandler(req, res)).catch(error => res.destroy(error));",
58 " })(request);",
59 "};",
60 ].join("\n"),
61 expr: "fetchNext",
62 },
63 };
64 const name = output.serverModules![0]!.name;
65 const source = makeNeonServeEntrySource(nodeServe);
66 yield* fs.writeFileString(
67 path.join(output.distDirectory!, name),
68 source,
69 );
70 return pinNodeServeModule(
71 { ...output, nodeServe },
72 yield* toOutputFile(name, source),
73 );
74 }).pipe(
75 Effect.mapError((cause) =>
76 cause instanceof DeployTargetError
77 ? cause
78 : new DeployTargetError({
79 platform: "neon",
80 message: "Failed to emit the Next.js Fetch entry.",
81 cause,
82 }),
83 ),
84 ),
85 };
86};
87export default target;
88
89/** Native Next.js development with Neon-compatible production output. */
90export const make = Effect.fn(function* (options: NextjsNodeOptions = {}) {
91 const fs = yield* FileSystem.FileSystem;
92 const path = yield* Path.Path;
93 const node = yield* makeNode(options);
94 const services = Layer.mergeAll(
95 Layer.succeed(FileSystem.FileSystem)(fs),
96 Layer.succeed(Path.Path)(path),
97 );
98 return {
99 dev: node.dev,
100 build: (buildOptions?: FrameworkBuildOptions) =>
101 Effect.gen(function* () {
102 const root = yield* Effect.sync(() =>
103 path.resolve(buildOptions?.root ?? options.root ?? process.cwd()),
104 );
105 return yield* target({ root }).build!({
106 root,
107 framework: "nextjs",
108 env: buildOptions?.env,
109 });
110 }).pipe(Effect.provide(services)),
111 };
112});