1import type { InputProps } from "../../Input.ts";
2import * as Namespace from "../../Namespace.ts";
3import { makeFrameworkSite, type FrameworkSiteProps } from "./FrameworkSite.ts";
4
5/** The framework-integration package that drives the Astro build. */
6export const ASTRO_FRAMEWORK_SPECIFIER =
7 "@alchemy.run/frontend-frameworks/astro";
8
9/** The AWS Lambda deploy target for the Astro build. */
10export const ASTRO_AWS_TARGET_SPECIFIER =
11 "@alchemy.run/frontend-frameworks/astro/aws";
12
13export interface AstroProps extends FrameworkSiteProps {
14 // Astro is configured in YOUR `astro.config.*`, which loads natively —
15 // this resource only deploys it. The `astro` bag is a deploy-time
16 // overlay merged OVER the file for values a config file cannot hold
17 // (per-stage values, other resources' Outputs).
18 /**
19 * Deploy-time Astro config overrides, merged OVER your natively-loaded
20 * `astro.config.*` (values here win). Use it for values that vary per
21 * stage or derive from other resources' Outputs — everything else
22 * belongs in the config file.
23 */
24 astro?: {
25 /** Deployed URL origin (astro's `site`). */
26 site?: string;
27 /** Base path the site is served from (astro's `base`). */
28 base?: string;
29 /**
30 * Astro output target — a deploy-topology decision (whether a
31 * server function exists). `"server"` renders pages on demand in
32 * the Lambda; individual pages opt into prerendering with
33 * `export const prerender = true`. `"static"` prerenders every
34 * page at build time and deploys assets-only (no Lambda).
35 * Supersedes a file-level `output`.
36 * @default "server"
37 */
38 output?: "server" | "static";
39 /** Source directory (astro's `srcDir`). */
40 srcDir?: string;
41 /** Public assets directory (astro's `publicDir`). */
42 publicDir?: string;
43 /** Build output directory (astro's `outDir`). */
44 outDir?: string;
45 /** Trailing-slash handling (astro's `trailingSlash`). */
46 trailingSlash?: "always" | "never" | "ignore";
47 };
48 /**
49 * Path to an alternate astro config file, relative to `rootDir`.
50 * Defaults to astro's own config discovery.
51 */
52 config?: string;
53 /**
54 * Serve the built error page (e.g. astro's `404.html`) for requests that
55 * match no uploaded file. Only applies to `astro: { output: "static" }`
56 * sites — a server-backed site forwards misses to the Lambda instead.
57 */
58 errorPage?: string;
59 /**
60 * Answer misses with the index page (200) instead of a 404. Only applies
61 * to `astro: { output: "static" }` sites.
62 */
63 spa?: boolean;
64}
65
66/**
67 * Deploy an [Astro](https://astro.build) application to AWS: the server
68 * bundle on a streaming Lambda Function URL, static assets (prerendered
69 * pages included) in S3, and a CloudFront distribution whose edge router
70 * serves uploaded files from S3 and forwards everything else to the server.
71 *
72 * The build runs through `@alchemy.run/frontend-frameworks/astro` with the
73 * `@alchemy.run/frontend-frameworks/astro/aws` deploy target (a wrangler-free
74 * AWS Lambda adapter is injected — your `astro.config.*` must not declare
75 * one) — the package must be installed in your project.
76 *
77 * Pages render on demand by default (`output: "server"`); pages that
78 * `export const prerender = true` are prerendered at build time and served
79 * from S3. With `astro: { output: "static" }` every page is prerendered
80 * and the deploy is assets-only — no Lambda.
81 *
82 * Your `astro.config.*` is the home for Astro configuration and loads
83 * natively; the `astro` prop is a deploy-time override bag merged OVER
84 * the file for values that vary per stage or derive from other
85 * resources' Outputs. `config` points at an alternate config file
86 * (relative to `rootDir`).
87 *
88 * ### Creating Astro Sites
89 * **Example:** Basic Astro App
90 * ```typescript
91 * const site = yield* AWS.Website.Astro("Web", {
92 * rootDir: "./app",
93 * });
94 * ```
95 *
96 * **Example:** Per-stage site URL override
97 * ```typescript
98 * const site = yield* AWS.Website.Astro("Blog", {
99 * rootDir: "./blog",
100 * astro: { site: "https://blog.example.com" },
101 * });
102 * ```
103 *
104 * **Example:** Custom Domain
105 * ```typescript
106 * const site = yield* AWS.Website.Astro("Web", {
107 * rootDir: "./app",
108 * domain: {
109 * name: "app.example.com",
110 * hostedZoneId: zone.hostedZoneId,
111 * },
112 * });
113 * ```
114 *
115 * ### Static Sites
116 * **Example:** Fully Static Astro Site
117 * ```typescript
118 * const site = yield* AWS.Website.Astro("Docs", {
119 * rootDir: "./docs",
120 * astro: { output: "static" },
121 * errorPage: "404.html",
122 * });
123 * ```
124 *
125 * ### Server Configuration
126 * **Example:** Tune The Server Function
127 * ```typescript
128 * const site = yield* AWS.Website.Astro("Web", {
129 * rootDir: "./app",
130 * memorySize: 2048,
131 * env: {
132 * API_BASE: api.url,
133 * },
134 * });
135 * ```
136 *
137 * @resource
138 */
139export const Astro = (id: string, props: InputProps<AstroProps> = {}) => {
140 const p = props as AstroProps;
141 // Server output is the documented default: astro's own zero-config
142 // default is `"static"`. The inline config merges OVER the project's
143 // `astro.config.*`, so an explicit file-level `output` is superseded;
144 // opt into a fully prerendered site with `astro: { output: "static" }`.
145 const output = p.astro?.output ?? "server";
146 return makeFrameworkSite(id, props, {
147 name: "Astro",
148 framework: ASTRO_FRAMEWORK_SPECIFIER,
149 target: ASTRO_AWS_TARGET_SPECIFIER,
150 options: { astro: { ...p.astro, output }, config: p.config },
151 static:
152 output === "static" ? { spa: p.spa, errorPage: p.errorPage } : undefined,
153 }).pipe(Namespace.push(id));
154};