| 1 | /** |
| 2 | * Cloudflare dev transport — HOST half. |
| 3 | * |
| 4 | * Runs in the alchemy / e2e-harness process: opens `cloudflare-runtime`'s |
| 5 | * platform proxy (a local workerd instance hosting the configured binding |
| 6 | * hooks behind the internal proxy worker) and produces the |
| 7 | * {@link NuxtDevPlatform} the framework half injects into `loadNuxt` |
| 8 | * overrides — the dev-only nitro plugin (`./plugin.ts`) plus the |
| 9 | * `runtimeConfig` connect info. The proxy's entire client state is |
| 10 | * `connectInfo: { url, token }` — two plain strings — which is what lets |
| 11 | * the nitro dev SSR worker THREAD reconstruct the platform over HTTP |
| 12 | * (`platform-proxy/connect`) with live-shared binding state. |
| 13 | */ |
| 14 | import { DeployTargetError } from "../../core/index.ts"; |
| 15 | import * as Effect from "effect/Effect"; |
| 16 | import type * as Scope from "effect/Scope"; |
| 17 | import * as NodePath from "node:path"; |
| 18 | import { fileURLToPath } from "node:url"; |
| 19 | import type { NuxtDevPlatform, NuxtDevPlatformContext } from "../Nuxt.ts"; |
| 20 | import { RUNTIME_CONFIG_KEY, type DevConnectInfo } from "./shared.ts"; |
| 21 | |
| 22 | /** |
| 23 | * The runtime-free client subpath of `cloudflare-runtime` the dev plugin is |
| 24 | * built on. Resolved HOST-side (this package depends on |
| 25 | * `cloudflare-runtime`) to an absolute path the dev worker imports |
| 26 | * directly — the worker itself cannot resolve the bare specifier when |
| 27 | * `cloudflare-runtime` is only a transitive dependency of the project. |
| 28 | */ |
| 29 | export const CLIENT_MODULE_SPECIFIER = |
| 30 | "@alchemy.run/cloudflare-runtime/core/platform-proxy/connect"; |
| 31 | |
| 32 | /** Resolve the absolute path of the shipped dev-only nitro plugin. */ |
| 33 | export const resolveDevPluginPath = (): string => { |
| 34 | const packageJson = fileURLToPath( |
| 35 | import.meta.resolve("@alchemy.run/frontend-frameworks/package.json"), |
| 36 | ); |
| 37 | return NodePath.join( |
| 38 | NodePath.dirname(packageJson), |
| 39 | "dist", |
| 40 | "nuxt", |
| 41 | "dev", |
| 42 | "plugin.js", |
| 43 | ); |
| 44 | }; |
| 45 | |
| 46 | /** Resolve {@link CLIENT_MODULE_SPECIFIER} to an absolute file path. */ |
| 47 | export const resolveClientModulePath = (): string => |
| 48 | fileURLToPath(import.meta.resolve(CLIENT_MODULE_SPECIFIER)); |
| 49 | |
| 50 | /** What the dev platform needs back from an opened proxy. */ |
| 51 | export interface DevProxyHandle { |
| 52 | /** Base URL of the proxy workerd instance. */ |
| 53 | readonly url: string; |
| 54 | /** The instance's auth token. */ |
| 55 | readonly token: string; |
| 56 | /** Tear the proxy down. Safe to call multiple times. */ |
| 57 | readonly dispose: () => Promise<void>; |
| 58 | } |
| 59 | |
| 60 | export interface OpenDevProxyOptions { |
| 61 | readonly name: string; |
| 62 | readonly compatibilityDate?: string | undefined; |
| 63 | readonly compatibilityFlags?: ReadonlyArray<string> | undefined; |
| 64 | /** `cloudflare-runtime` binding hooks (opaque through the framework half). */ |
| 65 | readonly bindings: ReadonlyArray<unknown>; |
| 66 | /** |
| 67 | * Pre-built `Context<RuntimeServices>` to host the proxy in (opaque |
| 68 | * through the framework half). Enables remote()-lowered bindings, which |
| 69 | * the proxy's internal local-only layer cannot serve. |
| 70 | */ |
| 71 | readonly services?: unknown; |
| 72 | } |
| 73 | |
| 74 | /** How the platform proxy is opened. A test seam; the default is {@link openPlatformProxy}. */ |
| 75 | export type OpenDevProxy = ( |
| 76 | options: OpenDevProxyOptions, |
| 77 | ) => Promise<DevProxyHandle>; |
| 78 | |
| 79 | /** |
| 80 | * The default opener: `cloudflare-runtime`'s `getPlatformProxy` (imported |
| 81 | * lazily so production builds never load the runtime machinery). The |
| 82 | * instance's `connectInfo` carries everything the dev plugin needs. |
| 83 | */ |
| 84 | export const openPlatformProxy: OpenDevProxy = async (options) => { |
| 85 | const { getPlatformProxy } = |
| 86 | await import("@alchemy.run/cloudflare-runtime/core/platform-proxy"); |
| 87 | const proxy = await getPlatformProxy({ |
| 88 | name: options.name, |
| 89 | ...(options.compatibilityDate !== undefined |
| 90 | ? { compatibilityDate: options.compatibilityDate } |
| 91 | : undefined), |
| 92 | ...(options.compatibilityFlags !== undefined |
| 93 | ? { compatibilityFlags: [...options.compatibilityFlags] } |
| 94 | : undefined), |
| 95 | bindings: options.bindings as Parameters< |
| 96 | typeof getPlatformProxy |
| 97 | >[0]["bindings"], |
| 98 | ...(options.services !== undefined |
| 99 | ? { |
| 100 | services: options.services as Parameters< |
| 101 | typeof getPlatformProxy |
| 102 | >[0]["services"], |
| 103 | } |
| 104 | : undefined), |
| 105 | }); |
| 106 | return { |
| 107 | url: proxy.connectInfo.url, |
| 108 | token: proxy.connectInfo.token, |
| 109 | dispose: proxy.dispose, |
| 110 | }; |
| 111 | }; |
| 112 | |
| 113 | export interface CloudflareDevPlatformOptions { |
| 114 | /** Name of the proxy workerd service. @default "nuxt-dev-platform-proxy" */ |
| 115 | readonly name?: string | undefined; |
| 116 | /** Compatibility date for the binding-proxy workerd instance. */ |
| 117 | readonly compatibilityDate?: string | undefined; |
| 118 | /** Compatibility flags for the binding-proxy workerd instance. */ |
| 119 | readonly compatibilityFlags?: ReadonlyArray<string> | undefined; |
| 120 | /** Override how the proxy is opened. A test seam. */ |
| 121 | readonly openProxy?: OpenDevProxy | undefined; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Literal env values for the plugin: strings pass through; everything else |
| 126 | * (resource bindings) is delivered through the proxy itself. |
| 127 | */ |
| 128 | const literalEnv = ( |
| 129 | env: Record<string, unknown> | undefined, |
| 130 | ): Record<string, string> | undefined => { |
| 131 | const out: Record<string, string> = {}; |
| 132 | for (const [key, value] of Object.entries(env ?? {})) { |
| 133 | if (typeof value === "string") { |
| 134 | out[key] = value; |
| 135 | } |
| 136 | } |
| 137 | return Object.keys(out).length > 0 ? out : undefined; |
| 138 | }; |
| 139 | |
| 140 | /** |
| 141 | * The Cloudflare target's dev platform: open the proxy (scoped — closing |
| 142 | * the Scope disposes the workerd instance) and hand the framework half the |
| 143 | * nitro plugin + `runtimeConfig` connect info to inject. |
| 144 | */ |
| 145 | export const makeCloudflareDevPlatform = |
| 146 | (options: CloudflareDevPlatformOptions = {}) => |
| 147 | ( |
| 148 | context: NuxtDevPlatformContext, |
| 149 | ): Effect.Effect<NuxtDevPlatform, DeployTargetError, Scope.Scope> => |
| 150 | Effect.gen(function* () { |
| 151 | const open = options.openProxy ?? openPlatformProxy; |
| 152 | const proxy = yield* Effect.acquireRelease( |
| 153 | Effect.tryPromise({ |
| 154 | try: () => |
| 155 | open({ |
| 156 | name: options.name ?? "nuxt-dev-platform-proxy", |
| 157 | compatibilityDate: options.compatibilityDate, |
| 158 | compatibilityFlags: options.compatibilityFlags, |
| 159 | bindings: context.bindings ?? [], |
| 160 | services: context.services, |
| 161 | }), |
| 162 | catch: (cause) => |
| 163 | new DeployTargetError({ |
| 164 | platform: "cloudflare", |
| 165 | message: "Failed to open the dev platform proxy", |
| 166 | cause, |
| 167 | }), |
| 168 | }), |
| 169 | (handle) => Effect.promise(() => handle.dispose().catch(() => {})), |
| 170 | ); |
| 171 | const clientModule = yield* Effect.try({ |
| 172 | try: resolveClientModulePath, |
| 173 | catch: (cause) => |
| 174 | new DeployTargetError({ |
| 175 | platform: "cloudflare", |
| 176 | message: |
| 177 | `Failed to resolve "${CLIENT_MODULE_SPECIFIER}" — the Nuxt dev bridge ` + |
| 178 | "requires @alchemy.run/cloudflare-runtime/core (a dependency of this package)", |
| 179 | cause, |
| 180 | }), |
| 181 | }); |
| 182 | const env = literalEnv(context.env); |
| 183 | const connectInfo: DevConnectInfo = { |
| 184 | url: proxy.url, |
| 185 | token: proxy.token, |
| 186 | clientModule, |
| 187 | ...(env !== undefined ? { env } : {}), |
| 188 | }; |
| 189 | return { |
| 190 | nitroPlugins: [resolveDevPluginPath()], |
| 191 | runtimeConfig: { [RUNTIME_CONFIG_KEY]: connectInfo }, |
| 192 | } satisfies NuxtDevPlatform; |
| 193 | }); |