| 1 | import type { InputProps } from "../../Input.ts"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import { AlchemyContext } from "../../AlchemyContext.ts"; |
| 4 | import * as Namespace from "../../Namespace.ts"; |
| 5 | import * as Output from "../../Output.ts"; |
| 6 | import { ProviderModePolicy } from "../../ProviderMode.ts"; |
| 7 | import type { PolicyStatement } from "../IAM/Policy.ts"; |
| 8 | import { Bucket } from "../S3/Bucket.ts"; |
| 9 | import { 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 | */ |
| 15 | export const VINEXT_AWS_FRAMEWORK_SPECIFIER = |
| 16 | "@alchemy.run/frontend-frameworks/vinext/aws"; |
| 17 | |
| 18 | /** The AWS Lambda deploy target for the vinext build. */ |
| 19 | export 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. */ |
| 23 | export const VINEXT_CACHE_BUCKET_ENV = "CACHE_BUCKET_NAME"; |
| 24 | |
| 25 | export 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 | */ |
| 70 | export 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)); |