Repository navigation
refactor(web): deploy the dashboard as a Cloudflare.Website.Vite worker #928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /** | ||
| * The deployed Worker's entry, as a plain module. | ||
| * | ||
| * `Cloudflare.Website.Vite` builds this through the Cloudflare Vite plugin and | ||
| * deploys it as the Worker, which is why it is a module rather than the Effect | ||
| * implementation the Worker class used to carry as its third argument: a vite | ||
| * source owns the entry, and that argument has nowhere to go. | ||
| * | ||
| * Nothing was lost in the move. The handler was already a plain async function | ||
| * in `./handler`, and the Effect wrapper around it only unwrapped the request, | ||
| * read three bindings off the environment and converted the response back. | ||
| */ | ||
| import { handleRequest } from "./handler" | ||
| import type { WebWorkerEnv } from "./worker-env" | ||
|
|
||
| export default { | ||
| fetch: (request: Request, env: WebWorkerEnv): Promise<Response> => handleRequest(request, env), | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,90 +1,104 @@ | ||
| /** | ||
| * The dashboard's Worker in alchemy's single-module form: this file is both | ||
| * the resource the root stack yields (`yield* Web`) and the bundle alchemy | ||
| * deploys (`main: import.meta.url`). The Vite build is yielded from the props, | ||
| * and every request goes to `./handler` — OG images, the share preview, the | ||
| * SPA shell fallback — which stays a plain function so its tests need no | ||
| * Worker runtime. | ||
| * The dashboard's Worker, deployed from its own Vite project. | ||
| * | ||
| * `Cloudflare.Website.Vite` owns the build: one `vite build` through the | ||
| * Cloudflare Vite plugin produces the client assets and the server bundle, and | ||
| * alchemy uploads both. It replaced a `Command.Build` that shelled out to | ||
| * `bun run build` and an `assets` block pointed at its `dist/`. | ||
| * | ||
| * The reason for the move is dev, not deploy. A vite source is served by | ||
| * alchemy's own vite child, so `bun dev` runs this Worker in workerd against a | ||
| * real vite dev server instead of spawning a bare `vite dev` beside the stack. | ||
| * That bare spawn inherited the alchemy CLI's `NODE_ENV=production`, which Vite | ||
| * reads for `import.meta.env.DEV`/`PROD` rather than taking from `--mode`, so | ||
| * the dev server served `MODE: "development"` next to `DEV: false, PROD: true`. | ||
| * Every dev-only branch was dead and every production branch live; `/lab` 404ing | ||
| * through `bun dev` was the visible half. Alchemy's vite child strips the | ||
| * variable and says why (`Cloudflare/Workers/ViteChild.ts`); it simply never ran | ||
| * for us, because we were not using a vite source. | ||
| * | ||
| * `VITE_*` keys in `env` are inlined into the client bundle as | ||
| * `import.meta.env.*` by the vite source, which is the same job the removed | ||
| * `Command.Build`'s env did through `vite.config.ts`'s `define`. Non-prefixed | ||
| * keys stay ordinary Worker bindings. | ||
| */ | ||
| import { ApiWorker, CLOUDFLARE_WORKER_PLACEMENT, MapleStack, resolveWorkerName } from "@maple/infra/cloudflare" | ||
| import { | ||
| ApiWorker, | ||
| CLOUDFLARE_WORKER_PLACEMENT, | ||
| MapleStack, | ||
| resolveWorkerName, | ||
| } from "@maple/infra/cloudflare" | ||
| import { plainFrom } from "@maple/infra/env" | ||
| import * as Cloudflare from "alchemy/Cloudflare" | ||
| import * as Command from "alchemy/Command" | ||
| import * as Output from "alchemy/Output" | ||
| import { Effect } from "effect" | ||
| import { HttpServerResponse } from "effect/unstable/http" | ||
| import { handleRequest } from "./handler" | ||
|
|
||
| /** | ||
| * Alchemy evaluates a Worker's props wherever the class is yielded — the | ||
| * deployed bundle included, where they are inert. `__ALCHEMY_RUNTIME__` folds to | ||
| * `true` there, so the stack-side branch below, and the `@maple/infra` and | ||
| * `alchemy/Command` modules only it reaches, are dead-code-eliminated. | ||
| */ | ||
| /** This app's own root, which is where its `vite.config.ts` lives. */ | ||
| const rootDir = new URL("..", import.meta.url).pathname | ||
|
|
||
| const props = Effect.gen(function* () { | ||
| if (globalThis.__ALCHEMY_RUNTIME__) return { main: import.meta.url } | ||
| const { stage, domains, urls } = yield* MapleStack | ||
| const api = yield* ApiWorker | ||
| // The build runs through `Command.Build` so the VITE_* env is part of the | ||
| // memo hash: a stage's URLs (or the commit) changing re-runs it with no | ||
| // source change. vite.config.ts turns these into `define` overrides. | ||
| const build = yield* Command.Build("web-build", { | ||
| command: "bun run build", | ||
| cwd: new URL("..", import.meta.url).pathname, | ||
| outdir: "dist", | ||
| env: { | ||
| VITE_API_BASE_URL: urls.api, | ||
| VITE_INGEST_URL: urls.ingest, | ||
| VITE_ELECTRIC_SYNC_URL: urls.electricSync, | ||
| VITE_MAPLE_AUTH_MODE: yield* plainFrom(["VITE_MAPLE_AUTH_MODE", "MAPLE_AUTH_MODE"], "self_hosted"), | ||
| VITE_CLERK_PUBLISHABLE_KEY: yield* plainFrom(["VITE_CLERK_PUBLISHABLE_KEY", "CLERK_PUBLISHABLE_KEY"], ""), | ||
| VITE_MAPLE_INGEST_KEY: yield* plainFrom(["VITE_MAPLE_INGEST_KEY", "MAPLE_OTEL_PUBLIC_INGEST_KEY"], ""), | ||
| // Stamped onto browser telemetry as `vcs.ref.head.revision` / `service.version`. | ||
| VITE_COMMIT_SHA: yield* plainFrom(["VITE_COMMIT_SHA", "COMMIT_SHA", "GITHUB_SHA"], ""), | ||
| }, | ||
| }) | ||
| return { | ||
| main: import.meta.url, | ||
| name: resolveWorkerName("web", stage), | ||
| rootDir, | ||
| // The deployed entry. A vite source owns the Worker entry, so the Effect | ||
| // implementation this class used to take as a third argument moved into | ||
| // this module; it was only unwrapping a request and three bindings. | ||
| main: "src/worker-entry.ts", | ||
| assets: { | ||
| directory: build.outdir, | ||
| hash: Output.map(build.hash, (h) => h.output ?? ""), | ||
| // Deep links must serve the shell at the requested URL: without this the | ||
| // binding 404s, the handler fetches /index.html, and the assets layer's | ||
| // trailing-slash normalization 307s that to "/" on every hard reload. | ||
| notFoundHandling: "single-page-application" as const, | ||
| }, | ||
| // The default memo scope hashes this app's own tree and the lockfile, which | ||
| // would not notice an edit to the `@maple/*` packages the bundle compiles | ||
| // in. `lockfile` is restated because providing `include` drops it. | ||
| memo: { | ||
| include: ["**/*", "../../packages/*/src/**", "../../lib/*/src/**"], | ||
| lockfile: true, | ||
| }, | ||
| placement: CLOUDFLARE_WORKER_PLACEMENT, | ||
| workersDev: true, | ||
| domain: domains.web, | ||
| // The share-preview lookups ride the service binding; the URL is still | ||
| // bound because bindings address requests by absolute URL. A dev stage | ||
| // without an api domain binds neither and previews degrade to the generic card. | ||
| env: { | ||
| // Bindings. The share-preview lookups ride the service binding; the URL is | ||
| // still bound because bindings address requests by absolute URL. A dev | ||
| // stage without an api domain binds neither and previews degrade to the | ||
| // generic card. | ||
| ...(urls.api === "" ? undefined : { MAPLE_API_BASE_URL: urls.api }), | ||
| API: api, | ||
| // Inlined into the client bundle as `import.meta.env.*`. These were the | ||
| // removed `Command.Build`'s env, and they remain build inputs: a stage's | ||
| // URLs or the commit changing rebuilds with no source change. | ||
| VITE_API_BASE_URL: urls.api, | ||
| VITE_INGEST_URL: urls.ingest, | ||
| VITE_ELECTRIC_SYNC_URL: urls.electricSync, | ||
| VITE_MAPLE_AUTH_MODE: yield* plainFrom( | ||
| ["VITE_MAPLE_AUTH_MODE", "MAPLE_AUTH_MODE"], | ||
| "self_hosted", | ||
| ), | ||
| VITE_CLERK_PUBLISHABLE_KEY: yield* plainFrom( | ||
| ["VITE_CLERK_PUBLISHABLE_KEY", "CLERK_PUBLISHABLE_KEY"], | ||
| "", | ||
| ), | ||
| VITE_MAPLE_INGEST_KEY: yield* plainFrom( | ||
| ["VITE_MAPLE_INGEST_KEY", "MAPLE_OTEL_PUBLIC_INGEST_KEY"], | ||
| "", | ||
| ), | ||
| // Stamped onto browser telemetry as `vcs.ref.head.revision` / `service.version`. | ||
| VITE_COMMIT_SHA: yield* plainFrom(["VITE_COMMIT_SHA", "COMMIT_SHA", "GITHUB_SHA"], ""), | ||
| }, | ||
| } | ||
| }) | ||
| }).pipe( | ||
| // `Website.Vite` takes props whose error channel is `never`, where | ||
| // `Cloudflare.Worker` tolerated one. The only failure in here is a | ||
| // `ConfigError` from `plainFrom`, and every one of those calls carries a | ||
| // default, so reaching the error channel at all means the stack cannot read | ||
| // its own configuration. There is nothing for a caller to do with that. | ||
| Effect.orDie, | ||
| ) | ||
|
|
||
| // The logical id stays `app`: it names the deployed resource, and a rename would | ||
| // plan a delete + create of the Worker behind `app.maple.dev`. | ||
| export default class Web extends Cloudflare.Worker<Web>()( | ||
| "app", | ||
| props, | ||
| Effect.succeed({ | ||
| fetch: Effect.gen(function* () { | ||
| const request = yield* Cloudflare.Workers.Request | ||
| const env = yield* Cloudflare.WorkerEnvironment | ||
| return HttpServerResponse.fromWeb( | ||
| yield* Effect.promise(() => | ||
| handleRequest(request, { | ||
| ASSETS: env.ASSETS, | ||
| API: env.API, | ||
| MAPLE_API_BASE_URL: env.MAPLE_API_BASE_URL, | ||
| }), | ||
| ), | ||
| ) | ||
| }), | ||
| }), | ||
| ) {} | ||
| export default class Web extends Cloudflare.Website.Vite<Web>()("app", props) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.