1import * as Namespace from "../../Namespace.ts";
2import {
3 makeFrameworkSite,
4 staticConfigFromAssets,
5 type FrameworkSiteProps,
6} from "./FrameworkSite.ts";
7
8/** The framework-integration package that drives the Vite build. */
9export const VITE_FRAMEWORK_SPECIFIER = "@alchemy.run/frontend-frameworks/vite";
10
11/** The Node container deploy target for the Vite build. */
12export const VITE_NODE_TARGET_SPECIFIER =
13 "@alchemy.run/frontend-frameworks/vite/node";
14
15const viteOptions = (props: ViteProps) =>
16 props.vite !== undefined &&
17 (props.vite.outDir !== undefined || props.vite.base !== undefined)
18 ? { vite: props.vite }
19 : undefined;
20
21export interface ViteProps extends FrameworkSiteProps {
22 /**
23 * Serializable Vite config merged OVER the project's own `vite.config.*`.
24 */
25 vite?: {
26 outDir?: string;
27 base?: string;
28 };
29}
30
31/**
32 * Deploy a plain [Vite](https://vite.dev) application to a Hetzner Cloud
33 * Server: `vite build` output served by a static-file systemd unit on
34 * port 3000. For client-only projects — React/Vue/Solid SPAs,
35 * `index.html` multi-page apps — whose entire deployable output is
36 * static assets.
37 *
38 * The build runs through `@alchemy.run/frontend-frameworks/vite` with the
39 * `@alchemy.run/frontend-frameworks/vite/node` deploy target — the package
40 * must be installed in your project. During `alchemy dev` the site is
41 * Vite's own dev server and no cloud resources are created.
42 *
43 *
44 * ### Creating Vite Sites
45 * **Example:** Basic Vite SPA
46 * ```typescript
47 * const site = yield* Hetzner.Website.Vite("Web");
48 * ```
49 *
50 * **Example:** Project in a Subdirectory
51 * ```typescript
52 * const site = yield* Hetzner.Website.Vite("Web", {
53 * rootDir: "./app",
54 * });
55 * ```
56 *
57 * **Example:** Existing Server
58 * ```typescript
59 * const server = yield* Hetzner.Server("box", {
60 * serverType: "cpx12",
61 * image: "ubuntu-24.04",
62 * location: "fsn1",
63 * });
64 * const site = yield* Hetzner.Website.Vite("Web", { server });
65 * ```
66 *
67 * ### Multi-Page Sites
68 * **Example:** Per-Route HTML Pages with a 404 Page
69 * ```typescript
70 * const site = yield* Hetzner.Website.Vite("Docs", {
71 * assets: { notFoundHandling: "404-page" },
72 * });
73 * ```
74 *
75 * ### Custom Domain
76 * **Example:** A Record on an Existing Zone
77 * ```typescript
78 * const site = yield* Hetzner.Website.Vite("Web", {
79 * domain: "app.example.com",
80 * zone,
81 * });
82 * ```
83 *
84 * ### Build Configuration
85 * **Example:** Custom Output Directory and Base Path
86 * ```typescript
87 * const site = yield* Hetzner.Website.Vite("Docs", {
88 * outDir: "build",
89 * base: "/docs/",
90 * });
91 * ```
92 *
93 * ### Local Development
94 * **Example:** Vite Dev Server Under `alchemy dev`
95 * ```typescript
96 * // `alchemy dev` starts `vite` programmatically: site.url is the local
97 * // dev server (HMR included); no Server or Service is created.
98 * const site = yield* Hetzner.Website.Vite("Web");
99 * ```
100 *
101 * @resource
102 * @product Website
103 */
104export const Vite = (id: string, props: ViteProps = {}) =>
105 makeFrameworkSite(id, props, {
106 name: "Vite",
107 framework: VITE_FRAMEWORK_SPECIFIER,
108 target: VITE_NODE_TARGET_SPECIFIER,
109 options: {
110 ...viteOptions(props),
111 notFoundHandling: "spa",
112 htmlHandling: props.assets?.htmlHandling,
113 },
114 static: staticConfigFromAssets(props.assets, {
115 notFoundHandling: "single-page-application",
116 }),
117 }).pipe(Namespace.push(id));