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 Vite build. */
6export const VITE_FRAMEWORK_SPECIFIER = "@alchemy.run/frontend-frameworks/vite";
7
8/** The AWS deploy target for the Vite build. */
9export const VITE_AWS_TARGET_SPECIFIER =
10 "@alchemy.run/frontend-frameworks/vite/aws";
11
12/**
13 * Serializable Vite build overrides merged OVER the project's own
14 * `vite.config.*` at deploy time (deploy-time values win). Mirrors the
15 * integration seam's `ViteBuildConfig`
16 * (`@alchemy.run/frontend-frameworks/vite`). Deliberately a small,
17 * JSON-serializable subset — plugins and other non-serializable config
18 * belong in the config file, which loads natively.
19 */
20export interface ViteBuildConfig {
21 /**
22 * Public base path the site deploys under (vite's `base`). Overrides
23 * the config file's `base` for both the production build and the dev
24 * server under `alchemy dev`.
25 */
26 base?: string;
27 /**
28 * Build output directory, relative to `rootDir` (vite's
29 * `build.outDir`). Overrides the config file's `build.outDir`.
30 * @default the config file's `build.outDir` (vite's default: "dist")
31 */
32 outDir?: string;
33}
34
35export interface ViteProps extends Omit<
36 FrameworkSiteProps,
37 // Assets-only: a plain Vite site never creates a server function.
38 "env" | "memorySize" | "timeout" | "architecture" | "runtime"
39> {
40 /**
41 * Deploy-time Vite overrides merged over your `vite.config.*`. The
42 * config file is the primary home for Vite configuration (it loads
43 * natively, plugins included) — reach for this bag when a value is
44 * decided by the deployment rather than the project, since config
45 * files cannot consume alchemy `Output`s.
46 */
47 vite?: ViteBuildConfig;
48 /**
49 * Alternate Vite config file to load instead of the auto-discovered
50 * `vite.config.*`, relative to {@link FrameworkSiteProps.rootDir | rootDir}.
51 */
52 config?: string;
53 /**
54 * Answer misses with the index page (200) instead of a 404, so
55 * client-side routes deep-link correctly. Plain Vite apps are typically
56 * single-page applications, so this defaults on; set `false` for
57 * multi-page (`index.html`-per-route) projects. Mutually exclusive with
58 * {@link errorPage}.
59 * @default true unless `errorPage` is set
60 */
61 spa?: boolean;
62 /**
63 * Serve the built error page (e.g. `404.html`) with a real `404` status
64 * for requests that match no uploaded file. Mutually exclusive with
65 * {@link spa}.
66 */
67 errorPage?: string;
68}
69
70/**
71 * Assemble the framework-integration `options` bag from the resource's
72 * `vite` overrides and `config` file selection. The integration seam is
73 * `ViteOptions.vite` (`@alchemy.run/frontend-frameworks/vite`), whose
74 * `configFile` is resolved relative to the project root. Shared with
75 * {@link Foldkit | AWS.Website.Foldkit}, which builds through the same
76 * pipeline.
77 *
78 * @internal
79 */
80export const viteFrameworkOptions = (
81 p: Pick<ViteProps, "vite" | "config">,
82): Record<string, unknown> | undefined =>
83 p.vite !== undefined || p.config !== undefined
84 ? {
85 vite: {
86 ...p.vite,
87 ...(p.config !== undefined ? { configFile: p.config } : undefined),
88 },
89 }
90 : undefined;
91
92/**
93 * Deploy a plain [Vite](https://vite.dev) application to AWS: static assets
94 * (the `vite build` output) in S3 behind a CloudFront distribution. For
95 * client-only projects — React/Vue/Solid SPAs, `index.html` multi-page
96 * apps — whose entire deployable output is static assets.
97 *
98 * The build runs through `@alchemy.run/frontend-frameworks/vite` with the
99 * `@alchemy.run/frontend-frameworks/vite/aws` deploy target — the package
100 * must be installed in your project. Your project's own `vite.config.*`
101 * (plugins included) drives the build; input files are content-hashed so
102 * unchanged projects skip the build and deploy entirely.
103 *
104 * During `alchemy dev` the site is Vite's own dev server (native HMR) and
105 * no cloud resources are created — the site's `url` is the dev server's
106 * local address. `Alchemy.remote()` opts back into the full deployment.
107 *
108 * SSR frameworks that wrap Vite deploy through their own composites
109 * ({@link Astro | AWS.Website.Astro}, {@link SvelteKit | AWS.Website.SvelteKit},
110 * {@link Octane | AWS.Website.Octane}, ...) — this composite never creates
111 * a server function.
112 *
113 * ### Creating Vite Sites
114 * **Example:** Basic Vite SPA
115 * ```typescript
116 * const site = yield* AWS.Website.Vite("Web");
117 * ```
118 *
119 * **Example:** Project in a Subdirectory
120 * ```typescript
121 * const site = yield* AWS.Website.Vite("Web", {
122 * rootDir: "./app",
123 * });
124 * ```
125 *
126 * **Example:** Custom Domain
127 * ```typescript
128 * const site = yield* AWS.Website.Vite("Web", {
129 * domain: {
130 * name: "app.example.com",
131 * hostedZoneId: zone.hostedZoneId,
132 * },
133 * });
134 * ```
135 *
136 * ### Multi-Page Sites
137 * **Example:** Per-Route HTML Pages with a 404 Page
138 * ```typescript
139 * const site = yield* AWS.Website.Vite("Docs", {
140 * spa: false,
141 * errorPage: "404.html",
142 * });
143 * ```
144 *
145 * ### Sharing a Router
146 * **Example:** Serve Through an Existing AWS.Website.Router
147 * ```typescript
148 * const router = yield* AWS.Website.Router("Router", {});
149 * const site = yield* AWS.Website.Vite("Web", {
150 * domain: { router },
151 * });
152 * ```
153 *
154 * ### Build Configuration
155 * Vite configuration lives in your project's own `vite.config.*`, which
156 * loads natively — plugins included. The `vite` bag is for deploy-time
157 * overrides merged over that file (config files cannot consume alchemy
158 * `Output`s), and `config` selects an alternate config file.
159 *
160 * **Example:** Deploy-Time Base Path Override
161 * ```typescript
162 * const site = yield* AWS.Website.Vite("Docs", {
163 * vite: { base: "/docs/" },
164 * });
165 * ```
166 *
167 * **Example:** Alternate Config File
168 * ```typescript
169 * const site = yield* AWS.Website.Vite("Web", {
170 * config: "vite.deploy.config.ts",
171 * });
172 * ```
173 *
174 * ### Local Development
175 * **Example:** Vite Dev Server Under `alchemy dev`
176 * ```typescript
177 * // `alchemy dev` starts `vite` programmatically: site.url is the local
178 * // dev server (HMR included); no bucket or distribution is created.
179 * const site = yield* AWS.Website.Vite("Web");
180 * ```
181 *
182 * @resource
183 */
184export const Vite = (id: string, props: InputProps<ViteProps> = {}) => {
185 const p = props as ViteProps;
186 return makeFrameworkSite(id, props, {
187 name: "Vite",
188 framework: VITE_FRAMEWORK_SPECIFIER,
189 target: VITE_AWS_TARGET_SPECIFIER,
190 options: viteFrameworkOptions(p),
191 // Plain Vite is assets-only: every page is client-rendered from the
192 // uploaded bundle, so the deploy never creates a server function.
193 // `spa` defaults on (Vite apps are typically SPAs) but yields to an
194 // explicit `errorPage` — the two are mutually exclusive downstream.
195 static: {
196 spa: p.spa ?? (p.errorPage === undefined ? true : undefined),
197 errorPage: p.errorPage,
198 },
199 }).pipe(Namespace.push(id));
200};