| 1 | /** |
| 2 | * `@alchemy.run/frontend-frameworks/solidstart/aws` — the AWS Lambda deploy |
| 3 | * target for the SolidStart integration. |
| 4 | * |
| 5 | * SolidStart's server half is nitro (`@solidjs/vite-plugin-nitro-2`), so its |
| 6 | * AWS story is nitro's `aws-lambda` preset: `.output/server` is a |
| 7 | * self-contained Node ESM deployment unit whose `index.mjs` exports a Lambda |
| 8 | * `handler`, and `.output/public` holds the client assets (prerendered pages |
| 9 | * included) for the CDN. What this target owns: |
| 10 | * |
| 11 | * - **`nitroPreset`** — `"aws-lambda"`, enforced as the last word on the |
| 12 | * nitro config the integration hands to the appended nitro plugin. |
| 13 | * - **`configureNitro`** — enables response streaming by default |
| 14 | * (`awsLambda.streaming: true`): the emitted handler wraps |
| 15 | * `awslambda.streamifyResponse` and expects a Lambda Function URL with |
| 16 | * `invokeMode: RESPONSE_STREAM`. Set `streaming: false` on the target |
| 17 | * config for the buffered APIGW-style handler. |
| 18 | * - **`bundle`** — Node resolve conditions and the `@aws-sdk/` externals |
| 19 | * (the Lambda Node.js runtime ships SDK v3) for callers that post-process |
| 20 | * server code. |
| 21 | * |
| 22 | * No `finish`: nitro's aws-lambda output already ships its own |
| 23 | * `package.json` (`type: "module"`) and copied externals, so the server |
| 24 | * directory deploys as-is. |
| 25 | * |
| 26 | * No `devPlatform` seam: `dev` runs SolidStart's own Vite dev server, which |
| 27 | * is already the AWS Lambda programming model (plain Node). |
| 28 | */ |
| 29 | import * as Effect from "effect/Effect"; |
| 30 | import { runBuildChild } from "../core/BuildChild.ts"; |
| 31 | import { DeployTargetError, makeDeployTarget } from "../core/index.ts"; |
| 32 | import { |
| 33 | make, |
| 34 | type SolidStartTarget, |
| 35 | type SolidStartTargetConfig, |
| 36 | } from "./SolidStart.ts"; |
| 37 | |
| 38 | /** The nitro deployment preset this target builds with. */ |
| 39 | export const NITRO_PRESET = "aws-lambda"; |
| 40 | |
| 41 | /** |
| 42 | * The importable specifier of nitro's aws-lambda streaming runtime handler — |
| 43 | * the module a custom entry would re-export to wrap the framework's handler. |
| 44 | */ |
| 45 | export const NITRO_HANDLER_SPECIFIER = |
| 46 | "nitropack/presets/aws-lambda/runtime/aws-lambda-streaming"; |
| 47 | |
| 48 | /** AWS-specific knobs carried on the shared {@link SolidStartTargetConfig}. */ |
| 49 | export interface SolidStartAwsTargetConfig extends SolidStartTargetConfig { |
| 50 | /** |
| 51 | * Whether the emitted Lambda handler streams its response |
| 52 | * (`awslambda.streamifyResponse`, requires a Function URL with |
| 53 | * `invokeMode: RESPONSE_STREAM`). The buffered handler answers |
| 54 | * APIGW/Function-URL events with a complete response object. |
| 55 | * @default true |
| 56 | */ |
| 57 | readonly streaming?: boolean | undefined; |
| 58 | } |
| 59 | |
| 60 | const fail = (message: string, cause?: unknown) => |
| 61 | new DeployTargetError({ platform: "aws", message, cause }); |
| 62 | |
| 63 | /** |
| 64 | * The adapter-driven target — the shape the framework's regular vite/nitro |
| 65 | * build pipeline consumes. Used directly in the build child (where |
| 66 | * `cwd === root` holds); {@link makeAwsTarget} wraps it with the wholesale |
| 67 | * `build` hook that spawns the child. |
| 68 | */ |
| 69 | const makeAwsAdapterTarget = ( |
| 70 | config: SolidStartAwsTargetConfig = {}, |
| 71 | ): SolidStartTarget => |
| 72 | makeDeployTarget({ |
| 73 | platform: "aws", |
| 74 | config, |
| 75 | bundle: { |
| 76 | conditions: ["node", "import", "module"], |
| 77 | external: ["@aws-sdk/"], |
| 78 | }, |
| 79 | nitroPreset: NITRO_PRESET, |
| 80 | configureNitro: (nitroConfig) => { |
| 81 | const awsLambda = |
| 82 | nitroConfig.awsLambda !== null && |
| 83 | typeof nitroConfig.awsLambda === "object" |
| 84 | ? (nitroConfig.awsLambda as Record<string, unknown>) |
| 85 | : {}; |
| 86 | nitroConfig.awsLambda = { |
| 87 | ...awsLambda, |
| 88 | streaming: config.streaming ?? true, |
| 89 | }; |
| 90 | }, |
| 91 | }); |
| 92 | |
| 93 | /** |
| 94 | * A SolidStart build loads and executes the project's `vite.config.*` (its |
| 95 | * `solidStart()` plugin scans the route directory from `process.cwd()`, and |
| 96 | * user plugins may mutate `process.env` or `process.chdir`) — none of which |
| 97 | * may touch the engine process, where many deploys share one event loop. |
| 98 | * This target's wholesale `build` therefore runs the framework in a |
| 99 | * disposable child process whose working directory IS the project root (see |
| 100 | * `core/BuildChild.ts`). The shared `core/BuildChildRunner` entry imports |
| 101 | * this module in the child and calls the exported {@link buildInChild}. |
| 102 | */ |
| 103 | export interface SolidStartAwsBuildChildConfig { |
| 104 | readonly rootDir: string; |
| 105 | /** The (JSON-serializable) target config the parent was created with. */ |
| 106 | readonly config: SolidStartAwsTargetConfig; |
| 107 | } |
| 108 | |
| 109 | export const buildInChild = (config: SolidStartAwsBuildChildConfig) => |
| 110 | Effect.gen(function* () { |
| 111 | const framework = yield* make({ |
| 112 | root: config.rootDir, |
| 113 | // The adapter-only target: no wholesale `build` hook, so the child |
| 114 | // runs the regular vite/nitro build pipeline (no recursion). |
| 115 | target: makeAwsAdapterTarget(config.config), |
| 116 | nitro: config.config.nitro, |
| 117 | }); |
| 118 | return yield* framework.build({ root: config.rootDir }); |
| 119 | }); |
| 120 | |
| 121 | /** |
| 122 | * Create the AWS Lambda {@link SolidStartTarget}. See the module doc for the |
| 123 | * seams. |
| 124 | */ |
| 125 | export const makeAwsTarget = ( |
| 126 | config: SolidStartAwsTargetConfig = {}, |
| 127 | ): SolidStartTarget => ({ |
| 128 | ...makeAwsAdapterTarget(config), |
| 129 | build: (context) => |
| 130 | runBuildChild({ |
| 131 | module: import.meta.url, |
| 132 | rootDir: context.root, |
| 133 | framework: "solidstart", |
| 134 | config: { |
| 135 | rootDir: context.root, |
| 136 | config, |
| 137 | } satisfies SolidStartAwsBuildChildConfig, |
| 138 | }).pipe(Effect.mapError((error) => fail(error.message, error.cause))), |
| 139 | }); |
| 140 | |
| 141 | /** |
| 142 | * The deploy-target module contract (`resolveDeployTarget` accepts the |
| 143 | * default export — or the named `target` export — as a value or factory). |
| 144 | */ |
| 145 | export const target = makeAwsTarget; |
| 146 | |
| 147 | export default makeAwsTarget; |