1import type { InputProps } from "../../Input.ts";
2import * as Effect from "effect/Effect";
3import { AlchemyContext } from "../../AlchemyContext.ts";
4import * as Namespace from "../../Namespace.ts";
5import * as Output from "../../Output.ts";
6import { ProviderModePolicy } from "../../ProviderMode.ts";
7import type { PolicyStatement } from "../IAM/Policy.ts";
8import { Bucket } from "../S3/Bucket.ts";
9import { makeFrameworkSite, type FrameworkSiteProps } from "./FrameworkSite.ts";
10
11/**
12 * The vinext-on-AWS framework module (it is its own deploy target — not
13 * OpenNext, not the Cloudflare Worker source).
14 */
15export const VINEXT_AWS_FRAMEWORK_SPECIFIER =
16 "@alchemy.run/frontend-frameworks/vinext/aws";
17
18/** The AWS Lambda deploy target for the vinext build. */
19export const VINEXT_AWS_TARGET_SPECIFIER =
20 "@alchemy.run/frontend-frameworks/vinext/aws";
21
22/** Env var the S3 data-cache adapter reads for the ISR/fetch cache bucket. */
23export const VINEXT_CACHE_BUCKET_ENV = "CACHE_BUCKET_NAME";
24
25export interface VinextProps extends FrameworkSiteProps {}
26
27/**
28 * Deploy a [vinext](https://vinext.dev) App Router application to AWS: the RSC
29 * server on a streaming Lambda Function URL, static assets in S3, and a
30 * CloudFront distribution whose edge router serves uploaded files from S3
31 * and forwards everything else to the server.
32 *
33 * ISR / `"use cache"` persistence is an S3 bucket (not Cloudflare KV).
34 * This resource provisions the bucket, sets `CACHE_BUCKET_NAME`, and
35 * grants the Lambda Get/Put/Delete/List. Alchemy injects the S3 data-cache
36 * adapter during the build; no Alchemy plugin is needed in `vite.config.ts`.
37 * A dedicated cache bucket keeps the graph acyclic (the asset bucket
38 * is bound to CloudFront).
39 *
40 * Requires vinext 1.0.0-beta.10 or newer. The Lambda build uses vinext's
41 * standalone packaging to include external runtime dependencies. Pages Router
42 * applications require a Node deployment target instead of Lambda.
43 *
44 * This is **not** `AWS.Website.Nextjs` (OpenNext) and **not**
45 * `Cloudflare.Website.Vinext`. During `alchemy dev` the site is
46 * `vinext dev` and no cloud resources are declared.
47 *
48 * ### Creating vinext Sites
49 * **Example:** Basic vinext App
50 * ```typescript
51 * const site = yield* AWS.Website.Vinext("Web", {
52 * rootDir: "./app",
53 * });
54 * ```
55 *
56 * **Example:** vite.config.ts (platform-agnostic)
57 * ```typescript
58 * import { defineConfig } from "vite";
59 * import vinext from "vinext";
60 *
61 * export default defineConfig({
62 * plugins: [vinext({ prerender: true })],
63 * });
64 * ```
65 *
66 * @resource
67 * @product Website
68 * @category Frontend
69 */
70export const Vinext = (id: string, propsIn: InputProps<VinextProps> = {}) =>
71 Effect.gen(function* () {
72 const props = propsIn as VinextProps;
73 const ctx = yield* AlchemyContext;
74 const remoted = yield* ProviderModePolicy;
75 const isLocal = ctx.dev && remoted !== true;
76
77 if (isLocal) {
78 return {
79 ...(yield* makeFrameworkSite(id, props, {
80 name: "Vinext",
81 framework: VINEXT_AWS_FRAMEWORK_SPECIFIER,
82 target: VINEXT_AWS_TARGET_SPECIFIER,
83 })),
84 cacheBucket: undefined,
85 };
86 }
87
88 const cacheBucket = yield* Bucket("CacheBucket", {
89 forceDestroy: props.forceDestroy,
90 tags: props.tags,
91 });
92 const site = yield* makeFrameworkSite(
93 id,
94 {
95 ...props,
96 env: {
97 [VINEXT_CACHE_BUCKET_ENV]: cacheBucket.bucketName,
98 ...props.env,
99 },
100 },
101 {
102 name: "Vinext",
103 framework: VINEXT_AWS_FRAMEWORK_SPECIFIER,
104 target: VINEXT_AWS_TARGET_SPECIFIER,
105 },
106 );
107 if (site.server !== undefined) {
108 yield* site.server
109 .bind`Allow(${site.server}, AWS.Website.Vinext.Cache(${cacheBucket}))`({
110 policyStatements: [
111 {
112 Effect: "Allow",
113 Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
114 Resource: [Output.interpolate`${cacheBucket.bucketArn}/*` as never],
115 },
116 {
117 Effect: "Allow",
118 Action: ["s3:ListBucket"],
119 Resource: [cacheBucket.bucketArn as never],
120 },
121 ] satisfies PolicyStatement[],
122 });
123 }
124 return { ...site, cacheBucket };
125 }).pipe(Namespace.push(id));