From d25d67297bb07aa33d6c28cb3ee5aa0e2af32946 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 02:05:11 -0700 Subject: [PATCH 01/29] switch to component-hosted http --- CHANGELOG.md | 22 ++ INTEGRATION.md | 376 +++++++++---------- README.md | 256 +++++-------- dist/cli/setup.d.ts | 2 +- dist/cli/setup.js | 116 ++---- example/convex/_generated/api.d.ts | 4 +- example/convex/convex.config.ts | 6 +- example/convex/http.ts | 29 -- example/convex/staticHosting.test.ts | 35 -- example/convex/staticHosting.ts | 19 +- example/dist/assets/index-BGY4sn62.js | 10 - example/dist/index.html | 2 +- example/src/App.tsx | 9 +- src/cli/init.ts | 181 --------- src/cli/setup.ts | 144 ++----- src/cli/upload.ts | 70 ++-- src/client/index.test.ts | 67 ---- src/client/index.ts | 522 +------------------------- src/component/_generated/api.ts | 2 + src/component/_generated/component.ts | 64 ---- src/component/convex.config.ts | 2 +- src/component/http.ts | 230 ++++++++++++ src/component/lib.test.ts | 98 ++--- src/component/lib.ts | 245 +++++------- src/react/index.tsx | 139 ++++--- src/test.ts | 2 +- 26 files changed, 873 insertions(+), 1779 deletions(-) delete mode 100644 example/convex/http.ts delete mode 100644 example/convex/staticHosting.test.ts delete mode 100644 example/dist/assets/index-BGY4sn62.js delete mode 100644 src/cli/init.ts delete mode 100644 src/client/index.test.ts create mode 100644 src/component/http.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ec03a..64c8127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.2.0 (Unreleased) + +Component-owned HTTP and storage. **Breaking — redeploy your static assets after +upgrading.** + +- The component now hosts its own HTTP endpoints and owns the file storage that + serves them. Wire it up with `app.use(staticHosting, { httpPrefix: "/" })` and + delete `convex/http.ts` + the upload-API re-exports from + `convex/staticHosting.ts`. +- Removed `registerStaticRoutes` and `exposeUploadApi` from the client API. + `exposeDeploymentQuery` and `getConvexUrl` remain if you use the UpdateBanner. +- The component is now named `staticHosting` (previously `selfHosting`). The + CLI invokes it directly via `npx convex run --component staticHosting + lib:...`. If you mount the component under a different name, pass + `--component `. +- `useDeploymentUpdates` / `UpdateBanner` use `useQuery_experimental` and + default to `api.staticHosting.getCurrentDeployment`. If you don't surface + deployment updates, you no longer need to expose anything. +- Assets uploaded under 0.1.x lived in the app's storage — those references + won't resolve in 0.2.x. Run `npx @convex-dev/static-hosting deploy` to + repopulate. + ## 0.1.4 - Added support for Windows diff --git a/INTEGRATION.md b/INTEGRATION.md index 8b48d48..ccb588d 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -1,79 +1,48 @@ # Integration Guide: @convex-dev/static-hosting -A Convex component that enables hosting static React/Vite apps using Convex HTTP actions and file storage. No external hosting provider required. +A Convex component that hosts static React/Vite apps directly on Convex — +serving from the component's own HTTP endpoints and storage. No external +hosting, and minimal app-side wiring. ## Quick Start -### Step 1: Install ```bash npm install @convex-dev/static-hosting +npx @convex-dev/static-hosting setup ``` -### Step 2: Setup (Choose One) +The setup script creates `convex/convex.config.ts` and adds a `deploy` script +to `package.json`. Then: -#### Option A: Automated Setup (Recommended) ```bash -npx @convex-dev/static-hosting setup +npm run deploy ``` -Interactive wizard that creates all necessary files. - -#### Option B: Manual Setup -See Manual Setup section below. ## Manual Setup -### Required Files +### `convex/convex.config.ts` + +This is the only file you need to touch in your app for the basic case. -#### 1. convex/convex.config.ts ```typescript import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config"; +import staticHosting from "@convex-dev/static-hosting/convex.config"; const app = defineApp(); -app.use(selfHosting); +app.use(staticHosting, { httpPrefix: "/" }); export default app; ``` -#### 2. convex/staticHosting.ts -```typescript -import { components } from "./_generated/api"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; - -// Internal functions for secure uploads (CLI only) -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -// Public query for live reload notifications -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); -``` - -#### 3. convex/http.ts -```typescript -import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Serve static files at root with SPA fallback -registerStaticRoutes(http, components.selfHosting); +`httpPrefix: "/"` mounts the component at the deployment root. If your app +needs to serve its own HTTP routes at the root, mount the component at a +sub-path like `httpPrefix: "/app/"` and set your bundler's base path to match +(e.g. `base: "/app/"` in `vite.config.ts`). -// Or serve at a path prefix (recommended if you have API routes): -// registerStaticRoutes(http, components.selfHosting, { -// pathPrefix: "/app", -// spaFallback: true, -// }); +> Run `npx convex dev` after editing `convex.config.ts` so codegen picks up the +> component. -export default http; -``` - -#### 4. package.json Deploy Script -Add a deploy script for easy deployments: +### `package.json` ```json { @@ -83,122 +52,131 @@ Add a deploy script for easy deployments: } ``` -## Common Commands +That's all that's required. + +## Deployment ```bash -# Interactive setup wizard -npx @convex-dev/static-hosting setup +# Login (first time) +npx convex login -# One-shot deployment (backend + static files) +# One-shot: build + deploy backend + upload static files npx @convex-dev/static-hosting deploy +``` -# Upload static files only (after building) -npx @convex-dev/static-hosting upload --build --prod +Or two-step: -# Traditional two-step deployment -npx convex deploy # Deploy backend -npx @convex-dev/static-hosting upload --build --prod # Deploy static files +```bash +npx convex deploy +npx @convex-dev/static-hosting upload --build --prod ``` -## Deployment Workflow +Your app is live at `https://.convex.site`. -### First Time Setup -```bash -# 1. Install -npm install @convex-dev/static-hosting +The CLI authenticates with `convex run --component staticHosting lib:...` to +talk to the component directly — your app does not need to expose +`generateUploadUrl`, `recordAsset`, or any other upload functions. -# 2. Run setup wizard -npx @convex-dev/static-hosting setup +## Live reload banner (optional) -# 3. Initialize Convex (if not already done) -npx convex dev --once +If you want to prompt users to reload when a new deployment ships: -# 4. Deploy everything -npm run deploy +### `convex/staticHosting.ts` + +```typescript +import { exposeDeploymentQuery } from "@convex-dev/static-hosting"; +import { components } from "./_generated/api"; + +export const { getCurrentDeployment } = exposeDeploymentQuery( + components.staticHosting, +); ``` -### Subsequent Deployments -```bash -npm run deploy # That's it! +### Frontend + +```tsx +import { UpdateBanner } from "@convex-dev/static-hosting/react"; + +function App() { + return ( + <> + + {/* rest of app */} + + ); +} +``` + +`UpdateBanner` resolves `api.staticHosting.getCurrentDeployment` by default. +For a different module name, pass the reference explicitly: + +```tsx +import { api } from "../convex/_generated/api"; + ``` -## CDN Mode (Optional) +If `UpdateBanner` is used without exposing the query, it logs a setup hint to +the console and stays hidden. -By default, all static files are stored in Convex storage and served via HTTP actions. With CDN mode, non-HTML assets (JS, CSS, images, fonts) are served from a CDN edge network via [convex-fs](https://convexfs.dev) (backed by Bunny.net), while HTML files continue to be served from Convex (needed for SPA routing). +For custom UI, use the hook: -This gives better performance for static assets and lower Convex bandwidth usage. +```tsx +import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; -### How it works +const { updateAvailable, reload, dismiss } = useDeploymentUpdates(); +``` + +## CDN mode (optional) -1. Browser requests `/assets/main-abc123.js` -2. Convex HTTP action sees the asset has a `blobId` (CDN asset) -3. Returns **302 redirect** to the convex-fs blob endpoint -4. convex-fs returns **302 redirect** to signed CDN URL -5. Browser fetches from CDN edge (and caches for hashed assets) +By default, every file is served from the component's HTTP handler reading the +component's storage. Non-HTML assets can be redirected to a CDN edge network +via [convex-fs](https://convexfs.dev) for lower bandwidth and better +edge-cache performance. -HTML requests (`index.html`) are always served directly from Convex storage. +The component already issues a 302 redirect to `${origin}/fs/blobs/` +when an asset has a `blobId` — those endpoints are served by `convex-fs` at the +deployment root. -### CDN Setup +### 1. Install convex-fs -#### 1. Install convex-fs ```bash npm install convex-fs ``` -#### 2. convex/convex.config.ts +### 2. `convex/convex.config.ts` + ```typescript import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config"; +import staticHosting from "@convex-dev/static-hosting/convex.config"; import fs from "convex-fs/convex.config"; const app = defineApp(); -app.use(selfHosting); +app.use(staticHosting, { httpPrefix: "/" }); app.use(fs); export default app; ``` -#### 3. convex/http.ts -```typescript -import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { registerRoutes } from "convex-fs"; -import { components } from "./_generated/api"; +### 3. Deploy with `--cdn` -const http = httpRouter(); +```bash +npx @convex-dev/static-hosting deploy --cdn +``` -// Register convex-fs routes (for CDN blob serving) -registerRoutes(http, components.fs, { - pathPrefix: "/fs", - downloadAuth: async () => true, -}); +### CDN garbage collection (optional) -// Register static file serving with CDN redirect -registerStaticRoutes(http, components.selfHosting, { - cdnBaseUrl: (req) => `${new URL(req.url).origin}/fs/blobs`, -}); +Old CDN blobs aren't auto-deleted (the component can't reach the +deployment-root `/fs/blobs/` endpoint with auth). Expose a delete function +in your app and pass it to the CLI: -export default http; -``` +`convex/cdn.ts`: -#### 4. convex/staticHosting.ts ```typescript -import { components } from "./_generated/api"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; import { internalAction } from "./_generated/server"; import { v } from "convex/values"; import { del } from "convex-fs"; +import { components } from "./_generated/api"; -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); - -// Thin action wrapper to delete old CDN blobs during garbage collection export const deleteCdnBlobs = internalAction({ args: { blobIds: v.array(v.string()) }, returns: v.null(), @@ -211,129 +189,115 @@ export const deleteCdnBlobs = internalAction({ }); ``` -#### 5. Deploy with --cdn flag -```bash -# One-shot deployment with CDN -npx @convex-dev/static-hosting deploy --cdn - -# Or upload only with CDN -npx @convex-dev/static-hosting upload --cdn --prod -``` +Then deploy with: -### CDN Deploy Script -```json -{ - "scripts": { - "deploy": "npx @convex-dev/static-hosting deploy --cdn" - } -} +```bash +npx @convex-dev/static-hosting deploy --cdn \ + --cdn-delete-function cdn:deleteCdnBlobs ``` -## Live Reload Feature (Optional) - -Add a banner that notifies users when a new deployment is available: +## Connecting to Convex from the static frontend -```typescript -// In your src/App.tsx or main component -import { UpdateBanner } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; - -function App() { - return ( -
- - {/* Rest of your app */} -
- ); -} -``` +When served from `*.convex.site`, derive the matching backend URL: -Or use the hook for custom UI: ```typescript -import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; +import { getConvexUrl } from "@convex-dev/static-hosting"; -const { updateAvailable, reload, dismiss } = useDeploymentUpdates( - api.staticHosting.getCurrentDeployment -); +const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); ``` ## Security -Upload functions are **internal** - they can only be called via: +Upload functions are **internal** to the Component.They can only be called via: - `npx convex run` (requires Convex CLI authentication) - Other Convex functions (server-side only) This means unauthorized users cannot upload files, even if they know your Convex URL. +## CLI Reference + +```bash +npx @convex-dev/static-hosting setup + # Creates convex/convex.config.ts and adds a deploy script. + +npx @convex-dev/static-hosting deploy [options] + -d, --dist Path to dist directory (default: ./dist) + -c, --component Component instance name (default: staticHosting) + --skip-build Skip the build step + --skip-convex Skip Convex backend deployment + --cdn Upload non-HTML assets to convex-fs CDN + +npx @convex-dev/static-hosting upload [options] + -d, --dist Path to dist directory (default: ./dist) + -c, --component Component instance name (default: staticHosting) + --prod Deploy to production deployment + -b, --build Run 'npm run build' with VITE_CONVEX_URL set + --cdn Upload non-HTML assets to convex-fs CDN + --cdn-delete-function App function path that deletes CDN blobs + -j, --concurrency Parallel upload workers (default: 5) +``` + +## Important Notes + +1. **Storage lives in the component.** Uploaded files are stored in the + component's `_storage` table, not your app's. If you mount multiple + instances of the component, each has its own storage. +2. Upload functions are **internal** — only reachable via the authenticated + `npx convex run --component` CLI flow. +3. Hashed assets get `immutable, max-age=31536000`; HTML uses ETag + revalidation. +4. Paths without a file extension fall back to `/index.html` (SPA mode). +5. Always pass `--build` to the upload CLI so `VITE_CONVEX_URL` matches the + target deployment. + ## Troubleshooting -### Files not updating after deployment -- Clear browser cache or use incognito mode +### 404s on every path + +Make sure `convex.config.ts` mounts the component and you've run `npx convex +dev` (or `npx convex deploy`) since adding it. The component's own +`http.ts` is auto-detected during codegen. + +### Wrong `VITE_CONVEX_URL` in the built bundle -### Build fails with wrong VITE_CONVEX_URL -Always use the `--build` flag when deploying: ```bash -# ✅ Correct - CLI sets VITE_CONVEX_URL for target environment +# Right — CLI sets VITE_CONVEX_URL for the target deployment npx @convex-dev/static-hosting deploy -# ❌ Wrong - uses dev URL from .env.local +# Wrong — uses VITE_CONVEX_URL from .env.local npm run build && npx @convex-dev/static-hosting upload --prod ``` -### "Cannot find module convex.config" -Make sure you've installed the package and it's listed in `package.json`: -```bash -npm install @convex-dev/static-hosting -``` +### Component name mismatch -### HTTP routes not working (404s) -- You must create `convex/http.ts` and register routes -- Run `npx convex dev` to regenerate types after adding http.ts +If you've renamed the component instance (`app.use(staticHosting, { name: +"custom" })`), pass it to the CLI: -### `--component` is the module name, not the component name -Despite its name, `--component ` refers to the **Convex module** where you exposed the upload API (i.e. `convex/.ts`), not the component name registered in `convex.config.ts`. If you put the `exposeUploadApi(...)` re-exports in `convex/myCustomName.ts`, pass: ```bash -npx @convex-dev/static-hosting upload --component myCustomName +npx @convex-dev/static-hosting upload --component custom ``` -Default is `staticHosting` (i.e. `convex/staticHosting.ts`). - -## API Reference - -### registerStaticRoutes(http, component, options?) -Registers HTTP routes for serving static files. -**Options**: -- `pathPrefix` (string): URL prefix for static files (default: "/") -- `spaFallback` (boolean): Enable SPA fallback to index.html (default: true) -- `cdnBaseUrl` (string | (request: Request) => string): Base URL for CDN blob redirects. When set, non-HTML assets with a `blobId` return a 302 redirect to `{cdnBaseUrl}/{blobId}`. Example: `(req) => \`${new URL(req.url).origin}/fs/blobs\`` +### Mounting under a sub-path -### exposeUploadApi(component) -Exposes internal functions for CLI-based uploads. +If you set `httpPrefix: "/app/"`, also set `base: "/app/"` in +`vite.config.ts` (or your bundler's equivalent) so emitted assets reference +the right URLs. -**Returns**: `{ generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets }` - -### exposeDeploymentQuery(component) -Exposes a query for live reload notifications. +## API Reference -**Returns**: `{ getCurrentDeployment }` +### `exposeDeploymentQuery(component)` -### getConvexUrl() -Browser-only function to derive Convex URL from `.convex.site` hostname. +Returns `{ getCurrentDeployment }` — a public query that wraps the +component's deployment singleton. Add this to your app only if you use +`` or `useDeploymentUpdates`. -**Usage**: -```typescript -import { getConvexUrl } from "@convex-dev/static-hosting"; +### `getConvexUrl()` -const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); -``` +Browser-only. Returns `https://.convex.cloud` when the page is +served from `.convex.site`. ## Additional Resources -- [README.md](./README.md) - Full documentation with advanced features -- [Example app](./example) - Working example implementation -- [Component source](./src/component) - Component internals +- [README.md](./README.md) — Full documentation +- [`example/`](./example) — Working example app +- [Component source](./src/component) diff --git a/README.md b/README.md index 1fb9a23..4d2a8a5 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,9 @@ npm install @convex-dev/static-hosting npx @convex-dev/static-hosting setup ``` -The interactive wizard will: -1. Create necessary Convex files -2. Add deploy script to package.json +The setup command creates `convex/convex.config.ts` (or shows you what to add) +and registers a `deploy` script. Then: -Then deploy: ```bash npm run deploy ``` @@ -58,93 +56,39 @@ https://github.com/user-attachments/assets/5eaf781f-87da-4292-9f96-38070c86cd39 npm install @convex-dev/static-hosting ``` -### 2. Add to your `convex/convex.config.ts`: +### 2. Wire up the component + +`convex/convex.config.ts`: ```ts import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config.js"; +import staticHosting from "@convex-dev/static-hosting/convex.config.js"; const app = defineApp(); -app.use(selfHosting); +app.use(staticHosting, { httpPrefix: "/" }); export default app; ``` -### 3. Register HTTP routes - -Create or update `convex/http.ts` to serve static files: - -```ts -import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Serve static files at the root path with SPA fallback -registerStaticRoutes(http, components.selfHosting); - -export default http; -``` - -### 4. Expose upload API (internal functions) - -Create a file like `convex/staticHosting.ts`: - -```ts -import { exposeUploadApi } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -// These are INTERNAL functions - only callable via `npx convex run` -// NOT accessible from the public internet -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); -``` +That's it for required wiring. The component owns its own HTTP routes and file +storage — you don't register routes, expose functions, or re-export an upload +API from your app. -**Note:** Run `npx convex dev` at least once after setup to push your schema and -enable HTTP actions. If you see the error "This Convex deployment does not have -HTTP actions enabled", it means the Convex backend hasn't been deployed yet. +> `httpPrefix: "/"` mounts the static site at the deployment root. If your app +> already serves its own HTTP routes there, either change those to a sub-path +> (e.g. `/api/...`) or mount the component at a sub-path instead (e.g. +> `httpPrefix: "/app/"`). -### 5. Add deploy script to package.json +### 3. Add a deploy script ```json { "scripts": { - "build": "vite build", - "deploy:static": "npx @convex-dev/static-hosting upload --build --prod" + "deploy": "npx @convex-dev/static-hosting deploy" } } ``` -**Important:** Use `--build` to ensure `VITE_CONVEX_URL` is set correctly for -production. Don't run `npm run build` separately before the upload command, as -that would use the dev URL from `.env.local`. - -**CLI Options:** - -```bash -npx @convex-dev/static-hosting upload [options] - -Options: - -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. - convex/.ts (default: staticHosting) - --prod Deploy to production Convex deployment - --dev Deploy to dev deployment (default) - -b, --build Run 'npm run build' with correct VITE_CONVEX_URL - -h, --help Show help -``` - -**Examples:** - -```bash -# Deploy to production with automatic build -npx @convex-dev/static-hosting upload --build --prod - -# Deploy to dev (for testing) -npx @convex-dev/static-hosting upload --build -``` - ### Using Non-Vite Bundlers The CLI's `--build` flag sets `VITE_CONVEX_URL` when running your build command. @@ -191,135 +135,133 @@ npx @convex-dev/static-hosting deploy ``` The `deploy` command: -1. Builds frontend with production `VITE_CONVEX_URL` -2. Deploys Convex backend (`npx convex deploy`) -3. Deploys static files to Convex storage -This minimizes the inconsistency window between backend and frontend updates. +1. Builds your frontend with the production `VITE_CONVEX_URL`. +2. Deploys the Convex backend. +3. Uploads `dist/` to the component's storage. -**Deploy command options:** +### Manual Two-Step Deployment ```bash -npx @convex-dev/static-hosting deploy [options] - -Options: - -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. - convex/.ts (default: staticHosting) - --skip-build Skip the build step (use existing dist) - --skip-convex Skip Convex backend deployment - -h, --help Show help -``` - -Add to `package.json` for easy deployments: +npx convex deploy +npx @convex-dev/static-hosting upload --build --prod -```json -{ - "scripts": { - "deploy": "npx @convex-dev/static-hosting deploy" - } -} -``` -### Manual Two-Step Deployment +Your app is live at `https://.convex.site`. -If you prefer more control, deploy separately: +### CLI options ```bash -# Deploy Convex backend -npx convex deploy +npx @convex-dev/static-hosting deploy [options] + -d, --dist Path to dist directory (default: ./dist) + -c, --component Component instance name (default: staticHosting) + --skip-build Skip the build step (use existing dist) + --skip-convex Skip Convex backend deployment + --cdn Upload non-HTML assets to convex-fs CDN -# Deploy static files -npx @convex-dev/static-hosting upload --build --prod +npx @convex-dev/static-hosting upload [options] + -d, --dist Path to dist directory (default: ./dist) + -c, --component Component instance name (default: staticHosting) + --prod Deploy to production deployment + -b, --build Run 'npm run build' with correct VITE_CONVEX_URL + --cdn Upload non-HTML assets to convex-fs CDN + --cdn-delete-function App function path that deletes CDN blobs (opt-in) + -j, --concurrency Parallel upload workers (default: 5) ``` -Your app is now live at `https://your-deployment.convex.site` +The CLI runs against the component directly (`npx convex run --component +staticHosting lib:...`) — your app does not need to export `generateUploadUrl`, +`recordAsset`, etc. If you mount the component under a different name, pass +`--component `. ## Security -The upload API uses **internal functions** that can only be called via: +The upload API uses **internal functions** in the Component that can only be called via: - `npx convex run` (requires Convex CLI authentication) -- Other Convex functions (server-side only) +- Other Convex functions in the Component (server-side only) This means unauthorized users **cannot** upload files to your site, even if they know your Convex URL. -## Live Reload on Deploy +## Live Reload on Deploy (optional) -Connected clients can be notified when a new deployment is available: +If you want a banner that prompts users to reload when a new deployment ships, +expose the deployment query in your app and drop in ``: -1. **Expose the deployment query**: +`convex/staticHosting.ts`: - ```ts - import { exposeDeploymentQuery } from "@convex-dev/static-hosting"; - import { components } from "./_generated/api"; +```ts +import { exposeDeploymentQuery } from "@convex-dev/static-hosting"; +import { components } from "./_generated/api"; - export const { getCurrentDeployment } = exposeDeploymentQuery( - components.selfHosting, - ); - ``` +export const { getCurrentDeployment } = exposeDeploymentQuery( + components.staticHosting, +); +``` -2. **Add the update banner to your app**: +`src/App.tsx`: - ```tsx - import { UpdateBanner } from "@convex-dev/static-hosting/react"; - import { api } from "../convex/_generated/api"; +```tsx +import { UpdateBanner } from "@convex-dev/static-hosting/react"; + +function App() { + return ( + <> + + {/* ... */} + + ); +} +``` - function App() { - return ( -
- - {/* rest of your app */} -
- ); - } - ``` +`UpdateBanner` resolves `api.staticHosting.getCurrentDeployment` by default. If +you re-export the query under a different module name, pass it explicitly: -Or use the hook for custom UI: +```tsx +import { api } from "../convex/_generated/api"; + +``` + +For custom UI, use the hook: ```tsx import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; -const { updateAvailable, reload, dismiss } = useDeploymentUpdates( - api.staticHosting.getCurrentDeployment, -); +const { updateAvailable, reload, dismiss } = useDeploymentUpdates(); ``` -## Configuration Options +If `UpdateBanner` is used without exposing the query, a setup warning is logged +to the console and the banner stays hidden. -### `registerStaticRoutes` +## Connecting to Convex from the static frontend + +When your frontend is served from `*.convex.site`, you can derive the matching +backend URL without an env var: ```ts -registerStaticRoutes(http, components.selfHosting, { - // URL prefix for static files (default: "/") - pathPrefix: "/app", +import { getConvexUrl } from "@convex-dev/static-hosting"; - // Enable SPA fallback to index.html (default: true) - spaFallback: true, -}); +const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); ``` -## How It Works +## How it works + +The static-hosting component owns both the HTTP handler and the file storage: + +1. **Build phase** — your bundler produces `dist/`. +2. **Upload phase** — the CLI authenticates with `npx convex run --component`, + calls the component's internal `generateUploadUrls`, uploads each file to the + component's storage, records metadata, and garbage-collects old deployments. +3. **Serve phase** — the component's HTTP action looks up assets by path, + strips its mount prefix from the URL, and streams from storage with smart + caching. Paths without an extension fall back to `/index.html` for SPAs. -1. **Build Phase**: Your bundler (Vite, etc.) creates optimized files in `dist/` -2. **Upload Phase**: The upload script uses `npx convex run` to: - - Generate signed upload URLs - - Upload each file to Convex storage - - Record file metadata in the component's database - - Garbage collect files from previous deployments -3. **Serve Phase**: HTTP actions serve files from storage with: - - Correct Content-Type headers - - Smart cache control (immutable for hashed assets) - - SPA fallback for client-side routing +Because everything lives inside the component, your app code stays one line. ## Example -Check out the [example](./example) directory for a complete working example. +See [`example/`](./example) for a complete Vite + React app integration. ```bash npm install @@ -328,7 +270,7 @@ npm run dev ## Contributing -See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines. +See [CONTRIBUTING.md](./CONTRIBUTING.md). ## License diff --git a/dist/cli/setup.d.ts b/dist/cli/setup.d.ts index e21c641..60d3435 100644 --- a/dist/cli/setup.d.ts +++ b/dist/cli/setup.d.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Interactive setup wizard for Convex Static Hosting. + * Setup wizard for Convex Static Hosting. * * Usage: * npx @convex-dev/static-hosting setup diff --git a/dist/cli/setup.js b/dist/cli/setup.js index 972eb4b..15222f4 100644 --- a/dist/cli/setup.js +++ b/dist/cli/setup.js @@ -1,152 +1,78 @@ #!/usr/bin/env node /** - * Interactive setup wizard for Convex Static Hosting. + * Setup wizard for Convex Static Hosting. * * Usage: * npx @convex-dev/static-hosting setup */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { createInterface } from "readline"; import { join } from "path"; -const rl = createInterface({ - input: process.stdin, - output: process.stdout, -}); function success(msg) { console.log(`✓ ${msg}`); } function skip(msg) { console.log(`· ${msg}`); } -/** - * Create convex/convex.config.ts - */ function createConvexConfig() { const configPath = join(process.cwd(), "convex", "convex.config.ts"); if (existsSync(configPath)) { const existing = readFileSync(configPath, "utf-8"); - if (existing.includes("selfHosting")) { + if (existing.includes("@convex-dev/static-hosting")) { skip("convex/convex.config.ts (already configured)"); - return false; + return; } - // File exists but doesn't have our component - tell user to add manually - console.log("\n⚠️ convex/convex.config.ts exists. Please add manually:"); - console.log(' import selfHosting from "@convex-dev/static-hosting/convex.config";'); - console.log(" app.use(selfHosting);\n"); - return false; + console.log("\n⚠️ convex/convex.config.ts exists. Add manually:"); + console.log(' import staticHosting from "@convex-dev/static-hosting/convex.config";'); + console.log(' app.use(staticHosting, { httpPrefix: "/" });\n'); + return; } writeFileSync(configPath, `import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config"; +import staticHosting from "@convex-dev/static-hosting/convex.config"; const app = defineApp(); -app.use(selfHosting); +app.use(staticHosting, { httpPrefix: "/" }); export default app; `); success("Created convex/convex.config.ts"); - return true; -} -/** - * Create convex/staticHosting.ts - */ -function createStaticHostingFile() { - const filePath = join(process.cwd(), "convex", "staticHosting.ts"); - if (existsSync(filePath)) { - skip("convex/staticHosting.ts (already exists)"); - return false; - } - writeFileSync(filePath, `import { components } from "./_generated/api"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; - -// Internal functions for secure uploads (CLI only) -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -// Public query for live reload notifications -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); -`); - success("Created convex/staticHosting.ts"); - return true; -} -/** - * Create convex/http.ts - */ -function createHttpFile() { - const filePath = join(process.cwd(), "convex", "http.ts"); - if (existsSync(filePath)) { - const existing = readFileSync(filePath, "utf-8"); - if (existing.includes("registerStaticRoutes")) { - skip("convex/http.ts (already configured)"); - return false; - } - console.log("\n⚠️ convex/http.ts exists. Please add manually:"); - console.log(' import { registerStaticRoutes } from "@convex-dev/static-hosting";'); - console.log(" registerStaticRoutes(http, components.selfHosting);\n"); - return false; - } - writeFileSync(filePath, `import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Serve static files at root with SPA fallback -registerStaticRoutes(http, components.selfHosting); - -export default http; -`); - success("Created convex/http.ts"); - return true; } -/** - * Update package.json with deploy script - */ function updatePackageJson() { const pkgPath = join(process.cwd(), "package.json"); if (!existsSync(pkgPath)) { console.log("⚠️ No package.json found"); - return false; + return; } const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); if (!pkg.scripts) pkg.scripts = {}; if (pkg.scripts.deploy) { skip("package.json deploy script (already exists)"); - return false; + return; } pkg.scripts.deploy = "npx @convex-dev/static-hosting deploy"; writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); success("Added deploy script to package.json"); - return true; } -async function main() { +function main() { console.log("\n🚀 Convex Static Hosting Setup\n"); - // Check for convex directory if (!existsSync("convex")) { mkdirSync("convex"); success("Created convex/ directory"); } - console.log("Creating files...\n"); - // Create the Convex files createConvexConfig(); - createStaticHostingFile(); - createHttpFile(); updatePackageJson(); - // Next steps console.log("\n✨ Setup complete!\n"); console.log("Next steps:\n"); console.log(" 1. npx convex dev # Generate types"); - console.log(" 2. npm run deploy # Deploy everything\n"); + console.log(" 2. npm run deploy # Build and deploy\n"); console.log("Your app will be at: https://.convex.site\n"); - rl.close(); + console.log("Optional: to use from @convex-dev/static-hosting/react,"); + console.log("create convex/staticHosting.ts:\n"); + console.log(' import { exposeDeploymentQuery } from "@convex-dev/static-hosting";'); + console.log(' import { components } from "./_generated/api";'); + console.log(" export const { getCurrentDeployment } = exposeDeploymentQuery("); + console.log(" components.staticHosting,"); + console.log(" );\n"); } -main().catch((err) => { - console.error("Setup failed:", err); - rl.close(); - process.exit(1); -}); +main(); //# sourceMappingURL=setup.js.map \ No newline at end of file diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 4562533..482f3a7 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -8,7 +8,6 @@ * @module */ -import type * as http from "../http.js"; import type * as staticHosting from "../staticHosting.js"; import type { @@ -18,7 +17,6 @@ import type { } from "convex/server"; declare const fullApi: ApiFromModules<{ - http: typeof http; staticHosting: typeof staticHosting; }>; @@ -49,5 +47,5 @@ export declare const internal: FilterApi< >; export declare const components: { - selfHosting: import("@convex-dev/static-hosting/_generated/component.js").ComponentApi<"selfHosting">; + staticHosting: import("@convex-dev/static-hosting/_generated/component.js").ComponentApi<"staticHosting">; }; diff --git a/example/convex/convex.config.ts b/example/convex/convex.config.ts index cc80b82..4f84e0b 100644 --- a/example/convex/convex.config.ts +++ b/example/convex/convex.config.ts @@ -1,7 +1,7 @@ import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config.js"; +import staticHosting from "@convex-dev/static-hosting/convex.config.js"; -const app = defineApp(); -app.use(selfHosting); +const app = defineApp({ httpPrefix: "/app" }); +app.use(staticHosting, { httpPrefix: "/" }); export default app; diff --git a/example/convex/http.ts b/example/convex/http.ts deleted file mode 100644 index e173bfa..0000000 --- a/example/convex/http.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Register static file serving routes. -// This will serve your built static files from Convex storage. -// -// By default, it serves files at the root path "/" with SPA fallback enabled. -// This means: -// - /index.html -> serves index.html -// - /assets/main.js -> serves the JS file -// - /about -> serves index.html (SPA fallback for routes without file extension) -registerStaticRoutes(http, components.selfHosting); - -// You can also serve at a specific path prefix: -// registerStaticRoutes(http, components.selfHosting, { -// pathPrefix: "/app", -// spaFallback: true, -// }); - -// You can disable SPA fallback for API-only static file serving: -// registerStaticRoutes(http, components.selfHosting, { -// pathPrefix: "/static", -// spaFallback: false, -// }); - -export default http; diff --git a/example/convex/staticHosting.test.ts b/example/convex/staticHosting.test.ts deleted file mode 100644 index dcd7a4a..0000000 --- a/example/convex/staticHosting.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { initConvexTest } from "./setup.test"; -import { internal } from "./_generated/api"; - -describe("static hosting example (internal functions)", () => { - beforeEach(async () => { - vi.useFakeTimers(); - }); - - afterEach(async () => { - vi.useRealTimers(); - }); - - test("generateUploadUrl returns a URL", async () => { - const t = initConvexTest(); - const uploadUrl = await t.mutation(internal.staticHosting.generateUploadUrl, {}); - expect(uploadUrl).toBeDefined(); - expect(typeof uploadUrl).toBe("string"); - }); - - test("listAssets returns empty array initially", async () => { - const t = initConvexTest(); - const assets = await t.query(internal.staticHosting.listAssets, {}); - expect(assets).toHaveLength(0); - }); - - test("gcOldAssets returns 0 with no assets", async () => { - const t = initConvexTest(); - const result = await t.mutation(internal.staticHosting.gcOldAssets, { - currentDeploymentId: "test-deployment", - }); - expect(result.deleted).toBe(0); - expect(result.blobIds).toHaveLength(0); - }); -}); diff --git a/example/convex/staticHosting.ts b/example/convex/staticHosting.ts index 0919351..b8d2d54 100644 --- a/example/convex/staticHosting.ts +++ b/example/convex/staticHosting.ts @@ -1,15 +1,8 @@ import { components } from "./_generated/api.js"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; +import { exposeDeploymentQuery } from "@convex-dev/static-hosting"; -// Expose the upload API as INTERNAL functions. -// These can only be called via `npx convex run` - not from the public internet. -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -// Expose the deployment query for live reload notifications. -// Clients subscribe to this to know when a new deployment is available. -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); +// Public query for live-reload notifications. Only needed if you use +// / useDeploymentUpdates from @convex-dev/static-hosting/react. +export const { getCurrentDeployment } = exposeDeploymentQuery( + components.staticHosting, +); diff --git a/example/dist/assets/index-BGY4sn62.js b/example/dist/assets/index-BGY4sn62.js deleted file mode 100644 index 3252a3e..0000000 --- a/example/dist/assets/index-BGY4sn62.js +++ /dev/null @@ -1,10 +0,0 @@ -(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))f(h);new MutationObserver(h=>{for(const y of h)if(y.type==="childList")for(const v of y.addedNodes)v.tagName==="LINK"&&v.rel==="modulepreload"&&f(v)}).observe(document,{childList:!0,subtree:!0});function s(h){const y={};return h.integrity&&(y.integrity=h.integrity),h.referrerPolicy&&(y.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?y.credentials="include":h.crossOrigin==="anonymous"?y.credentials="omit":y.credentials="same-origin",y}function f(h){if(h.ep)return;h.ep=!0;const y=s(h);fetch(h.href,y)}})();function rm(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Qc={exports:{}},Ba={};var ld;function hm(){if(ld)return Ba;ld=1;var o=Symbol.for("react.transitional.element"),i=Symbol.for("react.fragment");function s(f,h,y){var v=null;if(y!==void 0&&(v=""+y),h.key!==void 0&&(v=""+h.key),"key"in h){y={};for(var q in h)q!=="key"&&(y[q]=h[q])}else y=h;return h=y.ref,{$$typeof:o,type:f,key:v,ref:h!==void 0?h:null,props:y}}return Ba.Fragment=i,Ba.jsx=s,Ba.jsxs=s,Ba}var ad;function dm(){return ad||(ad=1,Qc.exports=hm()),Qc.exports}var Y=dm(),xc={exports:{}},G={};var ud;function ym(){if(ud)return G;ud=1;var o=Symbol.for("react.transitional.element"),i=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),h=Symbol.for("react.profiler"),y=Symbol.for("react.consumer"),v=Symbol.for("react.context"),q=Symbol.for("react.forward_ref"),E=Symbol.for("react.suspense"),_=Symbol.for("react.memo"),H=Symbol.for("react.lazy"),N=Symbol.for("react.activity"),et=Symbol.iterator;function L(m){return m===null||typeof m!="object"?null:(m=et&&m[et]||m["@@iterator"],typeof m=="function"?m:null)}var X={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},vt=Object.assign,oe={};function Dt(m,C,U){this.props=m,this.context=C,this.refs=oe,this.updater=U||X}Dt.prototype.isReactComponent={},Dt.prototype.setState=function(m,C){if(typeof m!="object"&&typeof m!="function"&&m!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,m,C,"setState")},Dt.prototype.forceUpdate=function(m){this.updater.enqueueForceUpdate(this,m,"forceUpdate")};function sn(){}sn.prototype=Dt.prototype;function Ht(m,C,U){this.props=m,this.context=C,this.refs=oe,this.updater=U||X}var re=Ht.prototype=new sn;re.constructor=Ht,vt(re,Dt.prototype),re.isPureReactComponent=!0;var Ce=Array.isArray;function Yt(){}var P={H:null,A:null,T:null,S:null},Gt=Object.prototype.hasOwnProperty;function qe(m,C,U){var x=U.ref;return{$$typeof:o,type:m,key:C,ref:x!==void 0?x:null,props:U}}function el(m,C){return qe(m.type,C,m.props)}function De(m){return typeof m=="object"&&m!==null&&m.$$typeof===o}function Xt(m){var C={"=":"=0",":":"=2"};return"$"+m.replace(/[=:]/g,function(U){return C[U]})}var Nn=/\/+/g;function je(m,C){return typeof m=="object"&&m!==null&&m.key!=null?Xt(""+m.key):C.toString(36)}function Oe(m){switch(m.status){case"fulfilled":return m.value;case"rejected":throw m.reason;default:switch(typeof m.status=="string"?m.then(Yt,Yt):(m.status="pending",m.then(function(C){m.status==="pending"&&(m.status="fulfilled",m.value=C)},function(C){m.status==="pending"&&(m.status="rejected",m.reason=C)})),m.status){case"fulfilled":return m.value;case"rejected":throw m.reason}}throw m}function M(m,C,U,x,Z){var J=typeof m;(J==="undefined"||J==="boolean")&&(m=null);var ut=!1;if(m===null)ut=!0;else switch(J){case"bigint":case"string":case"number":ut=!0;break;case"object":switch(m.$$typeof){case o:case i:ut=!0;break;case H:return ut=m._init,M(ut(m._payload),C,U,x,Z)}}if(ut)return Z=Z(m),ut=x===""?"."+je(m,0):x,Ce(Z)?(U="",ut!=null&&(U=ut.replace(Nn,"$&/")+"/"),M(Z,C,U,"",function(Xl){return Xl})):Z!=null&&(De(Z)&&(Z=el(Z,U+(Z.key==null||m&&m.key===Z.key?"":(""+Z.key).replace(Nn,"$&/")+"/")+ut)),C.push(Z)),1;ut=0;var Lt=x===""?".":x+":";if(Ce(m))for(var bt=0;bt>>1,ht=M[ct];if(0>>1;cth(U,V))xh(Z,U)?(M[ct]=Z,M[x]=V,ct=x):(M[ct]=U,M[C]=V,ct=C);else if(xh(Z,V))M[ct]=Z,M[x]=V,ct=x;else break t}}return D}function h(M,D){var V=M.sortIndex-D.sortIndex;return V!==0?V:M.id-D.id}if(o.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var y=performance;o.unstable_now=function(){return y.now()}}else{var v=Date,q=v.now();o.unstable_now=function(){return v.now()-q}}var E=[],_=[],H=1,N=null,et=3,L=!1,X=!1,vt=!1,oe=!1,Dt=typeof setTimeout=="function"?setTimeout:null,sn=typeof clearTimeout=="function"?clearTimeout:null,Ht=typeof setImmediate<"u"?setImmediate:null;function re(M){for(var D=s(_);D!==null;){if(D.callback===null)f(_);else if(D.startTime<=M)f(_),D.sortIndex=D.expirationTime,i(E,D);else break;D=s(_)}}function Ce(M){if(vt=!1,re(M),!X)if(s(E)!==null)X=!0,Yt||(Yt=!0,Xt());else{var D=s(_);D!==null&&Oe(Ce,D.startTime-M)}}var Yt=!1,P=-1,Gt=5,qe=-1;function el(){return oe?!0:!(o.unstable_now()-qeM&&el());){var ct=N.callback;if(typeof ct=="function"){N.callback=null,et=N.priorityLevel;var ht=ct(N.expirationTime<=M);if(M=o.unstable_now(),typeof ht=="function"){N.callback=ht,re(M),D=!0;break e}N===s(E)&&f(E),re(M)}else f(E);N=s(E)}if(N!==null)D=!0;else{var m=s(_);m!==null&&Oe(Ce,m.startTime-M),D=!1}}break t}finally{N=null,et=V,L=!1}D=void 0}}finally{D?Xt():Yt=!1}}}var Xt;if(typeof Ht=="function")Xt=function(){Ht(De)};else if(typeof MessageChannel<"u"){var Nn=new MessageChannel,je=Nn.port2;Nn.port1.onmessage=De,Xt=function(){je.postMessage(null)}}else Xt=function(){Dt(De,0)};function Oe(M,D){P=Dt(function(){M(o.unstable_now())},D)}o.unstable_IdlePriority=5,o.unstable_ImmediatePriority=1,o.unstable_LowPriority=4,o.unstable_NormalPriority=3,o.unstable_Profiling=null,o.unstable_UserBlockingPriority=2,o.unstable_cancelCallback=function(M){M.callback=null},o.unstable_forceFrameRate=function(M){0>M||125ct?(M.sortIndex=V,i(_,M),s(E)===null&&M===s(_)&&(vt?(sn(P),P=-1):vt=!0,Oe(Ce,V-ct))):(M.sortIndex=ht,i(E,M),X||L||(X=!0,Yt||(Yt=!0,Xt()))),M},o.unstable_shouldYield=el,o.unstable_wrapCallback=function(M){var D=et;return function(){var V=et;et=D;try{return M.apply(this,arguments)}finally{et=V}}}}(Hc)),Hc}var cd;function mm(){return cd||(cd=1,Bc.exports=gm()),Bc.exports}var jc={exports:{}},jt={};var fd;function vm(){if(fd)return jt;fd=1;var o=Wc();function i(E){var _="https://react.dev/errors/"+E;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(i){console.error(i)}}return o(),jc.exports=vm(),jc.exports}var rd;function pm(){if(rd)return Ha;rd=1;var o=mm(),i=Wc(),s=Sm();function f(t){var e="https://react.dev/errors/"+t;if(1ht||(t.current=ct[ht],ct[ht]=null,ht--)}function U(t,e){ht++,ct[ht]=t.current,t.current=e}var x=m(null),Z=m(null),J=m(null),ut=m(null);function Lt(t,e){switch(U(J,e),U(Z,t),U(x,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?Mh(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=Mh(e),t=Rh(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}C(x),U(x,t)}function bt(){C(x),C(Z),C(J)}function Xl(t){t.memoizedState!==null&&U(ut,t);var e=x.current,n=Rh(e,t.type);e!==n&&(U(Z,t),U(x,n))}function Ka(t){Z.current===t&&(C(x),C(Z)),ut.current===t&&(C(ut),Na._currentValue=V)}var mi,tf;function Qn(t){if(mi===void 0)try{throw Error()}catch(n){var e=n.stack.trim().match(/\n( *(at )?)/);mi=e&&e[1]||"",tf=-1)":-1a||d[l]!==b[a]){var O=` -`+d[l].replace(" at new "," at ");return t.displayName&&O.includes("")&&(O=O.replace("",t.displayName)),O}while(1<=l&&0<=a);break}}}finally{vi=!1,Error.prepareStackTrace=n}return(n=t?t.displayName||t.name:"")?Qn(n):""}function Yd(t,e){switch(t.tag){case 26:case 27:case 5:return Qn(t.type);case 16:return Qn("Lazy");case 13:return t.child!==e&&e!==null?Qn("Suspense Fallback"):Qn("Suspense");case 19:return Qn("SuspenseList");case 0:case 15:return Si(t.type,!1);case 11:return Si(t.type.render,!1);case 1:return Si(t.type,!0);case 31:return Qn("Activity");default:return""}}function ef(t){try{var e="",n=null;do e+=Yd(t,n),n=t,t=t.return;while(t);return e}catch(l){return` -Error generating stack: `+l.message+` -`+l.stack}}var pi=Object.prototype.hasOwnProperty,bi=o.unstable_scheduleCallback,Ti=o.unstable_cancelCallback,Gd=o.unstable_shouldYield,Xd=o.unstable_requestPaint,te=o.unstable_now,Zd=o.unstable_getCurrentPriorityLevel,nf=o.unstable_ImmediatePriority,lf=o.unstable_UserBlockingPriority,ka=o.unstable_NormalPriority,Kd=o.unstable_LowPriority,af=o.unstable_IdlePriority,kd=o.log,Jd=o.unstable_setDisableYieldValue,Zl=null,ee=null;function cn(t){if(typeof kd=="function"&&Jd(t),ee&&typeof ee.setStrictMode=="function")try{ee.setStrictMode(Zl,t)}catch{}}var ne=Math.clz32?Math.clz32:Wd,$d=Math.log,Fd=Math.LN2;function Wd(t){return t>>>=0,t===0?32:31-($d(t)/Fd|0)|0}var Ja=256,$a=262144,Fa=4194304;function xn(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Wa(t,e,n){var l=t.pendingLanes;if(l===0)return 0;var a=0,u=t.suspendedLanes,c=t.pingedLanes;t=t.warmLanes;var r=l&134217727;return r!==0?(l=r&~u,l!==0?a=xn(l):(c&=r,c!==0?a=xn(c):n||(n=r&~t,n!==0&&(a=xn(n))))):(r=l&~u,r!==0?a=xn(r):c!==0?a=xn(c):n||(n=l&~t,n!==0&&(a=xn(n)))),a===0?0:e!==0&&e!==a&&(e&u)===0&&(u=a&-a,n=e&-e,u>=n||u===32&&(n&4194048)!==0)?e:a}function Kl(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Id(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function uf(){var t=Fa;return Fa<<=1,(Fa&62914560)===0&&(Fa=4194304),t}function Ai(t){for(var e=[],n=0;31>n;n++)e.push(t);return e}function kl(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Pd(t,e,n,l,a,u){var c=t.pendingLanes;t.pendingLanes=n,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=n,t.entangledLanes&=n,t.errorRecoveryDisabledLanes&=n,t.shellSuspendCounter=0;var r=t.entanglements,d=t.expirationTimes,b=t.hiddenUpdates;for(n=c&~n;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var uy=/[\n"\\]/g;function de(t){return t.replace(uy,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function zi(t,e,n,l,a,u,c,r){t.name="",c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"?t.type=c:t.removeAttribute("type"),e!=null?c==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+he(e)):t.value!==""+he(e)&&(t.value=""+he(e)):c!=="submit"&&c!=="reset"||t.removeAttribute("value"),e!=null?Ci(t,c,he(e)):n!=null?Ci(t,c,he(n)):l!=null&&t.removeAttribute("value"),a==null&&u!=null&&(t.defaultChecked=!!u),a!=null&&(t.checked=a&&typeof a!="function"&&typeof a!="symbol"),r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"?t.name=""+he(r):t.removeAttribute("name")}function pf(t,e,n,l,a,u,c,r){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(t.type=u),e!=null||n!=null){if(!(u!=="submit"&&u!=="reset"||e!=null)){Ri(t);return}n=n!=null?""+he(n):"",e=e!=null?""+he(e):n,r||e===t.value||(t.value=e),t.defaultValue=e}l=l??a,l=typeof l!="function"&&typeof l!="symbol"&&!!l,t.checked=r?t.checked:!!l,t.defaultChecked=!!l,c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(t.name=c),Ri(t)}function Ci(t,e,n){e==="number"&&tu(t.ownerDocument)===t||t.defaultValue===""+n||(t.defaultValue=""+n)}function sl(t,e,n,l){if(t=t.options,e){e={};for(var a=0;a"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Qi=!1;if(Ye)try{var Wl={};Object.defineProperty(Wl,"passive",{get:function(){Qi=!0}}),window.addEventListener("test",Wl,Wl),window.removeEventListener("test",Wl,Wl)}catch{Qi=!1}var on=null,xi=null,nu=null;function Mf(){if(nu)return nu;var t,e=xi,n=e.length,l,a="value"in on?on.value:on.textContent,u=a.length;for(t=0;t=ta),Uf=" ",Nf=!1;function Qf(t,e){switch(t){case"keyup":return Ny.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function xf(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var rl=!1;function xy(t,e){switch(t){case"compositionend":return xf(e);case"keypress":return e.which!==32?null:(Nf=!0,Uf);case"textInput":return t=e.data,t===Uf&&Nf?null:t;default:return null}}function wy(t,e){if(rl)return t==="compositionend"||!Li&&Qf(t,e)?(t=Mf(),nu=xi=on=null,rl=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:n,offset:e-t};t=l}t:{for(;n;){if(n.nextSibling){n=n.nextSibling;break t}n=n.parentNode}n=void 0}n=Gf(n)}}function Zf(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?Zf(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function Kf(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=tu(t.document);e instanceof t.HTMLIFrameElement;){try{var n=typeof e.contentWindow.location.href=="string"}catch{n=!1}if(n)t=e.contentWindow;else break;e=tu(t.document)}return e}function Gi(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var Xy=Ye&&"documentMode"in document&&11>=document.documentMode,hl=null,Xi=null,aa=null,Zi=!1;function kf(t,e,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Zi||hl==null||hl!==tu(l)||(l=hl,"selectionStart"in l&&Gi(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),aa&&la(aa,l)||(aa=l,l=$u(Xi,"onSelect"),0>=c,a-=c,Ue=1<<32-ne(e)+a|n<k?(I=w,w=null):I=w.sibling;var lt=T(S,w,p[k],R);if(lt===null){w===null&&(w=I);break}t&&w&<.alternate===null&&e(S,w),g=u(lt,g,k),nt===null?B=lt:nt.sibling=lt,nt=lt,w=I}if(k===p.length)return n(S,w),tt&&Xe(S,k),B;if(w===null){for(;kk?(I=w,w=null):I=w.sibling;var Un=T(S,w,lt.value,R);if(Un===null){w===null&&(w=I);break}t&&w&&Un.alternate===null&&e(S,w),g=u(Un,g,k),nt===null?B=Un:nt.sibling=Un,nt=Un,w=I}if(lt.done)return n(S,w),tt&&Xe(S,k),B;if(w===null){for(;!lt.done;k++,lt=p.next())lt=z(S,lt.value,R),lt!==null&&(g=u(lt,g,k),nt===null?B=lt:nt.sibling=lt,nt=lt);return tt&&Xe(S,k),B}for(w=l(w);!lt.done;k++,lt=p.next())lt=A(w,S,k,lt.value,R),lt!==null&&(t&<.alternate!==null&&w.delete(lt.key===null?k:lt.key),g=u(lt,g,k),nt===null?B=lt:nt.sibling=lt,nt=lt);return t&&w.forEach(function(om){return e(S,om)}),tt&&Xe(S,k),B}function rt(S,g,p,R){if(typeof p=="object"&&p!==null&&p.type===vt&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case L:t:{for(var B=p.key;g!==null;){if(g.key===B){if(B=p.type,B===vt){if(g.tag===7){n(S,g.sibling),R=a(g,p.props.children),R.return=S,S=R;break t}}else if(g.elementType===B||typeof B=="object"&&B!==null&&B.$$typeof===Gt&&Kn(B)===g.type){n(S,g.sibling),R=a(g,p.props),oa(R,p),R.return=S,S=R;break t}n(S,g);break}else e(S,g);g=g.sibling}p.type===vt?(R=Vn(p.props.children,S.mode,R,p.key),R.return=S,S=R):(R=hu(p.type,p.key,p.props,null,S.mode,R),oa(R,p),R.return=S,S=R)}return c(S);case X:t:{for(B=p.key;g!==null;){if(g.key===B)if(g.tag===4&&g.stateNode.containerInfo===p.containerInfo&&g.stateNode.implementation===p.implementation){n(S,g.sibling),R=a(g,p.children||[]),R.return=S,S=R;break t}else{n(S,g);break}else e(S,g);g=g.sibling}R=Ii(p,S.mode,R),R.return=S,S=R}return c(S);case Gt:return p=Kn(p),rt(S,g,p,R)}if(Oe(p))return Q(S,g,p,R);if(Xt(p)){if(B=Xt(p),typeof B!="function")throw Error(f(150));return p=B.call(p),j(S,g,p,R)}if(typeof p.then=="function")return rt(S,g,pu(p),R);if(p.$$typeof===Ht)return rt(S,g,gu(S,p),R);bu(S,p)}return typeof p=="string"&&p!==""||typeof p=="number"||typeof p=="bigint"?(p=""+p,g!==null&&g.tag===6?(n(S,g.sibling),R=a(g,p),R.return=S,S=R):(n(S,g),R=Wi(p,S.mode,R),R.return=S,S=R),c(S)):n(S,g)}return function(S,g,p,R){try{fa=0;var B=rt(S,g,p,R);return El=null,B}catch(w){if(w===Al||w===vu)throw w;var nt=ae(29,w,null,S.mode);return nt.lanes=R,nt.return=S,nt}finally{}}}var Jn=vo(!0),So=vo(!1),gn=!1;function os(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function rs(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function mn(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function vn(t,e,n){var l=t.updateQueue;if(l===null)return null;if(l=l.shared,(at&2)!==0){var a=l.pending;return a===null?e.next=e:(e.next=a.next,a.next=e),l.pending=e,e=ru(t),to(t,null,n),e}return ou(t,l,e,n),ru(t)}function ra(t,e,n){if(e=e.updateQueue,e!==null&&(e=e.shared,(n&4194048)!==0)){var l=e.lanes;l&=t.pendingLanes,n|=l,e.lanes=n,cf(t,n)}}function hs(t,e){var n=t.updateQueue,l=t.alternate;if(l!==null&&(l=l.updateQueue,n===l)){var a=null,u=null;if(n=n.firstBaseUpdate,n!==null){do{var c={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};u===null?a=u=c:u=u.next=c,n=n.next}while(n!==null);u===null?a=u=e:u=u.next=e}else a=u=e;n={baseState:l.baseState,firstBaseUpdate:a,lastBaseUpdate:u,shared:l.shared,callbacks:l.callbacks},t.updateQueue=n;return}t=n.lastBaseUpdate,t===null?n.firstBaseUpdate=e:t.next=e,n.lastBaseUpdate=e}var ds=!1;function ha(){if(ds){var t=Tl;if(t!==null)throw t}}function da(t,e,n,l){ds=!1;var a=t.updateQueue;gn=!1;var u=a.firstBaseUpdate,c=a.lastBaseUpdate,r=a.shared.pending;if(r!==null){a.shared.pending=null;var d=r,b=d.next;d.next=null,c===null?u=b:c.next=b,c=d;var O=t.alternate;O!==null&&(O=O.updateQueue,r=O.lastBaseUpdate,r!==c&&(r===null?O.firstBaseUpdate=b:r.next=b,O.lastBaseUpdate=d))}if(u!==null){var z=a.baseState;c=0,O=b=d=null,r=u;do{var T=r.lane&-536870913,A=T!==r.lane;if(A?(W&T)===T:(l&T)===T){T!==0&&T===bl&&(ds=!0),O!==null&&(O=O.next={lane:0,tag:r.tag,payload:r.payload,callback:null,next:null});t:{var Q=t,j=r;T=e;var rt=n;switch(j.tag){case 1:if(Q=j.payload,typeof Q=="function"){z=Q.call(rt,z,T);break t}z=Q;break t;case 3:Q.flags=Q.flags&-65537|128;case 0:if(Q=j.payload,T=typeof Q=="function"?Q.call(rt,z,T):Q,T==null)break t;z=N({},z,T);break t;case 2:gn=!0}}T=r.callback,T!==null&&(t.flags|=64,A&&(t.flags|=8192),A=a.callbacks,A===null?a.callbacks=[T]:A.push(T))}else A={lane:T,tag:r.tag,payload:r.payload,callback:r.callback,next:null},O===null?(b=O=A,d=z):O=O.next=A,c|=T;if(r=r.next,r===null){if(r=a.shared.pending,r===null)break;A=r,r=A.next,A.next=null,a.lastBaseUpdate=A,a.shared.pending=null}}while(!0);O===null&&(d=z),a.baseState=d,a.firstBaseUpdate=b,a.lastBaseUpdate=O,u===null&&(a.shared.lanes=0),An|=c,t.lanes=c,t.memoizedState=z}}function po(t,e){if(typeof t!="function")throw Error(f(191,t));t.call(e)}function bo(t,e){var n=t.callbacks;if(n!==null)for(t.callbacks=null,t=0;tu?u:8;var c=M.T,r={};M.T=r,Us(t,!1,e,n);try{var d=a(),b=M.S;if(b!==null&&b(r,d),d!==null&&typeof d=="object"&&typeof d.then=="function"){var O=Py(d,l);ma(t,e,O,fe(t))}else ma(t,e,l,fe(t))}catch(z){ma(t,e,{then:function(){},status:"rejected",reason:z},fe())}finally{D.p=u,c!==null&&r.types!==null&&(c.types=r.types),M.T=c}}function ug(){}function qs(t,e,n,l){if(t.tag!==5)throw Error(f(476));var a=Io(t).queue;Wo(t,a,e,V,n===null?ug:function(){return Po(t),n(l)})}function Io(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:V,baseState:V,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Je,lastRenderedState:V},next:null};var n={};return e.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Je,lastRenderedState:n},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function Po(t){var e=Io(t);e.next===null&&(e=t.alternate.memoizedState),ma(t,e.next.queue,{},fe())}function Ds(){return Qt(Na)}function tr(){return At().memoizedState}function er(){return At().memoizedState}function ig(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var n=fe();t=mn(n);var l=vn(e,t,n);l!==null&&(Wt(l,e,n),ra(l,e,n)),e={cache:is()},t.payload=e;return}e=e.return}}function sg(t,e,n){var l=fe();n={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},qu(t)?lr(e,n):(n=$i(t,e,n,l),n!==null&&(Wt(n,t,l),ar(n,e,l)))}function nr(t,e,n){var l=fe();ma(t,e,n,l)}function ma(t,e,n,l){var a={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(qu(t))lr(e,a);else{var u=t.alternate;if(t.lanes===0&&(u===null||u.lanes===0)&&(u=e.lastRenderedReducer,u!==null))try{var c=e.lastRenderedState,r=u(c,n);if(a.hasEagerState=!0,a.eagerState=r,le(r,c))return ou(t,e,a,0),dt===null&&fu(),!1}catch{}finally{}if(n=$i(t,e,a,l),n!==null)return Wt(n,t,l),ar(n,e,l),!0}return!1}function Us(t,e,n,l){if(l={lane:2,revertLane:oc(),gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},qu(t)){if(e)throw Error(f(479))}else e=$i(t,n,l,2),e!==null&&Wt(e,t,2)}function qu(t){var e=t.alternate;return t===K||e!==null&&e===K}function lr(t,e){Ol=Eu=!0;var n=t.pending;n===null?e.next=e:(e.next=n.next,n.next=e),t.pending=e}function ar(t,e,n){if((n&4194048)!==0){var l=e.lanes;l&=t.pendingLanes,n|=l,e.lanes=n,cf(t,n)}}var va={readContext:Qt,use:Mu,useCallback:St,useContext:St,useEffect:St,useImperativeHandle:St,useLayoutEffect:St,useInsertionEffect:St,useMemo:St,useReducer:St,useRef:St,useState:St,useDebugValue:St,useDeferredValue:St,useTransition:St,useSyncExternalStore:St,useId:St,useHostTransitionStatus:St,useFormState:St,useActionState:St,useOptimistic:St,useMemoCache:St,useCacheRefresh:St};va.useEffectEvent=St;var ur={readContext:Qt,use:Mu,useCallback:function(t,e){return Vt().memoizedState=[t,e===void 0?null:e],t},useContext:Qt,useEffect:Yo,useImperativeHandle:function(t,e,n){n=n!=null?n.concat([t]):null,zu(4194308,4,Ko.bind(null,e,t),n)},useLayoutEffect:function(t,e){return zu(4194308,4,t,e)},useInsertionEffect:function(t,e){zu(4,2,t,e)},useMemo:function(t,e){var n=Vt();e=e===void 0?null:e;var l=t();if($n){cn(!0);try{t()}finally{cn(!1)}}return n.memoizedState=[l,e],l},useReducer:function(t,e,n){var l=Vt();if(n!==void 0){var a=n(e);if($n){cn(!0);try{n(e)}finally{cn(!1)}}}else a=e;return l.memoizedState=l.baseState=a,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:a},l.queue=t,t=t.dispatch=sg.bind(null,K,t),[l.memoizedState,t]},useRef:function(t){var e=Vt();return t={current:t},e.memoizedState=t},useState:function(t){t=Os(t);var e=t.queue,n=nr.bind(null,K,e);return e.dispatch=n,[t.memoizedState,n]},useDebugValue:zs,useDeferredValue:function(t,e){var n=Vt();return Cs(n,t,e)},useTransition:function(){var t=Os(!1);return t=Wo.bind(null,K,t.queue,!0,!1),Vt().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,n){var l=K,a=Vt();if(tt){if(n===void 0)throw Error(f(407));n=n()}else{if(n=e(),dt===null)throw Error(f(349));(W&127)!==0||Mo(l,e,n)}a.memoizedState=n;var u={value:n,getSnapshot:e};return a.queue=u,Yo(zo.bind(null,l,u,t),[t]),l.flags|=2048,Rl(9,{destroy:void 0},Ro.bind(null,l,u,n,e),null),n},useId:function(){var t=Vt(),e=dt.identifierPrefix;if(tt){var n=Ne,l=Ue;n=(l&~(1<<32-ne(l)-1)).toString(32)+n,e="_"+e+"R_"+n,n=_u++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof l.is=="string"?c.createElement("select",{is:l.is}):c.createElement("select"),l.multiple?u.multiple=!0:l.size&&(u.size=l.size);break;default:u=typeof l.is=="string"?c.createElement(a,{is:l.is}):c.createElement(a)}}u[Ut]=e,u[Zt]=l;t:for(c=e.child;c!==null;){if(c.tag===5||c.tag===6)u.appendChild(c.stateNode);else if(c.tag!==4&&c.tag!==27&&c.child!==null){c.child.return=c,c=c.child;continue}if(c===e)break t;for(;c.sibling===null;){if(c.return===null||c.return===e)break t;c=c.return}c.sibling.return=c.return,c=c.sibling}e.stateNode=u;t:switch(wt(u,a,l),a){case"button":case"input":case"select":case"textarea":l=!!l.autoFocus;break t;case"img":l=!0;break t;default:l=!1}l&&Fe(e)}}return gt(e),Ks(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,n),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==l&&Fe(e);else{if(typeof l!="string"&&e.stateNode===null)throw Error(f(166));if(t=J.current,Sl(e)){if(t=e.stateNode,n=e.memoizedProps,l=null,a=Nt,a!==null)switch(a.tag){case 27:case 5:l=a.memoizedProps}t[Ut]=e,t=!!(t.nodeValue===n||l!==null&&l.suppressHydrationWarning===!0||_h(t.nodeValue,n)),t||dn(e,!0)}else t=Fu(t).createTextNode(l),t[Ut]=e,e.stateNode=t}return gt(e),null;case 31:if(n=e.memoizedState,t===null||t.memoizedState!==null){if(l=Sl(e),n!==null){if(t===null){if(!l)throw Error(f(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(f(557));t[Ut]=e}else Yn(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;gt(e),t=!1}else n=ns(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),t=!0;if(!t)return e.flags&256?(ie(e),e):(ie(e),null);if((e.flags&128)!==0)throw Error(f(558))}return gt(e),null;case 13:if(l=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(a=Sl(e),l!==null&&l.dehydrated!==null){if(t===null){if(!a)throw Error(f(318));if(a=e.memoizedState,a=a!==null?a.dehydrated:null,!a)throw Error(f(317));a[Ut]=e}else Yn(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;gt(e),a=!1}else a=ns(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=a),a=!0;if(!a)return e.flags&256?(ie(e),e):(ie(e),null)}return ie(e),(e.flags&128)!==0?(e.lanes=n,e):(n=l!==null,t=t!==null&&t.memoizedState!==null,n&&(l=e.child,a=null,l.alternate!==null&&l.alternate.memoizedState!==null&&l.alternate.memoizedState.cachePool!==null&&(a=l.alternate.memoizedState.cachePool.pool),u=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(u=l.memoizedState.cachePool.pool),u!==a&&(l.flags|=2048)),n!==t&&n&&(e.child.flags|=8192),xu(e,e.updateQueue),gt(e),null);case 4:return bt(),t===null&&yc(e.stateNode.containerInfo),gt(e),null;case 10:return Ke(e.type),gt(e),null;case 19:if(C(Tt),l=e.memoizedState,l===null)return gt(e),null;if(a=(e.flags&128)!==0,u=l.rendering,u===null)if(a)pa(l,!1);else{if(pt!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(u=Au(t),u!==null){for(e.flags|=128,pa(l,!1),t=u.updateQueue,e.updateQueue=t,xu(e,t),e.subtreeFlags=0,t=n,n=e.child;n!==null;)eo(n,t),n=n.sibling;return U(Tt,Tt.current&1|2),tt&&Xe(e,l.treeForkCount),e.child}t=t.sibling}l.tail!==null&&te()>Lu&&(e.flags|=128,a=!0,pa(l,!1),e.lanes=4194304)}else{if(!a)if(t=Au(u),t!==null){if(e.flags|=128,a=!0,t=t.updateQueue,e.updateQueue=t,xu(e,t),pa(l,!0),l.tail===null&&l.tailMode==="hidden"&&!u.alternate&&!tt)return gt(e),null}else 2*te()-l.renderingStartTime>Lu&&n!==536870912&&(e.flags|=128,a=!0,pa(l,!1),e.lanes=4194304);l.isBackwards?(u.sibling=e.child,e.child=u):(t=l.last,t!==null?t.sibling=u:e.child=u,l.last=u)}return l.tail!==null?(t=l.tail,l.rendering=t,l.tail=t.sibling,l.renderingStartTime=te(),t.sibling=null,n=Tt.current,U(Tt,a?n&1|2:n&1),tt&&Xe(e,l.treeForkCount),t):(gt(e),null);case 22:case 23:return ie(e),gs(),l=e.memoizedState!==null,t!==null?t.memoizedState!==null!==l&&(e.flags|=8192):l&&(e.flags|=8192),l?(n&536870912)!==0&&(e.flags&128)===0&&(gt(e),e.subtreeFlags&6&&(e.flags|=8192)):gt(e),n=e.updateQueue,n!==null&&xu(e,n.retryQueue),n=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(n=t.memoizedState.cachePool.pool),l=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),l!==n&&(e.flags|=2048),t!==null&&C(Zn),null;case 24:return n=null,t!==null&&(n=t.memoizedState.cache),e.memoizedState.cache!==n&&(e.flags|=2048),Ke(Et),gt(e),null;case 25:return null;case 30:return null}throw Error(f(156,e.tag))}function hg(t,e){switch(ts(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return Ke(Et),bt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return Ka(e),null;case 31:if(e.memoizedState!==null){if(ie(e),e.alternate===null)throw Error(f(340));Yn()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(ie(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(f(340));Yn()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return C(Tt),null;case 4:return bt(),null;case 10:return Ke(e.type),null;case 22:case 23:return ie(e),gs(),t!==null&&C(Zn),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return Ke(Et),null;case 25:return null;default:return null}}function Cr(t,e){switch(ts(e),e.tag){case 3:Ke(Et),bt();break;case 26:case 27:case 5:Ka(e);break;case 4:bt();break;case 31:e.memoizedState!==null&&ie(e);break;case 13:ie(e);break;case 19:C(Tt);break;case 10:Ke(e.type);break;case 22:case 23:ie(e),gs(),t!==null&&C(Zn);break;case 24:Ke(Et)}}function ba(t,e){try{var n=e.updateQueue,l=n!==null?n.lastEffect:null;if(l!==null){var a=l.next;n=a;do{if((n.tag&t)===t){l=void 0;var u=n.create,c=n.inst;l=u(),c.destroy=l}n=n.next}while(n!==a)}}catch(r){st(e,e.return,r)}}function bn(t,e,n){try{var l=e.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var u=a.next;l=u;do{if((l.tag&t)===t){var c=l.inst,r=c.destroy;if(r!==void 0){c.destroy=void 0,a=e;var d=n,b=r;try{b()}catch(O){st(a,d,O)}}}l=l.next}while(l!==u)}}catch(O){st(e,e.return,O)}}function qr(t){var e=t.updateQueue;if(e!==null){var n=t.stateNode;try{bo(e,n)}catch(l){st(t,t.return,l)}}}function Dr(t,e,n){n.props=Fn(t.type,t.memoizedProps),n.state=t.memoizedState;try{n.componentWillUnmount()}catch(l){st(t,e,l)}}function Ta(t,e){try{var n=t.ref;if(n!==null){switch(t.tag){case 26:case 27:case 5:var l=t.stateNode;break;case 30:l=t.stateNode;break;default:l=t.stateNode}typeof n=="function"?t.refCleanup=n(l):n.current=l}}catch(a){st(t,e,a)}}function Qe(t,e){var n=t.ref,l=t.refCleanup;if(n!==null)if(typeof l=="function")try{l()}catch(a){st(t,e,a)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(a){st(t,e,a)}else n.current=null}function Ur(t){var e=t.type,n=t.memoizedProps,l=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":n.autoFocus&&l.focus();break t;case"img":n.src?l.src=n.src:n.srcSet&&(l.srcset=n.srcSet)}}catch(a){st(t,t.return,a)}}function ks(t,e,n){try{var l=t.stateNode;Qg(l,t.type,n,e),l[Zt]=e}catch(a){st(t,t.return,a)}}function Nr(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Rn(t.type)||t.tag===4}function Js(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||Nr(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Rn(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function $s(t,e,n){var l=t.tag;if(l===5||l===6)t=t.stateNode,e?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(t,e):(e=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,e.appendChild(t),n=n._reactRootContainer,n!=null||e.onclick!==null||(e.onclick=Ve));else if(l!==4&&(l===27&&Rn(t.type)&&(n=t.stateNode,e=null),t=t.child,t!==null))for($s(t,e,n),t=t.sibling;t!==null;)$s(t,e,n),t=t.sibling}function wu(t,e,n){var l=t.tag;if(l===5||l===6)t=t.stateNode,e?n.insertBefore(t,e):n.appendChild(t);else if(l!==4&&(l===27&&Rn(t.type)&&(n=t.stateNode),t=t.child,t!==null))for(wu(t,e,n),t=t.sibling;t!==null;)wu(t,e,n),t=t.sibling}function Qr(t){var e=t.stateNode,n=t.memoizedProps;try{for(var l=t.type,a=e.attributes;a.length;)e.removeAttributeNode(a[0]);wt(e,l,n),e[Ut]=t,e[Zt]=n}catch(u){st(t,t.return,u)}}var We=!1,Mt=!1,Fs=!1,xr=typeof WeakSet=="function"?WeakSet:Set,qt=null;function dg(t,e){if(t=t.containerInfo,vc=li,t=Kf(t),Gi(t)){if("selectionStart"in t)var n={start:t.selectionStart,end:t.selectionEnd};else t:{n=(n=t.ownerDocument)&&n.defaultView||window;var l=n.getSelection&&n.getSelection();if(l&&l.rangeCount!==0){n=l.anchorNode;var a=l.anchorOffset,u=l.focusNode;l=l.focusOffset;try{n.nodeType,u.nodeType}catch{n=null;break t}var c=0,r=-1,d=-1,b=0,O=0,z=t,T=null;e:for(;;){for(var A;z!==n||a!==0&&z.nodeType!==3||(r=c+a),z!==u||l!==0&&z.nodeType!==3||(d=c+l),z.nodeType===3&&(c+=z.nodeValue.length),(A=z.firstChild)!==null;)T=z,z=A;for(;;){if(z===t)break e;if(T===n&&++b===a&&(r=c),T===u&&++O===l&&(d=c),(A=z.nextSibling)!==null)break;z=T,T=z.parentNode}z=A}n=r===-1||d===-1?null:{start:r,end:d}}else n=null}n=n||{start:0,end:0}}else n=null;for(Sc={focusedElem:t,selectionRange:n},li=!1,qt=e;qt!==null;)if(e=qt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,qt=t;else for(;qt!==null;){switch(e=qt,u=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(n=0;n title"))),wt(u,l,n),u[Ut]=t,Ct(u),l=u;break t;case"link":var c=Vh("link","href",a).get(l+(n.href||""));if(c){for(var r=0;rrt&&(c=rt,rt=j,j=c);var S=Xf(r,j),g=Xf(r,rt);if(S&&g&&(A.rangeCount!==1||A.anchorNode!==S.node||A.anchorOffset!==S.offset||A.focusNode!==g.node||A.focusOffset!==g.offset)){var p=z.createRange();p.setStart(S.node,S.offset),A.removeAllRanges(),j>rt?(A.addRange(p),A.extend(g.node,g.offset)):(p.setEnd(g.node,g.offset),A.addRange(p))}}}}for(z=[],A=r;A=A.parentNode;)A.nodeType===1&&z.push({element:A,left:A.scrollLeft,top:A.scrollTop});for(typeof r.focus=="function"&&r.focus(),r=0;rn?32:n,M.T=null,n=lc,lc=null;var u=_n,c=nn;if(zt=0,Ul=_n=null,nn=0,(at&6)!==0)throw Error(f(331));var r=at;if(at|=4,Kr(u.current),Gr(u,u.current,c,n),at=r,Ra(0,!1),ee&&typeof ee.onPostCommitFiberRoot=="function")try{ee.onPostCommitFiberRoot(Zl,u)}catch{}return!0}finally{D.p=a,M.T=l,oh(t,e)}}function hh(t,e,n){e=ge(n,e),e=ws(t.stateNode,e,2),t=vn(t,e,2),t!==null&&(kl(t,2),xe(t))}function st(t,e,n){if(t.tag===3)hh(t,t,n);else for(;e!==null;){if(e.tag===3){hh(e,t,n);break}else if(e.tag===1){var l=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(En===null||!En.has(l))){t=ge(n,t),n=dr(2),l=vn(e,n,2),l!==null&&(yr(n,l,e,t),kl(l,2),xe(l));break}}e=e.return}}function sc(t,e,n){var l=t.pingCache;if(l===null){l=t.pingCache=new mg;var a=new Set;l.set(e,a)}else a=l.get(e),a===void 0&&(a=new Set,l.set(e,a));a.has(n)||(Ps=!0,a.add(n),t=Tg.bind(null,t,e,n),e.then(t,t))}function Tg(t,e,n){var l=t.pingCache;l!==null&&l.delete(e),t.pingedLanes|=t.suspendedLanes&n,t.warmLanes&=~n,dt===t&&(W&n)===n&&(pt===4||pt===3&&(W&62914560)===W&&300>te()-ju?(at&2)===0&&Nl(t,0):tc|=n,Dl===W&&(Dl=0)),xe(t)}function dh(t,e){e===0&&(e=uf()),t=Ln(t,e),t!==null&&(kl(t,e),xe(t))}function Ag(t){var e=t.memoizedState,n=0;e!==null&&(n=e.retryLane),dh(t,n)}function Eg(t,e){var n=0;switch(t.tag){case 31:case 13:var l=t.stateNode,a=t.memoizedState;a!==null&&(n=a.retryLane);break;case 19:l=t.stateNode;break;case 22:l=t.stateNode._retryCache;break;default:throw Error(f(314))}l!==null&&l.delete(e),dh(t,n)}function _g(t,e){return bi(t,e)}var Ku=null,xl=null,cc=!1,ku=!1,fc=!1,Mn=0;function xe(t){t!==xl&&t.next===null&&(xl===null?Ku=xl=t:xl=xl.next=t),ku=!0,cc||(cc=!0,Mg())}function Ra(t,e){if(!fc&&ku){fc=!0;do for(var n=!1,l=Ku;l!==null;){if(t!==0){var a=l.pendingLanes;if(a===0)var u=0;else{var c=l.suspendedLanes,r=l.pingedLanes;u=(1<<31-ne(42|t)+1)-1,u&=a&~(c&~r),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(n=!0,vh(l,u))}else u=W,u=Wa(l,l===dt?u:0,l.cancelPendingCommit!==null||l.timeoutHandle!==-1),(u&3)===0||Kl(l,u)||(n=!0,vh(l,u));l=l.next}while(n);fc=!1}}function Og(){yh()}function yh(){ku=cc=!1;var t=0;Mn!==0&&wg()&&(t=Mn);for(var e=te(),n=null,l=Ku;l!==null;){var a=l.next,u=gh(l,e);u===0?(l.next=null,n===null?Ku=a:n.next=a,a===null&&(xl=n)):(n=l,(t!==0||(u&3)!==0)&&(ku=!0)),l=a}zt!==0&&zt!==5||Ra(t),Mn!==0&&(Mn=0)}function gh(t,e){for(var n=t.suspendedLanes,l=t.pingedLanes,a=t.expirationTimes,u=t.pendingLanes&-62914561;0r)break;var O=d.transferSize,z=d.initiatorType;O&&Oh(z)&&(d=d.responseEnd,c+=O*(d"u"?null:document;function Bh(t,e,n){var l=wl;if(l&&typeof e=="string"&&e){var a=de(e);a='link[rel="'+t+'"][href="'+a+'"]',typeof n=="string"&&(a+='[crossorigin="'+n+'"]'),wh.has(a)||(wh.add(a),t={rel:t,crossOrigin:n,href:e},l.querySelector(a)===null&&(e=l.createElement("link"),wt(e,"link",t),Ct(e),l.head.appendChild(e)))}}function Zg(t){ln.D(t),Bh("dns-prefetch",t,null)}function Kg(t,e){ln.C(t,e),Bh("preconnect",t,e)}function kg(t,e,n){ln.L(t,e,n);var l=wl;if(l&&t&&e){var a='link[rel="preload"][as="'+de(e)+'"]';e==="image"&&n&&n.imageSrcSet?(a+='[imagesrcset="'+de(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(a+='[imagesizes="'+de(n.imageSizes)+'"]')):a+='[href="'+de(t)+'"]';var u=a;switch(e){case"style":u=Bl(t);break;case"script":u=Hl(t)}Te.has(u)||(t=N({rel:"preload",href:e==="image"&&n&&n.imageSrcSet?void 0:t,as:e},n),Te.set(u,t),l.querySelector(a)!==null||e==="style"&&l.querySelector(Da(u))||e==="script"&&l.querySelector(Ua(u))||(e=l.createElement("link"),wt(e,"link",t),Ct(e),l.head.appendChild(e)))}}function Jg(t,e){ln.m(t,e);var n=wl;if(n&&t){var l=e&&typeof e.as=="string"?e.as:"script",a='link[rel="modulepreload"][as="'+de(l)+'"][href="'+de(t)+'"]',u=a;switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=Hl(t)}if(!Te.has(u)&&(t=N({rel:"modulepreload",href:t},e),Te.set(u,t),n.querySelector(a)===null)){switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(Ua(u)))return}l=n.createElement("link"),wt(l,"link",t),Ct(l),n.head.appendChild(l)}}}function $g(t,e,n){ln.S(t,e,n);var l=wl;if(l&&t){var a=ul(l).hoistableStyles,u=Bl(t);e=e||"default";var c=a.get(u);if(!c){var r={loading:0,preload:null};if(c=l.querySelector(Da(u)))r.loading=5;else{t=N({rel:"stylesheet",href:t,"data-precedence":e},n),(n=Te.get(u))&&Oc(t,n);var d=c=l.createElement("link");Ct(d),wt(d,"link",t),d._p=new Promise(function(b,O){d.onload=b,d.onerror=O}),d.addEventListener("load",function(){r.loading|=1}),d.addEventListener("error",function(){r.loading|=2}),r.loading|=4,Iu(c,e,l)}c={type:"stylesheet",instance:c,count:1,state:r},a.set(u,c)}}}function Fg(t,e){ln.X(t,e);var n=wl;if(n&&t){var l=ul(n).hoistableScripts,a=Hl(t),u=l.get(a);u||(u=n.querySelector(Ua(a)),u||(t=N({src:t,async:!0},e),(e=Te.get(a))&&Mc(t,e),u=n.createElement("script"),Ct(u),wt(u,"link",t),n.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},l.set(a,u))}}function Wg(t,e){ln.M(t,e);var n=wl;if(n&&t){var l=ul(n).hoistableScripts,a=Hl(t),u=l.get(a);u||(u=n.querySelector(Ua(a)),u||(t=N({src:t,async:!0,type:"module"},e),(e=Te.get(a))&&Mc(t,e),u=n.createElement("script"),Ct(u),wt(u,"link",t),n.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},l.set(a,u))}}function Hh(t,e,n,l){var a=(a=J.current)?Wu(a):null;if(!a)throw Error(f(446));switch(t){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(e=Bl(n.href),n=ul(a).hoistableStyles,l=n.get(e),l||(l={type:"style",instance:null,count:0,state:null},n.set(e,l)),l):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){t=Bl(n.href);var u=ul(a).hoistableStyles,c=u.get(t);if(c||(a=a.ownerDocument||a,c={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(t,c),(u=a.querySelector(Da(t)))&&!u._p&&(c.instance=u,c.state.loading=5),Te.has(t)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Te.set(t,n),u||Ig(a,t,n,c.state))),e&&l===null)throw Error(f(528,""));return c}if(e&&l!==null)throw Error(f(529,""));return null;case"script":return e=n.async,n=n.src,typeof n=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=Hl(n),n=ul(a).hoistableScripts,l=n.get(e),l||(l={type:"script",instance:null,count:0,state:null},n.set(e,l)),l):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,t))}}function Bl(t){return'href="'+de(t)+'"'}function Da(t){return'link[rel="stylesheet"]['+t+"]"}function jh(t){return N({},t,{"data-precedence":t.precedence,precedence:null})}function Ig(t,e,n,l){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?l.loading=1:(e=t.createElement("link"),l.preload=e,e.addEventListener("load",function(){return l.loading|=1}),e.addEventListener("error",function(){return l.loading|=2}),wt(e,"link",n),Ct(e),t.head.appendChild(e))}function Hl(t){return'[src="'+de(t)+'"]'}function Ua(t){return"script[async]"+t}function Lh(t,e,n){if(e.count++,e.instance===null)switch(e.type){case"style":var l=t.querySelector('style[data-href~="'+de(n.href)+'"]');if(l)return e.instance=l,Ct(l),l;var a=N({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return l=(t.ownerDocument||t).createElement("style"),Ct(l),wt(l,"style",a),Iu(l,n.precedence,t),e.instance=l;case"stylesheet":a=Bl(n.href);var u=t.querySelector(Da(a));if(u)return e.state.loading|=4,e.instance=u,Ct(u),u;l=jh(n),(a=Te.get(a))&&Oc(l,a),u=(t.ownerDocument||t).createElement("link"),Ct(u);var c=u;return c._p=new Promise(function(r,d){c.onload=r,c.onerror=d}),wt(u,"link",l),e.state.loading|=4,Iu(u,n.precedence,t),e.instance=u;case"script":return u=Hl(n.src),(a=t.querySelector(Ua(u)))?(e.instance=a,Ct(a),a):(l=n,(a=Te.get(u))&&(l=N({},n),Mc(l,a)),t=t.ownerDocument||t,a=t.createElement("script"),Ct(a),wt(a,"link",l),t.head.appendChild(a),e.instance=a);case"void":return null;default:throw Error(f(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(l=e.instance,e.state.loading|=4,Iu(l,n.precedence,t));return e.instance}function Iu(t,e,n){for(var l=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),a=l.length?l[l.length-1]:null,u=a,c=0;c title"):null)}function Pg(t,e,n){if(n===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;switch(e.rel){case"stylesheet":return t=e.disabled,typeof e.precedence=="string"&&t==null;default:return!0}case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function Gh(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function tm(t,e,n,l){if(n.type==="stylesheet"&&(typeof l.media!="string"||matchMedia(l.media).matches!==!1)&&(n.state.loading&4)===0){if(n.instance===null){var a=Bl(l.href),u=e.querySelector(Da(a));if(u){e=u._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=ti.bind(t),e.then(t,t)),n.state.loading|=4,n.instance=u,Ct(u);return}u=e.ownerDocument||e,l=jh(l),(a=Te.get(a))&&Oc(l,a),u=u.createElement("link"),Ct(u);var c=u;c._p=new Promise(function(r,d){c.onload=r,c.onerror=d}),wt(u,"link",l),n.instance=u}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(n,e),(e=n.state.preload)&&(n.state.loading&3)===0&&(t.count++,n=ti.bind(t),e.addEventListener("load",n),e.addEventListener("error",n))}}var Rc=0;function em(t,e){return t.stylesheets&&t.count===0&&ni(t,t.stylesheets),0Rc?50:800)+e);return t.unsuspend=n,function(){t.unsuspend=null,clearTimeout(l),clearTimeout(a)}}:null}function ti(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ni(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var ei=null;function ni(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,ei=new Map,e.forEach(nm,t),ei=null,ti.call(t))}function nm(t,e){if(!(e.state.loading&4)){var n=ei.get(t);if(n)var l=n.get(null);else{n=new Map,ei.set(t,n);for(var a=t.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(i){console.error(i)}}return o(),wc.exports=pm(),wc.exports}var Tm=bm(),Be=[],Ee=[],Am=Uint8Array,Lc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";for(var Ll=0,Em=Lc.length;Ll0)throw new Error("Invalid string. Length must be a multiple of 4");var s=o.indexOf("=");s===-1&&(s=i);var f=s===i?0:4-s%4;return[s,f]}function Om(o,i,s){return(i+s)*3/4-s}function Ya(o){var i,s=_m(o),f=s[0],h=s[1],y=new Am(Om(o,f,h)),v=0,q=h>0?f-4:f,E;for(E=0;E>16&255,y[v++]=i>>8&255,y[v++]=i&255;return h===2&&(i=Ee[o.charCodeAt(E)]<<2|Ee[o.charCodeAt(E+1)]>>4,y[v++]=i&255),h===1&&(i=Ee[o.charCodeAt(E)]<<10|Ee[o.charCodeAt(E+1)]<<4|Ee[o.charCodeAt(E+2)]>>2,y[v++]=i>>8&255,y[v++]=i&255),y}function Mm(o){return Be[o>>18&63]+Be[o>>12&63]+Be[o>>6&63]+Be[o&63]}function Rm(o,i,s){for(var f,h=[],y=i;yq?q:v+y));return f===1?(i=o[s-1],h.push(Be[i>>2]+Be[i<<4&63]+"==")):f===2&&(i=(o[s-2]<<8)+o[s-1],h.push(Be[i>>10]+Be[i>>4&63]+Be[i<<2&63]+"=")),h.join("")}function un(o){if(o===void 0)return{};if(!Rd(o))throw new Error(`The arguments to a Convex function must be an object. Received: ${o}`);return o}function zm(o){if(typeof o>"u")throw new Error("Client created with undefined deployment address. If you used an environment variable, check that it's set.");if(typeof o!="string")throw new Error(`Invalid deployment address: found ${o}".`);if(!(o.startsWith("http:")||o.startsWith("https:")))throw new Error(`Invalid deployment address: Must start with "https://" or "http://". Found "${o}".`);try{new URL(o)}catch{throw new Error(`Invalid deployment address: "${o}" is not a valid URL. If you believe this URL is correct, use the \`skipConvexDeploymentUrlCheck\` option to bypass this.`)}if(o.endsWith(".convex.site"))throw new Error(`Invalid deployment address: "${o}" ends with .convex.site, which is used for HTTP Actions. Convex deployment URLs typically end with .convex.cloud? If you believe this URL is correct, use the \`skipConvexDeploymentUrlCheck\` option to bypass this.`)}function Rd(o){const i=typeof o=="object",s=Object.getPrototypeOf(o),f=s===null||s===Object.prototype||s?.constructor?.name==="Object";return i&&f}const zd=!0,Yl=BigInt("-9223372036854775808"),Pc=BigInt("9223372036854775807"),kc=BigInt("0"),Cm=BigInt("8"),qm=BigInt("256");function Cd(o){return Number.isNaN(o)||!Number.isFinite(o)||Object.is(o,-0)}function Dm(o){o>=Cm;return Ga(s)}function Um(o){const i=Ya(o);if(i.byteLength!==8)throw new Error(`Received ${i.byteLength} bytes, expected 8 for $integer`);let s=kc,f=kc;for(const h of i)s+=BigInt(h)*qm**f,f++;return s>Pc&&(s+=Yl+Yl),s}function Nm(o){if(odd)throw new Error(`Field name ${o} exceeds maximum field name length ${dd}.`);if(o.startsWith("$"))throw new Error(`Field name ${o} starts with a '$', which is reserved.`);for(let i=0;i=127)throw new Error(`Field name ${o} has invalid character '${o[i]}': Field names can only contain non-control ASCII characters`)}}function Gl(o){if(o===null||typeof o=="boolean"||typeof o=="number"||typeof o=="string")return o;if(Array.isArray(o))return o.map(f=>Gl(f));if(typeof o!="object")throw new Error(`Unexpected type of ${o}`);const i=Object.entries(o);if(i.length===1){const f=i[0][0];if(f==="$bytes"){if(typeof o.$bytes!="string")throw new Error(`Malformed $bytes field on ${o}`);return Ya(o.$bytes).buffer}if(f==="$integer"){if(typeof o.$integer!="string")throw new Error(`Malformed $integer field on ${o}`);return wm(o.$integer)}if(f==="$float"){if(typeof o.$float!="string")throw new Error(`Malformed $float field on ${o}`);const h=Ya(o.$float);if(h.byteLength!==8)throw new Error(`Received ${h.byteLength} bytes, expected 8 for $float`);const v=new DataView(h.buffer).getFloat64(0,zd);if(!Cd(v))throw new Error(`Float ${v} should be encoded as a number`);return v}if(f==="$set")throw new Error("Received a Set which is no longer supported as a Convex type.");if(f==="$map")throw new Error("Received a Map which is no longer supported as a Convex type.")}const s={};for(const[f,h]of Object.entries(o))qd(f),s[f]=Gl(h);return s}const yd=16384;function La(o){const i=JSON.stringify(o,(s,f)=>f===void 0?"undefined":typeof f=="bigint"?`${f.toString()}n`:f);if(i.length>yd){const s="[...truncated]";let f=yd-s.length;const h=i.codePointAt(f-1);return h!==void 0&&h>65535&&(f-=1),i.substring(0,f)+s}return i}function Jc(o,i,s,f){if(o===void 0){const v=s&&` (present at path ${s} in original object ${La(i)})`;throw new Error(`undefined is not a valid Convex value${v}. To learn about Convex's supported types, see https://docs.convex.dev/using/types.`)}if(o===null)return o;if(typeof o=="bigint"){if(oJc(v,i,s+`[${q}]`));if(o instanceof Set)throw new Error(Vc(s,"Set",[...o],i));if(o instanceof Map)throw new Error(Vc(s,"Map",[...o],i));if(!Rd(o)){const v=o?.constructor?.name,q=v?`${v} `:"";throw new Error(Vc(s,q,o,i))}const h={},y=Object.entries(o);y.sort(([v,q],[E,_])=>v===E?0:vi in o?Bm(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Yc=(o,i,s)=>Hm(o,typeof i!="symbol"?i+"":i,s),gd,md;const jm=Symbol.for("ConvexError");class $c extends(md=Error,gd=jm,md){constructor(i){super(typeof i=="string"?i:La(i)),Yc(this,"name","ConvexError"),Yc(this,"data"),Yc(this,gd,!0),this.data=i}}const Dd=()=>Array.from({length:4},()=>0);Dd();Dd();const vd="1.31.0";var Lm=Object.defineProperty,Vm=(o,i,s)=>i in o?Lm(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Sd=(o,i,s)=>Vm(o,typeof i!="symbol"?i+"":i,s);const Ym="color:rgb(0, 145, 255)";function Ud(o){switch(o){case"query":return"Q";case"mutation":return"M";case"action":return"A";case"any":return"?"}}class Nd{constructor(i){Sd(this,"_onLogLineFuncs"),Sd(this,"_verbose"),this._onLogLineFuncs={},this._verbose=i.verbose}addLogLineListener(i){let s=Math.random().toString(36).substring(2,15);for(let f=0;f<10&&this._onLogLineFuncs[s]!==void 0;f++)s=Math.random().toString(36).substring(2,15);return this._onLogLineFuncs[s]=i,()=>{delete this._onLogLineFuncs[s]}}logVerbose(...i){if(this._verbose)for(const s of Object.values(this._onLogLineFuncs))s("debug",`${new Date().toISOString()}`,...i)}log(...i){for(const s of Object.values(this._onLogLineFuncs))s("info",...i)}warn(...i){for(const s of Object.values(this._onLogLineFuncs))s("warn",...i)}error(...i){for(const s of Object.values(this._onLogLineFuncs))s("error",...i)}}function Qd(o){const i=new Nd(o);return i.addLogLineListener((s,...f)=>{switch(s){case"debug":console.debug(...f);break;case"info":console.log(...f);break;case"warn":console.warn(...f);break;case"error":console.error(...f);break;default:console.log(...f)}}),i}function xd(o){return new Nd(o)}function yi(o,i,s,f,h){const y=Ud(s);if(typeof h=="object"&&(h=`ConvexError ${JSON.stringify(h.errorData,null,2)}`),i==="info"){const v=h.match(/^\[.*?\] /);if(v===null){o.error(`[CONVEX ${y}(${f})] Could not parse console.log`);return}const q=h.slice(1,v[0].length-2),E=h.slice(v[0].length);o.log(`%c[CONVEX ${y}(${f})] [${q}]`,Ym,E)}else o.error(`[CONVEX ${y}(${f})] ${h}`)}function Gm(o,i){const s=`[CONVEX FATAL ERROR] ${i}`;return o.error(s),new Error(s)}function Vl(o,i,s){return`[CONVEX ${Ud(o)}(${i})] ${s.errorMessage} - Called by client`}function Fc(o,i){return i.data=o.errorData,i}function tl(o){const i=o.split(":");let s,f;return i.length===1?(s=i[0],f="default"):(s=i.slice(0,i.length-1).join(":"),f=i[i.length-1]),s.endsWith(".js")&&(s=s.slice(0,-3)),`${s}:${f}`}function Pn(o,i){return JSON.stringify({udfPath:tl(o),args:He(i)})}function pd(o,i,s){const{initialNumItems:f,id:h}=s;return JSON.stringify({type:"paginated",udfPath:tl(o),args:He(i),options:He({initialNumItems:f,id:h})})}var Xm=Object.defineProperty,Zm=(o,i,s)=>i in o?Xm(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,we=(o,i,s)=>Zm(o,typeof i!="symbol"?i+"":i,s);class Km{constructor(){we(this,"nextQueryId"),we(this,"querySetVersion"),we(this,"querySet"),we(this,"queryIdToToken"),we(this,"identityVersion"),we(this,"auth"),we(this,"outstandingQueriesOlderThanRestart"),we(this,"outstandingAuthOlderThanRestart"),we(this,"paused"),we(this,"pendingQuerySetModifications"),this.nextQueryId=0,this.querySetVersion=0,this.identityVersion=0,this.querySet=new Map,this.queryIdToToken=new Map,this.outstandingQueriesOlderThanRestart=new Set,this.outstandingAuthOlderThanRestart=!1,this.paused=!1,this.pendingQuerySetModifications=new Map}hasSyncedPastLastReconnect(){return this.outstandingQueriesOlderThanRestart.size===0&&!this.outstandingAuthOlderThanRestart}markAuthCompletion(){this.outstandingAuthOlderThanRestart=!1}subscribe(i,s,f,h){const y=tl(i),v=Pn(y,s),q=this.querySet.get(v);if(q!==void 0)return q.numSubscribers+=1,{queryToken:v,modification:null,unsubscribe:()=>this.removeSubscriber(v)};{const E=this.nextQueryId++,_={id:E,canonicalizedUdfPath:y,args:s,numSubscribers:1,journal:f,componentPath:h};this.querySet.set(v,_),this.queryIdToToken.set(E,v);const H=this.querySetVersion,N=this.querySetVersion+1,et={type:"Add",queryId:E,udfPath:y,args:[He(s)],journal:f,componentPath:h};return this.paused?this.pendingQuerySetModifications.set(E,et):this.querySetVersion=N,{queryToken:v,modification:{type:"ModifyQuerySet",baseVersion:H,newVersion:N,modifications:[et]},unsubscribe:()=>this.removeSubscriber(v)}}}transition(i){for(const s of i.modifications)switch(s.type){case"QueryUpdated":case"QueryFailed":{this.outstandingQueriesOlderThanRestart.delete(s.queryId);const f=s.journal;if(f!==void 0){const h=this.queryIdToToken.get(s.queryId);h!==void 0&&(this.querySet.get(h).journal=f)}break}case"QueryRemoved":{this.outstandingQueriesOlderThanRestart.delete(s.queryId);break}default:throw new Error(`Invalid modification ${s.type}`)}}queryId(i,s){const f=tl(i),h=Pn(f,s),y=this.querySet.get(h);return y!==void 0?y.id:null}isCurrentOrNewerAuthVersion(i){return i>=this.identityVersion}getAuth(){return this.auth}setAuth(i){this.auth={tokenType:"User",value:i};const s=this.identityVersion;return this.paused||(this.identityVersion=s+1),{type:"Authenticate",baseVersion:s,...this.auth}}setAdminAuth(i,s){const f={tokenType:"Admin",value:i,impersonating:s};this.auth=f;const h=this.identityVersion;return this.paused||(this.identityVersion=h+1),{type:"Authenticate",baseVersion:h,...f}}clearAuth(){this.auth=void 0,this.markAuthCompletion();const i=this.identityVersion;return this.paused||(this.identityVersion=i+1),{type:"Authenticate",tokenType:"None",baseVersion:i}}hasAuth(){return!!this.auth}isNewAuth(i){return this.auth?.value!==i}queryPath(i){const s=this.queryIdToToken.get(i);return s?this.querySet.get(s).canonicalizedUdfPath:null}queryArgs(i){const s=this.queryIdToToken.get(i);return s?this.querySet.get(s).args:null}queryToken(i){return this.queryIdToToken.get(i)??null}queryJournal(i){return this.querySet.get(i)?.journal}restart(i){this.unpause(),this.outstandingQueriesOlderThanRestart.clear();const s=[];for(const y of this.querySet.values()){const v={type:"Add",queryId:y.id,udfPath:y.canonicalizedUdfPath,args:[He(y.args)],journal:y.journal,componentPath:y.componentPath};s.push(v),i.has(y.id)||this.outstandingQueriesOlderThanRestart.add(y.id)}this.querySetVersion=1;const f={type:"ModifyQuerySet",baseVersion:0,newVersion:1,modifications:s};if(!this.auth)return this.identityVersion=0,[f,void 0];this.outstandingAuthOlderThanRestart=!0;const h={type:"Authenticate",baseVersion:0,...this.auth};return this.identityVersion=1,[f,h]}pause(){this.paused=!0}resume(){const i=this.pendingQuerySetModifications.size>0?{type:"ModifyQuerySet",baseVersion:this.querySetVersion,newVersion:++this.querySetVersion,modifications:Array.from(this.pendingQuerySetModifications.values())}:void 0,s=this.auth!==void 0?{type:"Authenticate",baseVersion:this.identityVersion++,...this.auth}:void 0;return this.unpause(),[i,s]}unpause(){this.paused=!1,this.pendingQuerySetModifications.clear()}removeSubscriber(i){const s=this.querySet.get(i);if(s.numSubscribers>1)return s.numSubscribers-=1,null;{this.querySet.delete(i),this.queryIdToToken.delete(s.id),this.outstandingQueriesOlderThanRestart.delete(s.id);const f=this.querySetVersion,h=this.querySetVersion+1,y={type:"Remove",queryId:s.id};return this.paused?this.pendingQuerySetModifications.has(s.id)?this.pendingQuerySetModifications.delete(s.id):this.pendingQuerySetModifications.set(s.id,y):this.querySetVersion=h,{type:"ModifyQuerySet",baseVersion:f,newVersion:h,modifications:[y]}}}}var km=Object.defineProperty,Jm=(o,i,s)=>i in o?km(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,oi=(o,i,s)=>Jm(o,typeof i!="symbol"?i+"":i,s);class $m{constructor(i,s){this.logger=i,this.markConnectionStateDirty=s,oi(this,"inflightRequests"),oi(this,"requestsOlderThanRestart"),oi(this,"inflightMutationsCount",0),oi(this,"inflightActionsCount",0),this.inflightRequests=new Map,this.requestsOlderThanRestart=new Set}request(i,s){const f=new Promise(h=>{const y=s?"Requested":"NotSent";this.inflightRequests.set(i.requestId,{message:i,status:{status:y,requestedAt:new Date,onResult:h}}),i.type==="Mutation"?this.inflightMutationsCount++:i.type==="Action"&&this.inflightActionsCount++});return this.markConnectionStateDirty(),f}onResponse(i){const s=this.inflightRequests.get(i.requestId);if(s===void 0||s.status.status==="Completed")return null;const f=s.message.type==="Mutation"?"mutation":"action",h=s.message.udfPath;for(const E of i.logLines)yi(this.logger,"info",f,h,E);const y=s.status;let v,q;if(i.success)v={success:!0,logLines:i.logLines,value:Gl(i.result)},q=()=>y.onResult(v);else{const E=i.result,{errorData:_}=i;yi(this.logger,"error",f,h,E),v={success:!1,errorMessage:E,errorData:_!==void 0?Gl(_):void 0,logLines:i.logLines},q=()=>y.onResult(v)}return i.type==="ActionResponse"||!i.success?(q(),this.inflightRequests.delete(i.requestId),this.requestsOlderThanRestart.delete(i.requestId),s.message.type==="Action"?this.inflightActionsCount--:s.message.type==="Mutation"&&this.inflightMutationsCount--,this.markConnectionStateDirty(),{requestId:i.requestId,result:v}):(s.status={status:"Completed",result:v,ts:i.ts,onResolve:q},null)}removeCompleted(i){const s=new Map;for(const[f,h]of this.inflightRequests.entries()){const y=h.status;y.status==="Completed"&&y.ts.lessThanOrEqual(i)&&(y.onResolve(),s.set(f,y.result),h.message.type==="Mutation"?this.inflightMutationsCount--:h.message.type==="Action"&&this.inflightActionsCount--,this.inflightRequests.delete(f),this.requestsOlderThanRestart.delete(f))}return s.size>0&&this.markConnectionStateDirty(),s}restart(){this.requestsOlderThanRestart=new Set(this.inflightRequests.keys());const i=[];for(const[s,f]of this.inflightRequests){if(f.status.status==="NotSent"){f.status.status="Requested",i.push(f.message);continue}if(f.message.type==="Mutation")i.push(f.message);else if(f.message.type==="Action"){if(this.inflightRequests.delete(s),this.requestsOlderThanRestart.delete(s),this.inflightActionsCount--,f.status.status==="Completed")throw new Error("Action should never be in 'Completed' state");f.status.onResult({success:!1,errorMessage:"Connection lost while action was in flight",logLines:[]})}}return this.markConnectionStateDirty(),i}resume(){const i=[];for(const[,s]of this.inflightRequests)if(s.status.status==="NotSent"){s.status.status="Requested",i.push(s.message);continue}return i}hasIncompleteRequests(){for(const i of this.inflightRequests.values())if(i.status.status==="Requested")return!0;return!1}hasInflightRequests(){return this.inflightRequests.size>0}hasSyncedPastLastReconnect(){return this.requestsOlderThanRestart.size===0}timeOfOldestInflightRequest(){if(this.inflightRequests.size===0)return null;let i=Date.now();for(const s of this.inflightRequests.values())s.status.status!=="Completed"&&s.status.requestedAt.getTime()i in o?ev(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,gi=(o,i,s)=>nv(o,typeof i!="symbol"?i+"":i,s);class Za{constructor(i){gi(this,"queryResults"),gi(this,"modifiedQueries"),this.queryResults=i,this.modifiedQueries=[]}getQuery(i,...s){const f=un(s[0]),h=_e(i),y=this.queryResults.get(Pn(h,f));if(y!==void 0)return Za.queryValue(y.result)}getAllQueries(i){const s=[],f=_e(i);for(const h of this.queryResults.values())h.udfPath===tl(f)&&s.push({args:h.args,value:Za.queryValue(h.result)});return s}setQuery(i,s,f){const h=un(s),y=_e(i),v=Pn(y,h);let q;f===void 0?q=void 0:q={success:!0,value:f,logLines:[]};const E={udfPath:y,args:h,result:q};this.queryResults.set(v,E),this.modifiedQueries.push(v)}static queryValue(i){if(i!==void 0)return i.success?i.value:void 0}}class lv{constructor(){gi(this,"queryResults"),gi(this,"optimisticUpdates"),this.queryResults=new Map,this.optimisticUpdates=[]}ingestQueryResultsFromServer(i,s){this.optimisticUpdates=this.optimisticUpdates.filter(v=>!s.has(v.mutationId));const f=this.queryResults;this.queryResults=new Map(i);const h=new Za(this.queryResults);for(const v of this.optimisticUpdates)v.update(h);const y=[];for(const[v,q]of this.queryResults){const E=f.get(v);(E===void 0||E.result!==q.result)&&y.push(v)}return y}applyOptimisticUpdate(i,s){this.optimisticUpdates.push({update:i,mutationId:s});const f=new Za(this.queryResults);return i(f),f.modifiedQueries}rawQueryResult(i){const s=this.queryResults.get(i);if(s!==void 0)return s.result}queryResult(i){const s=this.queryResults.get(i);if(s===void 0)return;const f=s.result;if(f!==void 0){if(f.success)return f.value;throw f.errorData!==void 0?Fc(f,new $c(Vl("query",s.udfPath,f))):new Error(Vl("query",s.udfPath,f))}}hasQueryResult(i){return this.queryResults.get(i)!==void 0}queryLogs(i){return this.queryResults.get(i)?.result?.logLines}}var av=Object.defineProperty,uv=(o,i,s)=>i in o?av(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Gc=(o,i,s)=>uv(o,typeof i!="symbol"?i+"":i,s);class It{constructor(i,s){Gc(this,"low"),Gc(this,"high"),Gc(this,"__isUnsignedLong__"),this.low=i|0,this.high=s|0,this.__isUnsignedLong__=!0}static isLong(i){return(i&&i.__isUnsignedLong__)===!0}static fromBytesLE(i){return new It(i[0]|i[1]<<8|i[2]<<16|i[3]<<24,i[4]|i[5]<<8|i[6]<<16|i[7]<<24)}toBytesLE(){const i=this.high,s=this.low;return[s&255,s>>>8&255,s>>>16&255,s>>>24,i&255,i>>>8&255,i>>>16&255,i>>>24]}static fromNumber(i){return isNaN(i)||i<0?bd:i>=iv?sv:new It(i%Va|0,i/Va|0)}toString(){return(BigInt(this.high)*BigInt(Va)+BigInt(this.low)).toString()}equals(i){return It.isLong(i)||(i=It.fromValue(i)),this.high>>>31===1&&i.high>>>31===1?!1:this.high===i.high&&this.low===i.low}notEquals(i){return!this.equals(i)}comp(i){return It.isLong(i)||(i=It.fromValue(i)),this.equals(i)?0:i.high>>>0>this.high>>>0||i.high===this.high&&i.low>>>0>this.low>>>0?-1:1}lessThanOrEqual(i){return this.comp(i)<=0}static fromValue(i){return typeof i=="number"?It.fromNumber(i):new It(i.low,i.high)}}const bd=new It(0,0),Td=65536,Va=Td*Td,iv=Va*Va,sv=new It(-1,-1);var cv=Object.defineProperty,fv=(o,i,s)=>i in o?cv(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,ri=(o,i,s)=>fv(o,typeof i!="symbol"?i+"":i,s);class Ad{constructor(i,s){ri(this,"version"),ri(this,"remoteQuerySet"),ri(this,"queryPath"),ri(this,"logger"),this.version={querySet:0,ts:It.fromNumber(0),identity:0},this.remoteQuerySet=new Map,this.queryPath=i,this.logger=s}transition(i){const s=i.startVersion;if(this.version.querySet!==s.querySet||this.version.ts.notEquals(s.ts)||this.version.identity!==s.identity)throw new Error(`Invalid start version: ${s.ts.toString()}:${s.querySet}:${s.identity}, transitioning from ${this.version.ts.toString()}:${this.version.querySet}:${this.version.identity}`);for(const f of i.modifications)switch(f.type){case"QueryUpdated":{const h=this.queryPath(f.queryId);if(h)for(const v of f.logLines)yi(this.logger,"info","query",h,v);const y=Gl(f.value??null);this.remoteQuerySet.set(f.queryId,{success:!0,value:y,logLines:f.logLines});break}case"QueryFailed":{const h=this.queryPath(f.queryId);if(h)for(const v of f.logLines)yi(this.logger,"info","query",h,v);const{errorData:y}=f;this.remoteQuerySet.set(f.queryId,{success:!1,errorMessage:f.errorMessage,errorData:y!==void 0?Gl(y):void 0,logLines:f.logLines});break}case"QueryRemoved":{this.remoteQuerySet.delete(f.queryId);break}default:throw new Error(`Invalid modification ${f.type}`)}this.version=i.endVersion}remoteQueryResults(){return this.remoteQuerySet}timestamp(){return this.version.ts}}function Xc(o){const i=Ya(o);return It.fromBytesLE(Array.from(i))}function ov(o){const i=new Uint8Array(o.toBytesLE());return Ga(i)}function Ed(o){switch(o.type){case"FatalError":case"AuthError":case"ActionResponse":case"TransitionChunk":case"Ping":return{...o};case"MutationResponse":return o.success?{...o,ts:Xc(o.ts)}:{...o};case"Transition":return{...o,startVersion:{...o.startVersion,ts:Xc(o.startVersion.ts)},endVersion:{...o.endVersion,ts:Xc(o.endVersion.ts)}}}}function rv(o){switch(o.type){case"Authenticate":case"ModifyQuerySet":case"Mutation":case"Action":case"Event":return{...o};case"Connect":return o.maxObservedTimestamp!==void 0?{...o,maxObservedTimestamp:ov(o.maxObservedTimestamp)}:{...o,maxObservedTimestamp:void 0}}}var hv=Object.defineProperty,dv=(o,i,s)=>i in o?hv(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Bt=(o,i,s)=>dv(o,typeof i!="symbol"?i+"":i,s);const yv=1e3,gv=1001,mv=1005,vv=4040;let hi;function di(){return hi===void 0&&(hi=Date.now()),typeof performance>"u"||!performance.now?Date.now():Math.round(hi+performance.now())}function _d(){return`t=${Math.round((di()-hi)/100)/10}s`}const Hd={InternalServerError:{timeout:1e3},SubscriptionsWorkerFullError:{timeout:3e3},TooManyConcurrentRequests:{timeout:3e3},CommitterFullError:{timeout:3e3},AwsTooManyRequestsException:{timeout:3e3},ExecuteFullError:{timeout:3e3},SystemTimeoutError:{timeout:3e3},ExpiredInQueue:{timeout:3e3},VectorIndexesUnavailable:{timeout:1e3},SearchIndexesUnavailable:{timeout:1e3},TableSummariesUnavailable:{timeout:1e3},VectorIndexTooLarge:{timeout:3e3},SearchIndexTooLarge:{timeout:3e3},TooManyWritesInTimePeriod:{timeout:3e3}};function Sv(o){if(o===void 0)return"Unknown";for(const i of Object.keys(Hd))if(o.startsWith(i))return i;return"Unknown"}class pv{constructor(i,s,f,h,y,v){this.markConnectionStateDirty=y,this.debug=v,Bt(this,"socket"),Bt(this,"connectionCount"),Bt(this,"_hasEverConnected",!1),Bt(this,"lastCloseReason"),Bt(this,"transitionChunkBuffer",null),Bt(this,"defaultInitialBackoff"),Bt(this,"maxBackoff"),Bt(this,"retries"),Bt(this,"serverInactivityThreshold"),Bt(this,"reconnectDueToServerInactivityTimeout"),Bt(this,"uri"),Bt(this,"onOpen"),Bt(this,"onResume"),Bt(this,"onMessage"),Bt(this,"webSocketConstructor"),Bt(this,"logger"),Bt(this,"onServerDisconnectError"),this.webSocketConstructor=f,this.socket={state:"disconnected"},this.connectionCount=0,this.lastCloseReason="InitialConnect",this.defaultInitialBackoff=1e3,this.maxBackoff=16e3,this.retries=0,this.serverInactivityThreshold=6e4,this.reconnectDueToServerInactivityTimeout=null,this.uri=i,this.onOpen=s.onOpen,this.onResume=s.onResume,this.onMessage=s.onMessage,this.onServerDisconnectError=s.onServerDisconnectError,this.logger=h,this.connect()}setSocketState(i){this.socket=i,this._logVerbose(`socket state changed: ${this.socket.state}, paused: ${"paused"in this.socket?this.socket.paused:void 0}`),this.markConnectionStateDirty()}assembleTransition(i){if(i.partNumber<0||i.partNumber>=i.totalParts||i.totalParts===0||this.transitionChunkBuffer&&(this.transitionChunkBuffer.totalParts!==i.totalParts||this.transitionChunkBuffer.transitionId!==i.transitionId))throw this.transitionChunkBuffer=null,new Error("Invalid TransitionChunk");if(this.transitionChunkBuffer===null&&(this.transitionChunkBuffer={chunks:[],totalParts:i.totalParts,transitionId:i.transitionId}),i.partNumber!==this.transitionChunkBuffer.chunks.length){const s=this.transitionChunkBuffer.chunks.length;throw this.transitionChunkBuffer=null,new Error(`TransitionChunk received out of order: expected part ${s}, got ${i.partNumber}`)}if(this.transitionChunkBuffer.chunks.push(i.chunk),this.transitionChunkBuffer.chunks.length===i.totalParts){const s=this.transitionChunkBuffer.chunks.join("");this.transitionChunkBuffer=null;const f=Ed(JSON.parse(s));if(f.type!=="Transition")throw new Error(`Expected Transition, got ${f.type} after assembling chunks`);return f}return null}connect(){if(this.socket.state==="terminated")return;if(this.socket.state!=="disconnected"&&this.socket.state!=="stopped")throw new Error("Didn't start connection from disconnected state: "+this.socket.state);const i=new this.webSocketConstructor(this.uri);this._logVerbose("constructed WebSocket"),this.setSocketState({state:"connecting",ws:i,paused:"no"}),this.resetServerInactivityTimeout(),i.onopen=()=>{if(this.logger.logVerbose("begin ws.onopen"),this.socket.state!=="connecting")throw new Error("onopen called with socket not in connecting state");this.setSocketState({state:"ready",ws:i,paused:this.socket.paused==="yes"?"uninitialized":"no"}),this.resetServerInactivityTimeout(),this.socket.paused==="no"&&(this._hasEverConnected=!0,this.onOpen({connectionCount:this.connectionCount,lastCloseReason:this.lastCloseReason,clientTs:di()})),this.lastCloseReason!=="InitialConnect"&&(this.lastCloseReason?this.logger.log("WebSocket reconnected at",_d(),"after disconnect due to",this.lastCloseReason):this.logger.log("WebSocket reconnected at",_d())),this.connectionCount+=1,this.lastCloseReason=null},i.onerror=s=>{this.transitionChunkBuffer=null;const f=s.message;f&&this.logger.log(`WebSocket error message: ${f}`)},i.onmessage=s=>{this.resetServerInactivityTimeout();const f=s.data.length;let h=Ed(JSON.parse(s.data));if(this._logVerbose(`received ws message with type ${h.type}`),h.type==="Ping")return;if(h.type==="TransitionChunk"){const v=this.assembleTransition(h);if(!v)return;h=v,this._logVerbose(`assembled full ws message of type ${h.type}`)}this.transitionChunkBuffer!==null&&(this.transitionChunkBuffer=null,this.logger.log(`Received unexpected ${h.type} while buffering TransitionChunks`)),h.type==="Transition"&&this.reportLargeTransition({messageLength:f,transition:h}),this.onMessage(h).hasSyncedPastLastReconnect&&(this.retries=0,this.markConnectionStateDirty())},i.onclose=s=>{if(this._logVerbose("begin ws.onclose"),this.transitionChunkBuffer=null,this.lastCloseReason===null&&(this.lastCloseReason=s.reason||`closed with code ${s.code}`),s.code!==yv&&s.code!==gv&&s.code!==mv&&s.code!==vv){let h=`WebSocket closed with code ${s.code}`;s.reason&&(h+=`: ${s.reason}`),this.logger.log(h),this.onServerDisconnectError&&s.reason&&this.onServerDisconnectError(h)}const f=Sv(s.reason);this.scheduleReconnect(f)}}socketState(){return this.socket.state}sendMessage(i){const s={type:i.type,...i.type==="Authenticate"&&i.tokenType==="User"?{value:`...${i.value.slice(-7)}`}:{}};if(this.socket.state==="ready"&&this.socket.paused==="no"){const f=rv(i),h=JSON.stringify(f);let y=!1;try{this.socket.ws.send(h),y=!0}catch(v){this.logger.log(`Failed to send message on WebSocket, reconnecting: ${v}`),this.closeAndReconnect("FailedToSendMessage")}return this._logVerbose(`${y?"sent":"failed to send"} message with type ${i.type}: ${JSON.stringify(s)}`),!0}return this._logVerbose(`message not sent (socket state: ${this.socket.state}, paused: ${"paused"in this.socket?this.socket.paused:void 0}): ${JSON.stringify(s)}`),!1}resetServerInactivityTimeout(){this.socket.state!=="terminated"&&(this.reconnectDueToServerInactivityTimeout!==null&&(clearTimeout(this.reconnectDueToServerInactivityTimeout),this.reconnectDueToServerInactivityTimeout=null),this.reconnectDueToServerInactivityTimeout=setTimeout(()=>{this.closeAndReconnect("InactiveServer")},this.serverInactivityThreshold))}scheduleReconnect(i){this.socket={state:"disconnected"};const s=this.nextBackoff(i);this.markConnectionStateDirty(),this.logger.log(`Attempting reconnect in ${Math.round(s)}ms`),setTimeout(()=>this.connect(),s)}closeAndReconnect(i){switch(this._logVerbose(`begin closeAndReconnect with reason ${i}`),this.socket.state){case"disconnected":case"terminated":case"stopped":return;case"connecting":case"ready":{this.lastCloseReason=i,this.close(),this.scheduleReconnect("client");return}default:this.socket}}close(){switch(this.transitionChunkBuffer=null,this.socket.state){case"disconnected":case"terminated":case"stopped":return Promise.resolve();case"connecting":{const i=this.socket.ws;return i.onmessage=s=>{this._logVerbose("Ignoring message received after close")},new Promise(s=>{i.onclose=()=>{this._logVerbose("Closed after connecting"),s()},i.onopen=()=>{this._logVerbose("Opened after connecting"),i.close()}})}case"ready":{this._logVerbose("ws.close called");const i=this.socket.ws;i.onmessage=f=>{this._logVerbose("Ignoring message received after close")};const s=new Promise(f=>{i.onclose=()=>{f()}});return i.close(),s}default:return this.socket,Promise.resolve()}}terminate(){switch(this.reconnectDueToServerInactivityTimeout&&clearTimeout(this.reconnectDueToServerInactivityTimeout),this.socket.state){case"terminated":case"stopped":case"disconnected":case"connecting":case"ready":{const i=this.close();return this.setSocketState({state:"terminated"}),i}default:throw this.socket,new Error(`Invalid websocket state: ${this.socket.state}`)}}stop(){switch(this.socket.state){case"terminated":return Promise.resolve();case"connecting":case"stopped":case"disconnected":case"ready":{const i=this.close();return this.socket={state:"stopped"},i}default:return this.socket,Promise.resolve()}}tryRestart(){switch(this.socket.state){case"stopped":break;case"terminated":case"connecting":case"ready":case"disconnected":this.logger.logVerbose("Restart called without stopping first");return;default:this.socket}this.connect()}pause(){switch(this.socket.state){case"disconnected":case"stopped":case"terminated":return;case"connecting":case"ready":{this.socket={...this.socket,paused:"yes"};return}default:{this.socket;return}}}resume(){switch(this.socket.state){case"connecting":this.socket={...this.socket,paused:"no"};return;case"ready":this.socket.paused==="uninitialized"?(this.socket={...this.socket,paused:"no"},this.onOpen({connectionCount:this.connectionCount,lastCloseReason:this.lastCloseReason,clientTs:di()})):this.socket.paused==="yes"&&(this.socket={...this.socket,paused:"no"},this.onResume());return;case"terminated":case"stopped":case"disconnected":return;default:this.socket}this.connect()}connectionState(){return{isConnected:this.socket.state==="ready",hasEverConnected:this._hasEverConnected,connectionCount:this.connectionCount,connectionRetries:this.retries}}_logVerbose(i){this.logger.logVerbose(i)}nextBackoff(i){const f=(i==="client"?100:i==="Unknown"?this.defaultInitialBackoff:Hd[i].timeout)*Math.pow(2,this.retries);this.retries+=1;const h=Math.min(f,this.maxBackoff),y=h*(Math.random()-.5);return h+y}reportLargeTransition({transition:i,messageLength:s}){if(i.clientClockSkew===void 0||i.serverTs===void 0)return;const f=di()-i.clientClockSkew-i.serverTs/1e6,h=`${Math.round(f)}ms`,y=`${Math.round(s/1e4)/100}MB`,v=s/(f/1e3),q=`${Math.round(v/1e4)/100}MB per second`;this._logVerbose(`received ${y} transition in ${h} at ${q}`),s>2e7?this.logger.log(`received query results totaling more that 20MB (${y}) which will take a long time to download on slower connections`):f>2e4&&this.logger.log(`received query results totaling ${y} which took more than 20s to arrive (${h})`),this.debug&&this.sendMessage({type:"Event",eventType:"ClientReceivedTransition",event:{transitionTransitTime:f,messageLength:s}})}}function bv(){return Tv()}function Tv(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,o=>{const i=Math.random()*16|0;return(o==="x"?i:i&3|8).toString(16)})}class ja extends Error{}ja.prototype.name="InvalidTokenError";function Av(o){return decodeURIComponent(atob(o).replace(/(.)/g,(i,s)=>{let f=s.charCodeAt(0).toString(16).toUpperCase();return f.length<2&&(f="0"+f),"%"+f}))}function Ev(o){let i=o.replace(/-/g,"+").replace(/_/g,"/");switch(i.length%4){case 0:break;case 2:i+="==";break;case 3:i+="=";break;default:throw new Error("base64 string is not of the correct length")}try{return Av(i)}catch{return atob(i)}}function jd(o,i){if(typeof o!="string")throw new ja("Invalid token specified: must be a string");i||(i={});const s=i.header===!0?0:1,f=o.split(".")[s];if(typeof f!="string")throw new ja(`Invalid token specified: missing part #${s+1}`);let h;try{h=Ev(f)}catch(y){throw new ja(`Invalid token specified: invalid base64 for part #${s+1} (${y.message})`)}try{return JSON.parse(h)}catch(y){throw new ja(`Invalid token specified: invalid json for part #${s+1} (${y.message})`)}}var _v=Object.defineProperty,Ov=(o,i,s)=>i in o?_v(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Ae=(o,i,s)=>Ov(o,typeof i!="symbol"?i+"":i,s);const Mv=20*24*60*60*1e3,Od=2;class Rv{constructor(i,s,f){Ae(this,"authState",{state:"noAuth"}),Ae(this,"configVersion",0),Ae(this,"syncState"),Ae(this,"authenticate"),Ae(this,"stopSocket"),Ae(this,"tryRestartSocket"),Ae(this,"pauseSocket"),Ae(this,"resumeSocket"),Ae(this,"clearAuth"),Ae(this,"logger"),Ae(this,"refreshTokenLeewaySeconds"),Ae(this,"tokenConfirmationAttempts",0),this.syncState=i,this.authenticate=s.authenticate,this.stopSocket=s.stopSocket,this.tryRestartSocket=s.tryRestartSocket,this.pauseSocket=s.pauseSocket,this.resumeSocket=s.resumeSocket,this.clearAuth=s.clearAuth,this.logger=f.logger,this.refreshTokenLeewaySeconds=f.refreshTokenLeewaySeconds}async setConfig(i,s){this.resetAuthState(),this._logVerbose("pausing WS for auth token fetch"),this.pauseSocket();const f=await this.fetchTokenAndGuardAgainstRace(i,{forceRefreshToken:!1});f.isFromOutdatedConfig||(f.value?(this.setAuthState({state:"waitingForServerConfirmationOfCachedToken",config:{fetchToken:i,onAuthChange:s},hasRetried:!1}),this.authenticate(f.value)):(this.setAuthState({state:"initialRefetch",config:{fetchToken:i,onAuthChange:s}}),await this.refetchToken()),this._logVerbose("resuming WS after auth token fetch"),this.resumeSocket())}onTransition(i){if(this.syncState.isCurrentOrNewerAuthVersion(i.endVersion.identity)&&!(i.endVersion.identity<=i.startVersion.identity)){if(this.authState.state==="waitingForServerConfirmationOfCachedToken"){this._logVerbose("server confirmed auth token is valid"),this.refetchToken(),this.authState.config.onAuthChange(!0);return}this.authState.state==="waitingForServerConfirmationOfFreshToken"&&(this._logVerbose("server confirmed new auth token is valid"),this.scheduleTokenRefetch(this.authState.token),this.tokenConfirmationAttempts=0,this.authState.hadAuth||this.authState.config.onAuthChange(!0))}}onAuthError(i){if(i.authUpdateAttempted===!1&&(this.authState.state==="waitingForServerConfirmationOfFreshToken"||this.authState.state==="waitingForServerConfirmationOfCachedToken")){this._logVerbose("ignoring non-auth token expired error");return}const{baseVersion:s}=i;if(!this.syncState.isCurrentOrNewerAuthVersion(s+1)){this._logVerbose("ignoring auth error for previous auth attempt");return}this.tryToReauthenticate(i)}async tryToReauthenticate(i){if(this._logVerbose(`attempting to reauthenticate: ${i.error}`),this.authState.state==="noAuth"||this.authState.state==="waitingForServerConfirmationOfFreshToken"&&this.tokenConfirmationAttempts>=Od){this.logger.error(`Failed to authenticate: "${i.error}", check your server auth config`),this.syncState.hasAuth()&&this.syncState.clearAuth(),this.authState.state!=="noAuth"&&this.setAndReportAuthFailed(this.authState.config.onAuthChange);return}this.authState.state==="waitingForServerConfirmationOfFreshToken"&&(this.tokenConfirmationAttempts++,this._logVerbose(`retrying reauthentication, ${Od-this.tokenConfirmationAttempts} attempts remaining`)),await this.stopSocket();const s=await this.fetchTokenAndGuardAgainstRace(this.authState.config.fetchToken,{forceRefreshToken:!0});s.isFromOutdatedConfig||(s.value&&this.syncState.isNewAuth(s.value)?(this.authenticate(s.value),this.setAuthState({state:"waitingForServerConfirmationOfFreshToken",config:this.authState.config,token:s.value,hadAuth:this.authState.state==="notRefetching"||this.authState.state==="waitingForScheduledRefetch"})):(this._logVerbose("reauthentication failed, could not fetch a new token"),this.syncState.hasAuth()&&this.syncState.clearAuth(),this.setAndReportAuthFailed(this.authState.config.onAuthChange)),this.tryRestartSocket())}async refetchToken(){if(this.authState.state==="noAuth")return;this._logVerbose("refetching auth token");const i=await this.fetchTokenAndGuardAgainstRace(this.authState.config.fetchToken,{forceRefreshToken:!0});i.isFromOutdatedConfig||(i.value?this.syncState.isNewAuth(i.value)?(this.setAuthState({state:"waitingForServerConfirmationOfFreshToken",hadAuth:this.syncState.hasAuth(),token:i.value,config:this.authState.config}),this.authenticate(i.value)):this.setAuthState({state:"notRefetching",config:this.authState.config}):(this._logVerbose("refetching token failed"),this.syncState.hasAuth()&&this.clearAuth(),this.setAndReportAuthFailed(this.authState.config.onAuthChange)),this._logVerbose("restarting WS after auth token fetch (if currently stopped)"),this.tryRestartSocket())}scheduleTokenRefetch(i){if(this.authState.state==="noAuth")return;const s=this.decodeToken(i);if(!s){this.logger.error("Auth token is not a valid JWT, cannot refetch the token");return}const{iat:f,exp:h}=s;if(!f||!h){this.logger.error("Auth token does not have required fields, cannot refetch the token");return}const y=h-f;if(y<=2){this.logger.error("Auth token does not live long enough, cannot refetch the token");return}let v=Math.min(Mv,(y-this.refreshTokenLeewaySeconds)*1e3);v<=0&&(this.logger.warn(`Refetching auth token immediately, configured leeway ${this.refreshTokenLeewaySeconds}s is larger than the token's lifetime ${y}s`),v=0);const q=setTimeout(()=>{this._logVerbose("running scheduled token refetch"),this.refetchToken()},v);this.setAuthState({state:"waitingForScheduledRefetch",refetchTokenTimeoutId:q,config:this.authState.config}),this._logVerbose(`scheduled preemptive auth token refetching in ${v}ms`)}async fetchTokenAndGuardAgainstRace(i,s){const f=++this.configVersion;this._logVerbose(`fetching token with config version ${f}`);const h=await i(s);return this.configVersion!==f?(this._logVerbose(`stale config version, expected ${f}, got ${this.configVersion}`),{isFromOutdatedConfig:!0}):{isFromOutdatedConfig:!1,value:h}}stop(){this.resetAuthState(),this.configVersion++,this._logVerbose(`config version bumped to ${this.configVersion}`)}setAndReportAuthFailed(i){i(!1),this.resetAuthState()}resetAuthState(){this.setAuthState({state:"noAuth"})}setAuthState(i){const s=i.state==="waitingForServerConfirmationOfFreshToken"?{hadAuth:i.hadAuth,state:i.state,token:`...${i.token.slice(-7)}`}:{state:i.state};switch(this._logVerbose(`setting auth state to ${JSON.stringify(s)}`),i.state){case"waitingForScheduledRefetch":case"notRefetching":case"noAuth":this.tokenConfirmationAttempts=0;break}this.authState.state==="waitingForScheduledRefetch"&&(clearTimeout(this.authState.refetchTokenTimeoutId),this.syncState.markAuthCompletion()),this.authState=i}decodeToken(i){try{return jd(i)}catch(s){return this._logVerbose(`Error decoding token: ${s instanceof Error?s.message:"Unknown error"}`),null}}_logVerbose(i){this.logger.logVerbose(`${i} [v${this.configVersion}]`)}}const zv=["convexClientConstructed","convexWebSocketOpen","convexFirstMessageReceived"];function Cv(o,i){const s={sessionId:i};typeof performance>"u"||!performance.mark||performance.mark(o,{detail:s})}function qv(o){let i=o.name.slice(6);return i=i.charAt(0).toLowerCase()+i.slice(1),{name:i,startTime:o.startTime}}function Dv(o){if(typeof performance>"u"||!performance.getEntriesByName)return[];const i=[];for(const s of zv){const f=performance.getEntriesByName(s).filter(h=>h.entryType==="mark").filter(h=>h.detail.sessionId===o);i.push(...f)}return i.map(qv)}var Uv=Object.defineProperty,Nv=(o,i,s)=>i in o?Uv(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Rt=(o,i,s)=>Nv(o,typeof i!="symbol"?i+"":i,s);class Qv{constructor(i,s,f){if(Rt(this,"address"),Rt(this,"state"),Rt(this,"requestManager"),Rt(this,"webSocketManager"),Rt(this,"authenticationManager"),Rt(this,"remoteQuerySet"),Rt(this,"optimisticQueryResults"),Rt(this,"_transitionHandlerCounter",0),Rt(this,"_nextRequestId"),Rt(this,"_onTransitionFns",new Map),Rt(this,"_sessionId"),Rt(this,"firstMessageReceived",!1),Rt(this,"debug"),Rt(this,"logger"),Rt(this,"maxObservedTimestamp"),Rt(this,"connectionStateSubscribers",new Map),Rt(this,"nextConnectionStateSubscriberId",0),Rt(this,"_lastPublishedConnectionState"),Rt(this,"markConnectionStateDirty",()=>{Promise.resolve().then(()=>{const L=this.connectionState();if(JSON.stringify(L)!==JSON.stringify(this._lastPublishedConnectionState)){this._lastPublishedConnectionState=L;for(const X of this.connectionStateSubscribers.values())X(L)}})}),Rt(this,"mark",L=>{this.debug&&Cv(L,this.sessionId)}),typeof i=="object")throw new Error("Passing a ClientConfig object is no longer supported. Pass the URL of the Convex deployment as a string directly.");f?.skipConvexDeploymentUrlCheck!==!0&&zm(i),f={...f};const h=f.authRefreshTokenLeewaySeconds??2;let y=f.webSocketConstructor;if(!y&&typeof WebSocket>"u")throw new Error("No WebSocket global variable defined! To use Convex in an environment without WebSocket try the HTTP client: https://docs.convex.dev/api/classes/browser.ConvexHttpClient");y=y||WebSocket,this.debug=f.reportDebugInfoToConvex??!1,this.address=i,this.logger=f.logger===!1?xd({verbose:f.verbose??!1}):f.logger!==!0&&f.logger?f.logger:Qd({verbose:f.verbose??!1});const v=i.search("://");if(v===-1)throw new Error("Provided address was not an absolute URL.");const q=i.substring(v+3),E=i.substring(0,v);let _;if(E==="http")_="ws";else if(E==="https")_="wss";else throw new Error(`Unknown parent protocol ${E}`);const H=`${_}://${q}/api/${vd}/sync`;this.state=new Km,this.remoteQuerySet=new Ad(L=>this.state.queryPath(L),this.logger),this.requestManager=new $m(this.logger,this.markConnectionStateDirty);const N=()=>{this.webSocketManager.pause(),this.state.pause()};this.authenticationManager=new Rv(this.state,{authenticate:L=>{const X=this.state.setAuth(L);return this.webSocketManager.sendMessage(X),X.baseVersion},stopSocket:()=>this.webSocketManager.stop(),tryRestartSocket:()=>this.webSocketManager.tryRestart(),pauseSocket:N,resumeSocket:()=>this.webSocketManager.resume(),clearAuth:()=>{this.clearAuth()}},{logger:this.logger,refreshTokenLeewaySeconds:h}),this.optimisticQueryResults=new lv,this.addOnTransitionHandler(L=>{s(L.queries.map(X=>X.token))}),this._nextRequestId=0,this._sessionId=bv();const{unsavedChangesWarning:et}=f;if(typeof window>"u"||typeof window.addEventListener>"u"){if(et===!0)throw new Error("unsavedChangesWarning requested, but window.addEventListener not found! Remove {unsavedChangesWarning: true} from Convex client options.")}else et!==!1&&window.addEventListener("beforeunload",L=>{if(this.requestManager.hasIncompleteRequests()){L.preventDefault();const X="Are you sure you want to leave? Your changes may not be saved.";return(L||window.event).returnValue=X,X}});this.webSocketManager=new pv(H,{onOpen:L=>{this.mark("convexWebSocketOpen"),this.webSocketManager.sendMessage({...L,type:"Connect",sessionId:this._sessionId,maxObservedTimestamp:this.maxObservedTimestamp});const X=new Set(this.remoteQuerySet.remoteQueryResults().keys());this.remoteQuerySet=new Ad(Dt=>this.state.queryPath(Dt),this.logger);const[vt,oe]=this.state.restart(X);oe&&this.webSocketManager.sendMessage(oe),this.webSocketManager.sendMessage(vt);for(const Dt of this.requestManager.restart())this.webSocketManager.sendMessage(Dt)},onResume:()=>{const[L,X]=this.state.resume();X&&this.webSocketManager.sendMessage(X),L&&this.webSocketManager.sendMessage(L);for(const vt of this.requestManager.resume())this.webSocketManager.sendMessage(vt)},onMessage:L=>{switch(this.firstMessageReceived||(this.firstMessageReceived=!0,this.mark("convexFirstMessageReceived"),this.reportMarks()),L.type){case"Transition":{this.observedTimestamp(L.endVersion.ts),this.authenticationManager.onTransition(L),this.remoteQuerySet.transition(L),this.state.transition(L);const X=this.requestManager.removeCompleted(this.remoteQuerySet.timestamp());this.notifyOnQueryResultChanges(X);break}case"MutationResponse":{L.success&&this.observedTimestamp(L.ts);const X=this.requestManager.onResponse(L);X!==null&&this.notifyOnQueryResultChanges(new Map([[X.requestId,X.result]]));break}case"ActionResponse":{this.requestManager.onResponse(L);break}case"AuthError":{this.authenticationManager.onAuthError(L);break}case"FatalError":{const X=Gm(this.logger,L.error);throw this.webSocketManager.terminate(),X}}return{hasSyncedPastLastReconnect:this.hasSyncedPastLastReconnect()}},onServerDisconnectError:f.onServerDisconnectError},y,this.logger,this.markConnectionStateDirty,this.debug),this.mark("convexClientConstructed"),f.expectAuth&&N()}hasSyncedPastLastReconnect(){return this.requestManager.hasSyncedPastLastReconnect()||this.state.hasSyncedPastLastReconnect()}observedTimestamp(i){(this.maxObservedTimestamp===void 0||this.maxObservedTimestamp.lessThanOrEqual(i))&&(this.maxObservedTimestamp=i)}getMaxObservedTimestamp(){return this.maxObservedTimestamp}notifyOnQueryResultChanges(i){const s=this.remoteQuerySet.remoteQueryResults(),f=new Map;for(const[y,v]of s){const q=this.state.queryToken(y);if(q!==null){const E={result:v,udfPath:this.state.queryPath(y),args:this.state.queryArgs(y)};f.set(q,E)}}const h=this.optimisticQueryResults.ingestQueryResultsFromServer(f,new Set(i.keys()));this.handleTransition({queries:h.map(y=>{const v=this.optimisticQueryResults.rawQueryResult(y);return{token:y,modification:{kind:"Updated",result:v}}}),reflectedMutations:Array.from(i).map(([y,v])=>({requestId:y,result:v})),timestamp:this.remoteQuerySet.timestamp()})}handleTransition(i){for(const s of this._onTransitionFns.values())s(i)}addOnTransitionHandler(i){const s=this._transitionHandlerCounter++;return this._onTransitionFns.set(s,i),()=>this._onTransitionFns.delete(s)}getCurrentAuthClaims(){const i=this.state.getAuth();let s={};if(i&&i.tokenType==="User")try{s=i?jd(i.value):{}}catch{s={}}else return;return{token:i.value,decoded:s}}setAuth(i,s){this.authenticationManager.setConfig(i,s)}hasAuth(){return this.state.hasAuth()}setAdminAuth(i,s){const f=this.state.setAdminAuth(i,s);this.webSocketManager.sendMessage(f)}clearAuth(){const i=this.state.clearAuth();this.webSocketManager.sendMessage(i)}subscribe(i,s,f){const h=un(s),{modification:y,queryToken:v,unsubscribe:q}=this.state.subscribe(i,h,f?.journal,f?.componentPath);return y!==null&&this.webSocketManager.sendMessage(y),{queryToken:v,unsubscribe:()=>{const E=q();E&&this.webSocketManager.sendMessage(E)}}}localQueryResult(i,s){const f=un(s),h=Pn(i,f);return this.optimisticQueryResults.queryResult(h)}localQueryResultByToken(i){return this.optimisticQueryResults.queryResult(i)}hasLocalQueryResultByToken(i){return this.optimisticQueryResults.hasQueryResult(i)}localQueryLogs(i,s){const f=un(s),h=Pn(i,f);return this.optimisticQueryResults.queryLogs(h)}queryJournal(i,s){const f=un(s),h=Pn(i,f);return this.state.queryJournal(h)}connectionState(){const i=this.webSocketManager.connectionState();return{hasInflightRequests:this.requestManager.hasInflightRequests(),isWebSocketConnected:i.isConnected,hasEverConnected:i.hasEverConnected,connectionCount:i.connectionCount,connectionRetries:i.connectionRetries,timeOfOldestInflightRequest:this.requestManager.timeOfOldestInflightRequest(),inflightMutations:this.requestManager.inflightMutations(),inflightActions:this.requestManager.inflightActions()}}subscribeToConnectionState(i){const s=this.nextConnectionStateSubscriberId++;return this.connectionStateSubscribers.set(s,i),()=>{this.connectionStateSubscribers.delete(s)}}async mutation(i,s,f){const h=await this.mutationInternal(i,s,f);if(!h.success)throw h.errorData!==void 0?Fc(h,new $c(Vl("mutation",i,h))):new Error(Vl("mutation",i,h));return h.value}async mutationInternal(i,s,f,h){const{mutationPromise:y}=this.enqueueMutation(i,s,f,h);return y}enqueueMutation(i,s,f,h){const y=un(s);this.tryReportLongDisconnect();const v=this.nextRequestId;if(this._nextRequestId++,f!==void 0){const H=f.optimisticUpdate;if(H!==void 0){const N=X=>{H(X,y)instanceof Promise&&this.logger.warn("Optimistic update handler returned a Promise. Optimistic updates should be synchronous.")},L=this.optimisticQueryResults.applyOptimisticUpdate(N,v).map(X=>{const vt=this.localQueryResultByToken(X);return{token:X,modification:{kind:"Updated",result:vt===void 0?void 0:{success:!0,value:vt,logLines:[]}}}});this.handleTransition({queries:L,reflectedMutations:[],timestamp:this.remoteQuerySet.timestamp()})}}const q={type:"Mutation",requestId:v,udfPath:i,componentPath:h,args:[He(y)]},E=this.webSocketManager.sendMessage(q),_=this.requestManager.request(q,E);return{requestId:v,mutationPromise:_}}async action(i,s){const f=await this.actionInternal(i,s);if(!f.success)throw f.errorData!==void 0?Fc(f,new $c(Vl("action",i,f))):new Error(Vl("action",i,f));return f.value}async actionInternal(i,s,f){const h=un(s),y=this.nextRequestId;this._nextRequestId++,this.tryReportLongDisconnect();const v={type:"Action",requestId:y,udfPath:i,componentPath:f,args:[He(h)]},q=this.webSocketManager.sendMessage(v);return this.requestManager.request(v,q)}async close(){return this.authenticationManager.stop(),this.webSocketManager.terminate()}get url(){return this.address}get nextRequestId(){return this._nextRequestId}get sessionId(){return this._sessionId}reportMarks(){if(this.debug){const i=Dv(this.sessionId);this.webSocketManager.sendMessage({type:"Event",eventType:"ClientConnect",event:i})}}tryReportLongDisconnect(){if(!this.debug)return;const i=this.connectionState().timeOfOldestInflightRequest;if(i===null||Date.now()-i.getTime()<=60*1e3)return;const s=`${this.address}/api/debug_event`;fetch(s,{method:"POST",headers:{"Content-Type":"application/json","Convex-Client":`npm-${vd}`},body:JSON.stringify({event:"LongWebsocketDisconnect"})}).then(f=>{f.ok||this.logger.warn("Analytics request failed with response:",f.body)}).catch(f=>{this.logger.warn("Analytics response failed with error:",f)})}}function Zc(o){if(typeof o!="object"||o===null||!Array.isArray(o.page)||typeof o.isDone!="boolean"||typeof o.continueCursor!="string")throw new Error(`Not a valid paginated query result: ${o?.toString()}`);return o}var xv=Object.defineProperty,wv=(o,i,s)=>i in o?xv(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Md=(o,i,s)=>wv(o,typeof i!="symbol"?i+"":i,s);class Bv{constructor(i,s){this.client=i,this.onTransition=s,Md(this,"paginatedQuerySet",new Map),Md(this,"lastTransitionTs"),this.lastTransitionTs=It.fromNumber(0),this.client.addOnTransitionHandler(f=>this.onBaseTransition(f))}subscribe(i,s,f){const h=tl(i),y=pd(h,s,f),v=()=>this.removePaginatedQuerySubscriber(y),q=this.paginatedQuerySet.get(y);return q?(q.numSubscribers+=1,{paginatedQueryToken:y,unsubscribe:v}):(this.paginatedQuerySet.set(y,{token:y,canonicalizedUdfPath:h,args:s,numSubscribers:1,options:{initialNumItems:f.initialNumItems},nextPageKey:0,pageKeys:[],pageKeyToQuery:new Map,ongoingSplits:new Map,skip:!1,id:f.id}),this.addPageToPaginatedQuery(y,null,f.initialNumItems),{paginatedQueryToken:y,unsubscribe:v})}localQueryResult(i,s,f){const h=tl(i),y=pd(h,s,f);return this.localQueryResultByToken(y)}localQueryResultByToken(i){const s=this.paginatedQuerySet.get(i);if(!s)return;const f=this.activePageQueryTokens(s);if(f.length===0)return{results:[],status:"LoadingFirstPage",loadMore:E=>this.loadMoreOfPaginatedQuery(i,E)};let h=[],y=!1,v=!1;for(const E of f){const _=this.client.localQueryResultByToken(E);if(_===void 0){y=!0,v=!1;continue}const H=Zc(_);h=h.concat(H.page),v=!!H.isDone}let q;return y?q=h.length===0?"LoadingFirstPage":"LoadingMore":v?q="Exhausted":q="CanLoadMore",{results:h,status:q,loadMore:E=>this.loadMoreOfPaginatedQuery(i,E)}}onBaseTransition(i){const s=i.queries.map(v=>v.token),f=this.queriesContainingTokens(s);let h=[];f.length>0&&(this.processPaginatedQuerySplits(f,v=>this.client.localQueryResultByToken(v)),h=f.map(v=>({token:v,modification:{kind:"Updated",result:this.localQueryResultByToken(v)}})));const y={...i,paginatedQueries:h};this.onTransition(y)}loadMoreOfPaginatedQuery(i,s){this.mustGetPaginatedQuery(i);const f=this.queryTokenForLastPageOfPaginatedQuery(i),h=this.client.localQueryResultByToken(f);if(!h)return!1;const y=Zc(h);if(y.isDone)return!1;this.addPageToPaginatedQuery(i,y.continueCursor,s);const v={timestamp:this.lastTransitionTs,reflectedMutations:[],queries:[],paginatedQueries:[{token:i,modification:{kind:"Updated",result:this.localQueryResultByToken(i)}}]};return this.onTransition(v),!0}queriesContainingTokens(i){if(i.length===0)return[];const s=[],f=new Set(i);for(const[h,y]of this.paginatedQuerySet)for(const v of this.allQueryTokens(y))if(f.has(v)){s.push(h);break}return s}processPaginatedQuerySplits(i,s){for(const f of i){const h=this.mustGetPaginatedQuery(f),{ongoingSplits:y,pageKeyToQuery:v,pageKeys:q}=h;for(const[E,[_,H]]of y)s(v.get(_).queryToken)!==void 0&&s(v.get(H).queryToken)!==void 0&&this.completePaginatedQuerySplit(h,E,_,H);for(const E of q){if(y.has(E))continue;const _=v.get(E).queryToken,H=s(_);if(!H)continue;const N=Zc(H);N.splitCursor&&(N.pageStatus==="SplitRecommended"||N.pageStatus==="SplitRequired"||N.page.length>h.options.initialNumItems*2)&&this.splitPaginatedQueryPage(h,E,N.splitCursor,N.continueCursor)}}}splitPaginatedQueryPage(i,s,f,h){const y=i.nextPageKey++,v=i.nextPageKey++,q={cursor:h,numItems:i.options.initialNumItems,id:i.id},E=this.client.subscribe(i.canonicalizedUdfPath,{...i.args,paginationOpts:{...q,cursor:null,endCursor:f}});i.pageKeyToQuery.set(y,E);const _=this.client.subscribe(i.canonicalizedUdfPath,{...i.args,paginationOpts:{...q,cursor:f,endCursor:h}});i.pageKeyToQuery.set(v,_),i.ongoingSplits.set(s,[y,v])}addPageToPaginatedQuery(i,s,f){const h=this.mustGetPaginatedQuery(i),y=h.nextPageKey++,v={cursor:s,numItems:f,id:h.id},q={...h.args,paginationOpts:v},E=this.client.subscribe(h.canonicalizedUdfPath,q);return h.pageKeys.push(y),h.pageKeyToQuery.set(y,E),E}removePaginatedQuerySubscriber(i){const s=this.paginatedQuerySet.get(i);if(s&&(s.numSubscribers-=1,!(s.numSubscribers>0))){for(const f of s.pageKeyToQuery.values())f.unsubscribe();this.paginatedQuerySet.delete(i)}}completePaginatedQuerySplit(i,s,f,h){const y=i.pageKeyToQuery.get(s);i.pageKeyToQuery.delete(s);const v=i.pageKeys.indexOf(s);i.pageKeys.splice(v,1,f,h),i.ongoingSplits.delete(s),y.unsubscribe()}activePageQueryTokens(i){return i.pageKeys.map(s=>i.pageKeyToQuery.get(s).queryToken)}allQueryTokens(i){return Array.from(i.pageKeyToQuery.values()).map(s=>s.queryToken)}queryTokenForLastPageOfPaginatedQuery(i){const s=this.mustGetPaginatedQuery(i),f=s.pageKeys[s.pageKeys.length-1];if(f===void 0)throw new Error(`No pages for paginated query ${i}`);return s.pageKeyToQuery.get(f).queryToken}mustGetPaginatedQuery(i){const s=this.paginatedQuerySet.get(i);if(!s)throw new Error("paginated query no longer exists for token "+i);return s}}function Hv({getCurrentValue:o,subscribe:i}){const[s,f]=Pt.useState(()=>({getCurrentValue:o,subscribe:i,value:o()}));let h=s.value;return(s.getCurrentValue!==o||s.subscribe!==i)&&(h=o(),f({getCurrentValue:o,subscribe:i,value:h})),Pt.useEffect(()=>{let y=!1;const v=()=>{y||f(E=>{if(E.getCurrentValue!==o||E.subscribe!==i)return E;const _=o();return E.value===_?E:{...E,value:_}})},q=i(v);return v(),()=>{y=!0,q()}},[o,i]),h}var jv=Object.defineProperty,Lv=(o,i,s)=>i in o?jv(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,an=(o,i,s)=>Lv(o,typeof i!="symbol"?i+"":i,s);const Vv=5e3;if(typeof Ic>"u")throw new Error("Required dependency 'react' not found");class Yv{constructor(i,s){if(an(this,"address"),an(this,"cachedSync"),an(this,"cachedPaginatedQueryClient"),an(this,"listeners"),an(this,"options"),an(this,"closed",!1),an(this,"_logger"),an(this,"adminAuth"),an(this,"fakeUserIdentity"),i===void 0)throw new Error("No address provided to ConvexReactClient.\nIf trying to deploy to production, make sure to follow all the instructions found at https://docs.convex.dev/production/hosting/\nIf running locally, make sure to run `convex dev` and ensure the .env.local file is populated.");if(typeof i!="string")throw new Error(`ConvexReactClient requires a URL like 'https://happy-otter-123.convex.cloud', received something of type ${typeof i} instead.`);if(!i.includes("://"))throw new Error("Provided address was not an absolute URL.");this.address=i,this.listeners=new Map,this._logger=s?.logger===!1?xd({verbose:s?.verbose??!1}):s?.logger!==!0&&s?.logger?s.logger:Qd({verbose:s?.verbose??!1}),this.options={...s,logger:this._logger}}get url(){return this.address}get sync(){if(this.closed)throw new Error("ConvexReactClient has already been closed.");return this.cachedSync?this.cachedSync:(this.cachedSync=new Qv(this.address,()=>{},this.options),this.adminAuth&&this.cachedSync.setAdminAuth(this.adminAuth,this.fakeUserIdentity),this.cachedPaginatedQueryClient=new Bv(this.cachedSync,i=>this.handleTransition(i)),this.cachedSync)}get paginatedQueryClient(){if(this.sync,this.cachedPaginatedQueryClient)return this.cachedPaginatedQueryClient;throw new Error("Should already be instantiated")}setAuth(i,s){if(typeof i=="string")throw new Error("Passing a string to ConvexReactClient.setAuth is no longer supported, please upgrade to passing in an async function to handle reauthentication.");this.sync.setAuth(i,s??(()=>{}))}clearAuth(){this.sync.clearAuth()}setAdminAuth(i,s){if(this.adminAuth=i,this.fakeUserIdentity=s,this.closed)throw new Error("ConvexReactClient has already been closed.");this.cachedSync&&this.sync.setAdminAuth(i,s)}watchQuery(i,...s){const[f,h]=s,y=_e(i);return{onUpdate:v=>{const{queryToken:q,unsubscribe:E}=this.sync.subscribe(y,f,h),_=this.listeners.get(q);return _!==void 0?_.add(v):this.listeners.set(q,new Set([v])),()=>{if(this.closed)return;const H=this.listeners.get(q);H.delete(v),H.size===0&&this.listeners.delete(q),E()}},localQueryResult:()=>{if(this.cachedSync)return this.cachedSync.localQueryResult(y,f)},localQueryLogs:()=>{if(this.cachedSync)return this.cachedSync.localQueryLogs(y,f)},journal:()=>{if(this.cachedSync)return this.cachedSync.queryJournal(y,f)}}}prewarmQuery(i){const s=i.extendSubscriptionFor??Vv,h=this.watchQuery(i.query,i.args||{}).onUpdate(()=>{});setTimeout(h,s)}watchPaginatedQuery(i,s,f){const h=_e(i);return{onUpdate:y=>{const{paginatedQueryToken:v,unsubscribe:q}=this.paginatedQueryClient.subscribe(h,s||{},f),E=this.listeners.get(v);return E!==void 0?E.add(y):this.listeners.set(v,new Set([y])),()=>{if(this.closed)return;const _=this.listeners.get(v);_.delete(y),_.size===0&&this.listeners.delete(v),q()}},localQueryResult:()=>this.paginatedQueryClient.localQueryResult(h,s,f)}}mutation(i,...s){const[f,h]=s,y=_e(i);return this.sync.mutation(y,f,h)}action(i,...s){const f=_e(i);return this.sync.action(f,...s)}query(i,...s){const f=this.watchQuery(i,...s),h=f.localQueryResult();return h!==void 0?Promise.resolve(h):new Promise((y,v)=>{const q=f.onUpdate(()=>{q();try{y(f.localQueryResult())}catch(E){v(E)}})})}connectionState(){return this.sync.connectionState()}subscribeToConnectionState(i){return this.sync.subscribeToConnectionState(i)}get logger(){return this._logger}async close(){if(this.closed=!0,this.listeners=new Map,this.cachedPaginatedQueryClient&&(this.cachedPaginatedQueryClient=void 0),this.cachedSync){const i=this.cachedSync;this.cachedSync=void 0,await i.close()}}handleTransition(i){const s=i.queries.map(h=>h.token),f=i.paginatedQueries.map(h=>h.token);this.transition([...s,...f])}transition(i){for(const s of i){const f=this.listeners.get(s);if(f)for(const h of f)h()}}}const Ld=Ic.createContext(void 0);function Gv(){return Pt.useContext(Ld)}const Xv=({client:o,children:i})=>Ic.createElement(Ld.Provider,{value:o},i);function Zv(o,...i){const s=i[0]==="skip",f=i[0]==="skip"?{}:un(i[0]),h=typeof o=="string"?Pm(o):o,y=_e(h),v=Pt.useMemo(()=>s?{}:{query:{query:h,args:f}},[JSON.stringify(He(f)),y,s]),E=$v(v).query;if(E instanceof Error)throw E;return E}var Kv=Object.defineProperty,kv=(o,i,s)=>i in o?Kv(o,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):o[i]=s,Kc=(o,i,s)=>kv(o,typeof i!="symbol"?i+"":i,s);class Jv{constructor(i){Kc(this,"createWatch"),Kc(this,"queries"),Kc(this,"listeners"),this.createWatch=i,this.queries={},this.listeners=new Set}setQueries(i){for(const s of Object.keys(i)){const{query:f,args:h,paginationOptions:y}=i[s];if(_e(f),this.queries[s]===void 0)this.addQuery(s,f,h,y?{paginationOptions:y}:{});else{const v=this.queries[s];(_e(f)!==_e(v.query)||JSON.stringify(He(h))!==JSON.stringify(He(v.args))||JSON.stringify(y)!==JSON.stringify(v.paginationOptions))&&(this.removeQuery(s),this.addQuery(s,f,h,y?{paginationOptions:y}:{}))}}for(const s of Object.keys(this.queries))i[s]===void 0&&this.removeQuery(s)}subscribe(i){return this.listeners.add(i),()=>{this.listeners.delete(i)}}getLocalResults(i){const s={};for(const f of Object.keys(i)){const{query:h,args:y}=i[f],v=i[f].paginationOptions;_e(h);const q=this.createWatch(h,y,v?{paginationOptions:v}:{});let E;try{E=q.localQueryResult()}catch(_){if(_ instanceof Error)E=_;else throw _}s[f]=E}return s}setCreateWatch(i){this.createWatch=i;for(const s of Object.keys(this.queries)){const{query:f,args:h,watch:y,paginationOptions:v}=this.queries[s],q="journal"in y?y.journal():void 0;this.removeQuery(s),this.addQuery(s,f,h,{...q?{journal:q}:[],...v?{paginationOptions:v}:{}})}}destroy(){for(const i of Object.keys(this.queries))this.removeQuery(i);this.listeners=new Set}addQuery(i,s,f,{paginationOptions:h,journal:y}){if(this.queries[i]!==void 0)throw new Error(`Tried to add a new query with identifier ${i} when it already exists.`);const v=this.createWatch(s,f,{...y?{journal:y}:[],...h?{paginationOptions:h}:{}}),q=v.onUpdate(()=>this.notifyListeners());this.queries[i]={query:s,args:f,watch:v,unsubscribe:q,...h?{paginationOptions:h}:{}}}removeQuery(i){const s=this.queries[i];if(s===void 0)throw new Error(`No query found with identifier ${i}.`);s.unsubscribe(),delete this.queries[i]}notifyListeners(){for(const i of this.listeners)i()}}function $v(o){const i=Gv();if(i===void 0)throw new Error("Could not find Convex client! `useQuery` must be used in the React component tree under `ConvexProvider`. Did you forget it? See https://docs.convex.dev/quick-start#set-up-convex-in-your-react-app");const s=Pt.useMemo(()=>(f,h,{journal:y,paginationOptions:v})=>v?i.watchPaginatedQuery(f,h,v):i.watchQuery(f,h,y?{journal:y}:{}),[i]);return Fv(o,s)}function Fv(o,i){const[s]=Pt.useState(()=>new Jv(i));s.createWatch!==i&&s.setCreateWatch(i),Pt.useEffect(()=>()=>s.destroy(),[s]);const f=Pt.useMemo(()=>({getCurrentValue:()=>s.getLocalResults(o),subscribe:h=>(s.setQueries(o),s.subscribe(h))}),[s,o]);return Hv(f)}function Wv(o){const i=Zv(o,{}),[s,f]=Pt.useState(null),[h,y]=Pt.useState(null);return i&&s===null&&f(i.currentDeploymentId),{updateAvailable:Pt.useMemo(()=>{if(!i||s===null)return!1;const _=i.currentDeploymentId!==s,H=i.currentDeploymentId===h;return _&&!H},[i,s,h]),reload:()=>{window.location.reload()},dismiss:()=>{i&&y(i.currentDeploymentId)},deployment:i}}function Iv({getCurrentDeployment:o,message:i="A new version is available!",buttonText:s="Reload",dismissable:f=!0,className:h,style:y}){const{updateAvailable:v,reload:q,dismiss:E}=Wv(o);if(!v)return null;const _={position:"fixed",bottom:"1rem",right:"1rem",backgroundColor:"#1a1a2e",color:"#fff",padding:"1rem 1.5rem",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0, 0, 0, 0.3)",display:"flex",alignItems:"center",gap:"1rem",zIndex:9999,fontFamily:"system-ui, -apple-system, sans-serif",fontSize:"14px",...y},H={backgroundColor:"#4f46e5",color:"#fff",border:"none",padding:"0.5rem 1rem",borderRadius:"4px",cursor:"pointer",fontWeight:500},N={background:"none",border:"none",color:"#888",cursor:"pointer",padding:"0.25rem",fontSize:"18px",lineHeight:1};return Y.jsxs("div",{className:h,style:_,children:[Y.jsx("span",{children:i}),Y.jsx("button",{onClick:q,style:H,children:s}),f&&Y.jsx("button",{onClick:E,style:N,"aria-label":"Dismiss",children:"×"})]})}function Vd(o,i){const s={get(f,h){if(typeof h=="string"){const y=[...i,h];return Vd(o,y)}else if(h===wd){if(i.length<1){const y=[o,...i].join(".");throw new Error(`API path is expected to be of the form \`${o}.childComponent.functionName\`. Found: \`${y}\``)}return"_reference/childComponent/"+i.join("/")}else return}};return new Proxy({},s)}const Pv=()=>Vd("components",[]),t0=tv;Pv();function e0(){const[o,i]=Pt.useState(0);return Y.jsxs("div",{className:"app",children:[Y.jsx(Iv,{getCurrentDeployment:t0.example.getCurrentDeployment,message:"🚀 New version deployed!",buttonText:"Refresh"}),Y.jsxs("header",{className:"header",children:[Y.jsx("h1",{children:"🚀 Convex Static Hosting"}),Y.jsx("p",{className:"subtitle",children:"Self-hosted React app on Convex"}),Y.jsx("h1",{children:"Finally!"})]}),Y.jsxs("main",{className:"main",children:[Y.jsxs("div",{className:"card",children:[Y.jsx("h2",{children:"It works!"}),Y.jsx("p",{children:"This React app is being served directly from Convex HTTP actions and file storage. No external hosting required!"}),Y.jsxs("div",{className:"counter",children:[Y.jsx("button",{onClick:()=>i(s=>s-1),children:"−"}),Y.jsx("span",{className:"count",children:o}),Y.jsx("button",{onClick:()=>i(s=>s+1),children:"+"})]})]}),Y.jsxs("div",{className:"features",children:[Y.jsxs("div",{className:"feature",children:[Y.jsx("span",{className:"icon",children:"📦"}),Y.jsx("h3",{children:"Simple Upload"}),Y.jsxs("p",{children:["Run ",Y.jsx("code",{children:"npm run deploy:static"})," to upload your built files to Convex storage"]})]}),Y.jsxs("div",{className:"feature",children:[Y.jsx("span",{className:"icon",children:"🔄"}),Y.jsx("h3",{children:"Live Reload"}),Y.jsx("p",{children:"Connected clients are notified when you deploy - with a prompt to reload"})]}),Y.jsxs("div",{className:"feature",children:[Y.jsx("span",{className:"icon",children:"⚡"}),Y.jsx("h3",{children:"Smart Caching"}),Y.jsx("p",{children:"Hashed assets get long-term caching, while HTML is always fresh"})]})]}),Y.jsxs("div",{className:"card",children:[Y.jsx("h3",{children:"How it works"}),Y.jsxs("ol",{children:[Y.jsx("li",{children:"Build your app with Vite or your bundler of choice"}),Y.jsx("li",{children:"Upload the dist/ folder using the provided script"}),Y.jsxs("li",{children:["Access your app at ",Y.jsx("code",{children:"your-deployment.convex.site"})]})]})]})]}),Y.jsx("footer",{className:"footer",children:Y.jsxs("p",{children:["Built with"," ",Y.jsx("a",{href:"https://convex.dev",target:"_blank",rel:"noopener noreferrer",children:"Convex"})," ","+"," ",Y.jsx("a",{href:"https://react.dev",target:"_blank",rel:"noopener noreferrer",children:"React"})]})})]})}const n0=new Yv("https://qualified-goat-610.convex.cloud");Tm.createRoot(document.getElementById("root")).render(Y.jsx(Pt.StrictMode,{children:Y.jsx(Xv,{client:n0,children:Y.jsx(e0,{})})})); diff --git a/example/dist/index.html b/example/dist/index.html index f3581da..240c775 100644 --- a/example/dist/index.html +++ b/example/dist/index.html @@ -4,7 +4,7 @@ Example App - + diff --git a/example/src/App.tsx b/example/src/App.tsx index 480582c..41cd1fb 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { UpdateBanner } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; import "./App.css"; function App() { @@ -8,12 +7,8 @@ function App() { return (
- {/* Shows a banner when a new deployment is available */} - + {/* Resolves convex/staticHosting.ts:getCurrentDeployment by default. */} +

🚀 Convex Static Hosting

diff --git a/src/cli/init.ts b/src/cli/init.ts deleted file mode 100644 index 93d29ed..0000000 --- a/src/cli/init.ts +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env node -/** - * Output integration instructions for LLMs. - * - * Usage: - * npx @convex-dev/static-hosting init - */ - -const instructions = ` -# Convex Static Hosting - Integration Instructions - -You are integrating the @convex-dev/static-hosting component into a Convex app. -This component enables hosting static files (React/Vite apps) directly on Convex. - -## What This Component Does - -- Stores static files in Convex storage -- Serves files via HTTP actions with proper MIME types -- Supports SPA routing (fallback to index.html) -- Smart caching: hashed assets cached forever, HTML revalidates -- ETag support for efficient cache revalidation -- Live reload notifications when new deployments happen - -## Files to Create/Modify - -### 1. convex/convex.config.ts (create or modify) - -\`\`\`typescript -import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config"; - -const app = defineApp(); -app.use(selfHosting); - -export default app; -\`\`\` - -### 2. convex/staticHosting.ts (create) - -\`\`\`typescript -import { components } from "./_generated/api"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; - -// Internal functions for secure uploads (only callable via CLI) -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -// Public query for live reload notifications -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); -\`\`\` - -### 3. convex/http.ts (create or modify) - -\`\`\`typescript -import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Option A: Serve at root (if no other HTTP routes) -registerStaticRoutes(http, components.selfHosting); - -// Option B: Serve at /app/ prefix (recommended if you have API routes) -// registerStaticRoutes(http, components.selfHosting, { -// pathPrefix: "/app", -// }); - -// Add other HTTP routes here if needed -// http.route({ path: "/api/webhook", method: "POST", handler: ... }); - -export default http; -\`\`\` - -### 4. package.json scripts (add) - -\`\`\`json -{ - "scripts": { - "build": "vite build", - "deploy:static": "npx @convex-dev/static-hosting upload --build --prod" - } -} -\`\`\` - -IMPORTANT: Use \`--build\` flag instead of running \`npm run build\` separately. -The \`--build\` flag ensures \`VITE_CONVEX_URL\` is set correctly for the target -environment (production or dev). Running build separately uses .env.local which -has the dev URL. - -### 5. src/App.tsx (optional: add live reload banner) - -\`\`\`typescript -import { UpdateBanner } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; - -function App() { - return ( -
- {/* Shows banner when new deployment is available */} - - - {/* Rest of your app */} -
- ); -} -\`\`\` - -Or use the hook for custom UI: -\`\`\`typescript -import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; - -function App() { - const { updateAvailable, reload, dismiss } = useDeploymentUpdates( - api.staticHosting.getCurrentDeployment - ); - - // Custom update notification UI -} -\`\`\` - -## Deployment - -\`\`\`bash -# Login to Convex (first time) -npx convex login - -# Deploy Convex backend to production FIRST -npx convex deploy - -# Deploy static files to production -npm run deploy:static - -# Your app is now live at: -# https://your-deployment.convex.site -# (or https://your-deployment.convex.site/app/ if using path prefix) -\`\`\` - -For development/testing: -\`\`\`bash -# Push to dev environment -npx convex dev --once - -# Deploy static files to dev (omit --prod) -npx @convex-dev/static-hosting upload --build -\`\`\` - -## CLI Reference - -\`\`\`bash -npx @convex-dev/static-hosting upload [options] - -Options: - -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. convex/.ts (default: staticHosting) - --prod Deploy to production Convex deployment - --dev Deploy to dev deployment (default) - -b, --build Run 'npm run build' with correct VITE_CONVEX_URL - -h, --help Show help -\`\`\` - -## Important Notes - -1. The upload functions are INTERNAL - they can only be called via \`npx convex run\`, not from the public internet -2. Static files are stored in the app's storage (not the component's) for proper isolation -3. Hashed assets (e.g., main-abc123.js) get immutable caching; HTML files always revalidate -4. The component supports SPA routing - routes without file extensions serve index.html -5. Always use \`--build\` flag to ensure VITE_CONVEX_URL is set correctly for the target environment -6. Deploy Convex backend (\`npx convex deploy\`) BEFORE deploying static files to production -`; - -console.log(instructions); diff --git a/src/cli/setup.ts b/src/cli/setup.ts index f771446..f487b18 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -1,20 +1,14 @@ #!/usr/bin/env node /** - * Interactive setup wizard for Convex Static Hosting. + * Setup wizard for Convex Static Hosting. * * Usage: * npx @convex-dev/static-hosting setup */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { createInterface } from "readline"; import { join } from "path"; -const rl = createInterface({ - input: process.stdin, - output: process.stdout, -}); - function success(msg: string): void { console.log(`✓ ${msg}`); } @@ -23,121 +17,42 @@ function skip(msg: string): void { console.log(`· ${msg}`); } -/** - * Create convex/convex.config.ts - */ -function createConvexConfig(): boolean { +function createConvexConfig(): void { const configPath = join(process.cwd(), "convex", "convex.config.ts"); if (existsSync(configPath)) { const existing = readFileSync(configPath, "utf-8"); - if (existing.includes("selfHosting")) { + if (existing.includes("@convex-dev/static-hosting")) { skip("convex/convex.config.ts (already configured)"); - return false; + return; } - // File exists but doesn't have our component - tell user to add manually - console.log("\n⚠️ convex/convex.config.ts exists. Please add manually:"); + console.log("\n⚠️ convex/convex.config.ts exists. Add manually:"); console.log( - ' import selfHosting from "@convex-dev/static-hosting/convex.config";', + ' import staticHosting from "@convex-dev/static-hosting/convex.config";', ); - console.log(" app.use(selfHosting);\n"); - return false; + console.log(' app.use(staticHosting, { httpPrefix: "/" });\n'); + return; } writeFileSync( configPath, `import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config"; +import staticHosting from "@convex-dev/static-hosting/convex.config"; const app = defineApp(); -app.use(selfHosting); +app.use(staticHosting, { httpPrefix: "/" }); export default app; `, ); success("Created convex/convex.config.ts"); - return true; -} - -/** - * Create convex/staticHosting.ts - */ -function createStaticHostingFile(): boolean { - const filePath = join(process.cwd(), "convex", "staticHosting.ts"); - - if (existsSync(filePath)) { - skip("convex/staticHosting.ts (already exists)"); - return false; - } - - writeFileSync( - filePath, - `import { components } from "./_generated/api"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; - -// Internal functions for secure uploads (CLI only) -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -// Public query for live reload notifications -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); -`, - ); - success("Created convex/staticHosting.ts"); - return true; -} - -/** - * Create convex/http.ts - */ -function createHttpFile(): boolean { - const filePath = join(process.cwd(), "convex", "http.ts"); - - if (existsSync(filePath)) { - const existing = readFileSync(filePath, "utf-8"); - if (existing.includes("registerStaticRoutes")) { - skip("convex/http.ts (already configured)"); - return false; - } - console.log("\n⚠️ convex/http.ts exists. Please add manually:"); - console.log( - ' import { registerStaticRoutes } from "@convex-dev/static-hosting";', - ); - console.log(" registerStaticRoutes(http, components.selfHosting);\n"); - return false; - } - - writeFileSync( - filePath, - `import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Serve static files at root with SPA fallback -registerStaticRoutes(http, components.selfHosting); - -export default http; -`, - ); - success("Created convex/http.ts"); - return true; } -/** - * Update package.json with deploy script - */ -function updatePackageJson(): boolean { +function updatePackageJson(): void { const pkgPath = join(process.cwd(), "package.json"); - if (!existsSync(pkgPath)) { console.log("⚠️ No package.json found"); - return false; + return; } const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); @@ -145,44 +60,43 @@ function updatePackageJson(): boolean { if (pkg.scripts.deploy) { skip("package.json deploy script (already exists)"); - return false; + return; } pkg.scripts.deploy = "npx @convex-dev/static-hosting deploy"; writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); success("Added deploy script to package.json"); - return true; } -async function main(): Promise { +function main(): void { console.log("\n🚀 Convex Static Hosting Setup\n"); - // Check for convex directory if (!existsSync("convex")) { mkdirSync("convex"); success("Created convex/ directory"); } - console.log("Creating files...\n"); - - // Create the Convex files createConvexConfig(); - createStaticHostingFile(); - createHttpFile(); updatePackageJson(); - // Next steps console.log("\n✨ Setup complete!\n"); console.log("Next steps:\n"); console.log(" 1. npx convex dev # Generate types"); - console.log(" 2. npm run deploy # Deploy everything\n"); + console.log(" 2. npm run deploy # Build and deploy\n"); console.log("Your app will be at: https://.convex.site\n"); - - rl.close(); + console.log( + "Optional: to use from @convex-dev/static-hosting/react,", + ); + console.log("create convex/staticHosting.ts:\n"); + console.log( + ' import { exposeDeploymentQuery } from "@convex-dev/static-hosting";', + ); + console.log(' import { components } from "./_generated/api";'); + console.log( + " export const { getCurrentDeployment } = exposeDeploymentQuery(", + ); + console.log(" components.staticHosting,"); + console.log(" );\n"); } -main().catch((err) => { - console.error("Setup failed:", err); - rl.close(); - process.exit(1); -}); +main(); diff --git a/src/cli/upload.ts b/src/cli/upload.ts index 3ea663f..5c0dcea 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -1,13 +1,13 @@ #!/usr/bin/env node /** - * CLI tool to upload static files to Convex storage. + * CLI tool to upload static files to a Convex static-hosting component. * * Usage: * npx @convex-dev/static-hosting upload [options] * * Options: * --dist Path to dist directory (default: ./dist) - * --component Module name where upload API is exposed — i.e. convex/.ts (default: staticHosting) + * --component Component instance name (default: staticHosting) * --prod Deploy to production deployment * --help Show help */ @@ -102,13 +102,11 @@ Upload static files from a dist directory to Convex storage. Options: -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. - convex/.ts (default: staticHosting). Not - the registered component name from convex.config.ts. + -c, --component Static-hosting component instance name (default: staticHosting) --prod Deploy to production deployment -b, --build Run 'npm run build' with correct VITE_CONVEX_URL before uploading --cdn Upload non-HTML assets to convex-fs CDN instead of Convex storage - --cdn-delete-function Convex function to delete CDN blobs (default: :deleteCdnBlobs) + --cdn-delete-function App function to delete CDN blobs (e.g. staticHosting:deleteCdnBlobs) -j, --concurrency Number of parallel uploads (default: 5) -h, --help Show this help message @@ -145,11 +143,13 @@ function getConvexEnv(name: string, prod: boolean): string | null { } function convexRunAsync( + componentName: string | undefined, functionPath: string, args: Record = {}, ): Promise { return runConvexAsync([ "run", + ...(componentName ? ["--component", componentName] : []), functionPath, JSON.stringify(args), "--typecheck=disable", @@ -194,7 +194,8 @@ async function uploadWithConcurrency( // Step 1: Generate all upload URLs in one batch call console.log(` Generating ${storageFiles.length} upload URLs...`); const urlsOutput = await convexRunAsync( - `${componentName}:generateUploadUrls`, + componentName, + "lib:generateUploadUrls", { count: storageFiles.length }, ); const uploadUrls: string[] = JSON.parse(urlsOutput); @@ -217,8 +218,12 @@ async function uploadWithConcurrency( storageIds[idx] = storageId; completed++; const isHtml = file.contentType.startsWith("text/html"); - console.log(` [${completed}/${total}] ${file.path} (${isHtml ? "storage/html" : "storage"})`); - })().then(() => { pending.delete(task); }); + console.log( + ` [${completed}/${total}] ${file.path} (${isHtml ? "storage/html" : "storage"})`, + ); + })().then(() => { + pending.delete(task); + }); pending.add(task); if (pending.size >= concurrency) { await Promise.race(pending); @@ -261,7 +266,9 @@ async function uploadWithConcurrency( }); completed++; console.log(` [${completed}/${total}] ${file.path} (cdn)`); - })().then(() => { pending.delete(task); }); + })().then(() => { + pending.delete(task); + }); pending.add(task); if (pending.size >= concurrency) { await Promise.race(pending); @@ -278,7 +285,7 @@ async function uploadWithConcurrency( const cdnAssets = allAssets.filter((a) => a.blobId); if (storageAssets.length > 0) { - await convexRunAsync(`${componentName}:recordAssets`, { + await convexRunAsync(componentName, "lib:recordAssets", { assets: storageAssets.map((a) => ({ path: a.path, storageId: a.storageId!, @@ -290,7 +297,7 @@ async function uploadWithConcurrency( // CDN assets still need individual recording (they use blobId not storageId) for (const asset of cdnAssets) { - await convexRunAsync(`${componentName}:recordAsset`, { + await convexRunAsync(componentName, "lib:recordAsset", { path: asset.path, blobId: asset.blobId, contentType: asset.contentType, @@ -396,7 +403,9 @@ async function main(): Promise { if (useCdn) { siteUrl = getConvexSiteUrl(useProd); if (!siteUrl) { - console.error("Error: Could not determine Convex site URL for CDN uploads."); + console.error( + "Error: Could not determine Convex site URL for CDN uploads.", + ); console.error("Make sure your Convex deployment is running."); process.exit(1); } @@ -434,28 +443,39 @@ async function main(): Promise { console.log(""); // Garbage collect old files - const gcOutput = await convexRunAsync(`${componentName}:gcOldAssets`, { + const gcOutput = await convexRunAsync(componentName, "lib:gcOldAssets", { currentDeploymentId: deploymentId, }); const gcResult = JSON.parse(gcOutput); - - // Handle both old format (number) and new format ({ deleted, blobIds }) - const deletedCount = typeof gcResult === "number" ? gcResult : gcResult.deleted; - const oldBlobIds: string[] = typeof gcResult === "object" && gcResult.blobIds ? gcResult.blobIds : []; + const deletedCount: number = gcResult.deleted; + const oldBlobIds: string[] = gcResult.blobIds ?? []; if (deletedCount > 0) { - console.log(`Cleaned up ${deletedCount} old storage file(s) from previous deployments`); + console.log( + `Cleaned up ${deletedCount} old storage file(s) from previous deployments`, + ); } - // Clean up old CDN blobs if any - if (oldBlobIds.length > 0) { - const cdnDeleteFn = args.cdnDeleteFunction || `${componentName}:deleteCdnBlobs`; + // Clean up old CDN blobs if the app exposes a delete function. Component + // actions can't reach the deployment-root /fs/blobs endpoint, so CDN GC + // remains an opt-in app-level function. + if (oldBlobIds.length > 0 && args.cdnDeleteFunction) { try { - await convexRunAsync(cdnDeleteFn, { blobIds: oldBlobIds }); - console.log(`Cleaned up ${oldBlobIds.length} old CDN blob(s) from previous deployments`); + await convexRunAsync(undefined, args.cdnDeleteFunction, { + blobIds: oldBlobIds, + }); + console.log( + `Cleaned up ${oldBlobIds.length} old CDN blob(s) from previous deployments`, + ); } catch { - console.warn(`Warning: Could not delete old CDN blobs. Make sure ${cdnDeleteFn} is defined.`); + console.warn( + `Warning: Could not delete old CDN blobs via ${args.cdnDeleteFunction}.`, + ); } + } else if (oldBlobIds.length > 0) { + console.log( + `${oldBlobIds.length} old CDN blob(s) left in place. Pass --cdn-delete-function to clean them up.`, + ); } console.log(""); diff --git a/src/client/index.test.ts b/src/client/index.test.ts deleted file mode 100644 index 765e535..0000000 --- a/src/client/index.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { exposeUploadApi, getMimeType } from "./index.js"; -import { anyApi, type ApiFromModules } from "convex/server"; -import { components, initConvexTest } from "./setup.test.js"; - -export const { generateUploadUrl, recordAsset, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -const testApi = ( - anyApi as unknown as ApiFromModules<{ - "index.test": { - generateUploadUrl: typeof generateUploadUrl; - recordAsset: typeof recordAsset; - gcOldAssets: typeof gcOldAssets; - listAssets: typeof listAssets; - }; - }> -)["index.test"]; - -describe("client tests", () => { - test("should expose upload API functions", async () => { - const t = initConvexTest(); - - // Test generateUploadUrl - const uploadUrl = await t.mutation(testApi.generateUploadUrl, {}); - expect(uploadUrl).toBeDefined(); - expect(typeof uploadUrl).toBe("string"); - }); - - test("should list empty assets initially", async () => { - const t = initConvexTest(); - - const assets = await t.query(testApi.listAssets, {}); - expect(assets).toHaveLength(0); - }); - - test("gc should return 0 with no assets", async () => { - const t = initConvexTest(); - - const result = await t.mutation(testApi.gcOldAssets, { - currentDeploymentId: "test-deployment", - }); - expect(result.deleted).toBe(0); - expect(result.blobIds).toHaveLength(0); - }); -}); - -describe("getMimeType", () => { - test("returns correct MIME types for common extensions", () => { - expect(getMimeType("/index.html")).toBe("text/html; charset=utf-8"); - expect(getMimeType("/assets/main.js")).toBe( - "application/javascript; charset=utf-8", - ); - expect(getMimeType("/styles/app.css")).toBe("text/css; charset=utf-8"); - expect(getMimeType("/data.json")).toBe("application/json; charset=utf-8"); - expect(getMimeType("/image.png")).toBe("image/png"); - expect(getMimeType("/photo.jpg")).toBe("image/jpeg"); - expect(getMimeType("/icon.svg")).toBe("image/svg+xml"); - expect(getMimeType("/favicon.ico")).toBe("image/x-icon"); - expect(getMimeType("/font.woff2")).toBe("font/woff2"); - }); - - test("returns octet-stream for unknown extensions", () => { - expect(getMimeType("/file.xyz")).toBe("application/octet-stream"); - expect(getMimeType("/unknown")).toBe("application/octet-stream"); - }); -}); diff --git a/src/client/index.ts b/src/client/index.ts index 6e4ccf5..e85e8c9 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,518 +1,25 @@ -import { - httpActionGeneric, - internalMutationGeneric, - internalQueryGeneric, - queryGeneric, -} from "convex/server"; -import type { HttpRouter } from "convex/server"; -import { v } from "convex/values"; +import { queryGeneric } from "convex/server"; import type { ComponentApi } from "../component/_generated/component.js"; -// MIME type mapping for common file types -const MIME_TYPES: Record = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", - ".webmanifest": "application/manifest+json", - ".xml": "application/xml", -}; - -/** - * Generate HTML page shown when no assets have been deployed yet. - */ -function getSetupHtml(): string { - return ` - - - - - Convex Static Hosting - - - -

Almost there!

-

Your Convex backend is running, but no static files have been deployed yet.

- -
- 1 - Build your frontend -
npm run build
-
- -
- 2 - Deploy your static files -
npx @convex-dev/static-hosting deploy
-
- -

Or deploy everything in one command:

-
npm run deploy
- -

- Learn more at github.com/get-convex/static-hosting -

- -`; -} - -/** - * Get MIME type for a file path based on its extension. - */ -export function getMimeType(path: string): string { - const ext = path.substring(path.lastIndexOf(".")).toLowerCase(); - return MIME_TYPES[ext] || "application/octet-stream"; -} - -/** - * Check if a path has a file extension. - */ -function hasFileExtension(path: string): boolean { - const lastSegment = path.split("/").pop() || ""; - return lastSegment.includes(".") && !lastSegment.startsWith("."); -} - -/** - * Check if asset is a hashed asset (for cache control). - * Vite produces: index-lj_vq_aF.js, style-B71cUw87.css - */ -function isHashedAsset(path: string): boolean { - return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); -} - -/** - * Register HTTP routes for serving static files. - * This creates a catch-all route that serves files from Convex storage - * with SPA fallback support. - * - * @param http - The HTTP router to register routes on - * @param component - The component API reference - * @param options - Configuration options - * @param options.pathPrefix - URL prefix for static files (default: "/") - * @param options.spaFallback - Enable SPA fallback to index.html (default: true) - * - * @example - * ```typescript - * // In your convex/http.ts - * import { httpRouter } from "convex/server"; - * import { registerStaticRoutes } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * const http = httpRouter(); - * - * // Serve static files at root - * registerStaticRoutes(http, components.selfHosting); - * - * // Or serve at a specific path prefix - * registerStaticRoutes(http, components.selfHosting, { - * pathPrefix: "/app", - * }); - * - * export default http; - * ``` - */ -/** - * Check if a content type is HTML. - */ -function isHtmlContentType(contentType: string): boolean { - return contentType.startsWith("text/html"); -} - -export function registerStaticRoutes( - http: HttpRouter, - component: ComponentApi, - { - pathPrefix = "/", - spaFallback = true, - cdnBaseUrl, - }: { - pathPrefix?: string; - spaFallback?: boolean; - /** Base URL for CDN blob redirects (e.g., `(req) => \`${new URL(req.url).origin}/fs/blobs\``). - * When set, assets with a blobId (non-HTML) will return a 302 redirect to `{cdnBaseUrl}/{blobId}`. */ - cdnBaseUrl?: string | ((request: Request) => string); - } = {}, -) { - // Normalize pathPrefix - ensure it starts with / and doesn't end with / - const normalizedPrefix = - pathPrefix === "/" ? "" : pathPrefix.replace(/\/$/, ""); - - const serveStaticFile = httpActionGeneric(async (ctx, request) => { - const url = new URL(request.url); - let path = url.pathname; - - // Remove prefix if present - if (normalizedPrefix && path.startsWith(normalizedPrefix)) { - path = path.slice(normalizedPrefix.length) || "/"; - } - - // Normalize: serve index.html for root - if (path === "" || path === "/") { - path = "/index.html"; - } - - // Look up the asset - type AssetDoc = { - _id: string; - _creationTime: number; - path: string; - storageId?: string; - blobId?: string; - contentType: string; - deploymentId: string; - } | null; - - let asset: AssetDoc = await ctx.runQuery(component.lib.getByPath, { path }); - - // SPA fallback: if not found and no file extension, serve index.html - if (!asset && spaFallback && !hasFileExtension(path)) { - asset = await ctx.runQuery(component.lib.getByPath, { - path: "/index.html", - }); - } - - // 404 if still not found - if (!asset) { - // If looking for index.html and it's not there, show setup instructions - if (path === "/index.html") { - return new Response(getSetupHtml(), { - status: 200, - headers: { "Content-Type": "text/html; charset=utf-8" }, - }); - } - return new Response("Not Found", { - status: 404, - headers: { "Content-Type": "text/plain" }, - }); - } - - // CDN redirect: if asset has blobId, is not HTML, and cdnBaseUrl is configured - if (asset.blobId && cdnBaseUrl && !isHtmlContentType(asset.contentType)) { - const baseUrl = - typeof cdnBaseUrl === "function" ? cdnBaseUrl(request) : cdnBaseUrl; - const redirectUrl = `${baseUrl.replace(/\/$/, "")}/${asset.blobId}`; - - // Cache control for redirect: hashed assets can cache the redirect itself - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; - - return new Response(null, { - status: 302, - headers: { - Location: redirectUrl, - "Cache-Control": cacheControl, - }, - }); - } - - // Serve from Convex storage - if (!asset.storageId) { - return new Response("Asset not available", { - status: 500, - headers: { "Content-Type": "text/plain" }, - }); - } - - // Use storageId as ETag (unique per file content) - const etag = `"${asset.storageId}"`; - - // Check for conditional request (If-None-Match) - const ifNoneMatch = request.headers.get("If-None-Match"); - if (ifNoneMatch === etag) { - // Client has current version - return 304 Not Modified - return new Response(null, { - status: 304, - headers: { - ETag: etag, - "Cache-Control": isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate", - }, - }); - } - - // Get file from storage - const blob = await ctx.storage.get(asset.storageId); - if (!blob) { - return new Response("Storage error", { - status: 500, - headers: { "Content-Type": "text/plain" }, - }); - } - - // Cache control: hashed assets can be cached forever - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; - - return new Response(blob, { - status: 200, - headers: { - "Content-Type": asset.contentType, - "Cache-Control": cacheControl, - ETag: etag, - "X-Content-Type-Options": "nosniff", - }, - }); - }); - - // Use pathPrefix routing - http.route({ - pathPrefix: pathPrefix === "/" ? "/" : `${normalizedPrefix}/`, - method: "GET", - handler: serveStaticFile, - }); - - // Also handle exact prefix without trailing slash - if (normalizedPrefix) { - http.route({ - path: normalizedPrefix, - method: "GET", - handler: serveStaticFile, - }); - } -} - -/** - * Expose the upload API as INTERNAL functions for secure deployments. - * These functions can only be called via `npx convex run` or from other Convex functions. - * - * @param component - The component API reference - * - * @example - * ```typescript - * // In your convex/staticHosting.ts - * import { exposeUploadApi } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - * exposeUploadApi(components.selfHosting); - * ``` - * - * Then deploy using: - * ```bash - * npm run deploy:static - * ``` - */ -export function exposeUploadApi(component: ComponentApi) { - return { - /** - * Generate a signed URL for uploading a file. - * Files are stored in the app's storage (not the component's). - */ - generateUploadUrl: internalMutationGeneric({ - args: {}, - handler: async (ctx) => { - return await ctx.storage.generateUploadUrl(); - }, - }), - - /** - * Record an uploaded asset in the database. - * Automatically cleans up old storage files when replacing. - * Pass storageId for Convex storage assets, or blobId for CDN assets. - */ - recordAsset: internalMutationGeneric({ - args: { - path: v.string(), - storageId: v.optional(v.string()), - blobId: v.optional(v.string()), - contentType: v.string(), - deploymentId: v.string(), - }, - handler: async (ctx, args) => { - const { oldStorageId, oldBlobId } = await ctx.runMutation( - component.lib.recordAsset, - { - path: args.path, - ...(args.storageId ? { storageId: args.storageId } : {}), - ...(args.blobId ? { blobId: args.blobId } : {}), - contentType: args.contentType, - deploymentId: args.deploymentId, - }, - ); - if (oldStorageId) { - try { - await ctx.storage.delete(oldStorageId); - } catch { - // Ignore - old file may have been in different storage - } - } - // Return oldBlobId so caller can clean up CDN blobs if needed - return oldBlobId ?? null; - }, - }), - - /** - * Garbage collect old assets and notify clients of the new deployment. - * Returns the count of deleted assets. - * Also triggers connected clients to reload via the deployment subscription. - */ - gcOldAssets: internalMutationGeneric({ - args: { - currentDeploymentId: v.string(), - }, - handler: async (ctx, args) => { - const { storageIds, blobIds } = await ctx.runMutation( - component.lib.gcOldAssets, - { - currentDeploymentId: args.currentDeploymentId, - }, - ); - for (const storageId of storageIds) { - try { - await ctx.storage.delete(storageId); - } catch { - // Ignore - old file may have been in different storage - } - } - - // Update deployment info to trigger client reloads - await ctx.runMutation(component.lib.setCurrentDeployment, { - deploymentId: args.currentDeploymentId, - }); - - // Return both counts and blobIds for CDN cleanup - return { deleted: storageIds.length, blobIds }; - }, - }), - - /** - * Generate multiple signed upload URLs in one call. - * Much faster than calling generateUploadUrl N times. - */ - generateUploadUrls: internalMutationGeneric({ - args: { count: v.number() }, - handler: async (ctx, { count }) => { - const urls: string[] = []; - for (let i = 0; i < count; i++) { - urls.push(await ctx.storage.generateUploadUrl()); - } - return urls; - }, - }), - - /** - * Record multiple uploaded assets in one call. - */ - recordAssets: internalMutationGeneric({ - args: { - assets: v.array( - v.object({ - path: v.string(), - storageId: v.string(), - contentType: v.string(), - deploymentId: v.string(), - }), - ), - }, - handler: async (ctx, { assets }) => { - for (const asset of assets) { - await ctx.runMutation(component.lib.recordAsset, { - path: asset.path, - storageId: asset.storageId, - contentType: asset.contentType, - deploymentId: asset.deploymentId, - }); - } - }, - }), - - /** - * List all static assets (for debugging). - */ - listAssets: internalQueryGeneric({ - args: { - limit: v.optional(v.number()), - }, - handler: async (ctx, args) => { - return await ctx.runQuery(component.lib.listAssets, { - limit: args.limit, - }); - }, - }), - }; -} - /** * Expose a query that clients can subscribe to for live reload on deploy. - * When a new deployment happens, subscribed clients will be notified. - * - * @param component - The component API reference + * This is only needed if you use `UpdateBanner` / `useDeploymentUpdates` from + * `@convex-dev/static-hosting/react`. If you don't surface deployment updates + * in your app, you don't need to call this. * * @example * ```typescript - * // In your convex/staticHosting.ts - * import { exposeUploadApi, exposeDeploymentQuery } from "@convex-dev/static-hosting"; + * // convex/staticHosting.ts + * import { exposeDeploymentQuery } from "@convex-dev/static-hosting"; * import { components } from "./_generated/api"; * - * export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - * exposeUploadApi(components.selfHosting); - * - * export const { getCurrentDeployment } = exposeDeploymentQuery(components.selfHosting); + * export const { getCurrentDeployment } = exposeDeploymentQuery( + * components.staticHosting, + * ); * ``` */ export function exposeDeploymentQuery(component: ComponentApi) { return { - /** - * Get the current deployment info. - * Subscribe to this query to detect when a new deployment happens. - */ getCurrentDeployment: queryGeneric({ args: {}, handler: async (ctx) => { @@ -523,13 +30,12 @@ export function exposeDeploymentQuery(component: ComponentApi) { } /** - * Derive the Convex cloud URL from a .convex.site hostname. - * Useful for client-side code that needs to connect to the Convex backend - * when hosted on Convex static hosting. + * Derive the Convex cloud URL from a `.convex.site` hostname. + * Useful when your frontend is served from Convex static hosting and needs + * to connect to its own Convex backend without an explicit env var. * * @example * ```typescript - * // In your React app's main.tsx * import { getConvexUrl } from "@convex-dev/static-hosting"; * * const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); @@ -540,14 +46,10 @@ export function getConvexUrl(): string { if (typeof window === "undefined") { throw new Error("getConvexUrl() can only be called in a browser context"); } - - // If hosted on Convex (.convex.site), derive API URL (.convex.cloud) if (window.location.hostname.endsWith(".convex.site")) { return `https://${window.location.hostname.replace(".convex.site", ".convex.cloud")}`; } - throw new Error( "Unable to derive Convex URL. Please set VITE_CONVEX_URL environment variable.", ); } - diff --git a/src/component/_generated/api.ts b/src/component/_generated/api.ts index e4e5cbd..d541c1b 100644 --- a/src/component/_generated/api.ts +++ b/src/component/_generated/api.ts @@ -8,6 +8,7 @@ * @module */ +import type * as http from "../http.js"; import type * as lib from "../lib.js"; import type { @@ -18,6 +19,7 @@ import type { import { anyApi, componentsGeneric } from "convex/server"; const fullApi: ApiFromModules<{ + http: typeof http; lib: typeof lib; }> = anyApi as any; diff --git a/src/component/_generated/component.ts b/src/component/_generated/component.ts index 4272d10..d8d2852 100644 --- a/src/component/_generated/component.ts +++ b/src/component/_generated/component.ts @@ -24,35 +24,6 @@ import type { FunctionReference } from "convex/server"; export type ComponentApi = { lib: { - gcOldAssets: FunctionReference< - "mutation", - "internal", - { currentDeploymentId: string }, - { blobIds: Array; storageIds: Array }, - Name - >; - generateUploadUrl: FunctionReference< - "mutation", - "internal", - {}, - string, - Name - >; - getByPath: FunctionReference< - "query", - "internal", - { path: string }, - { - _creationTime: number; - _id: string; - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - } | null, - Name - >; getCurrentDeployment: FunctionReference< "query", "internal", @@ -65,40 +36,5 @@ export type ComponentApi = } | null, Name >; - listAssets: FunctionReference< - "query", - "internal", - { limit?: number }, - Array<{ - _creationTime: number; - _id: string; - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - }>, - Name - >; - recordAsset: FunctionReference< - "mutation", - "internal", - { - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - }, - { oldBlobId: string | null; oldStorageId: string | null }, - Name - >; - setCurrentDeployment: FunctionReference< - "mutation", - "internal", - { deploymentId: string }, - null, - Name - >; }; }; diff --git a/src/component/convex.config.ts b/src/component/convex.config.ts index 1178996..dc76c53 100644 --- a/src/component/convex.config.ts +++ b/src/component/convex.config.ts @@ -1,3 +1,3 @@ import { defineComponent } from "convex/server"; -export default defineComponent("selfHosting"); +export default defineComponent("staticHosting"); diff --git a/src/component/http.ts b/src/component/http.ts new file mode 100644 index 0000000..6da2c2a --- /dev/null +++ b/src/component/http.ts @@ -0,0 +1,230 @@ +import { httpRouter } from "convex/server"; +import { httpAction } from "./_generated/server.js"; +import { internal } from "./_generated/api.js"; + +const MIME_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".txt": "text/plain; charset=utf-8", + ".map": "application/json", + ".webmanifest": "application/manifest+json", + ".xml": "application/xml", +}; + +function getMimeType(path: string): string { + const ext = path.substring(path.lastIndexOf(".")).toLowerCase(); + return MIME_TYPES[ext] || "application/octet-stream"; +} + +function hasFileExtension(path: string): boolean { + const lastSegment = path.split("/").pop() || ""; + return lastSegment.includes(".") && !lastSegment.startsWith("."); +} + +// Vite hashed asset suffix: e.g. `index-lj_vq_aF.js`, `style-B71cUw87.css` +function isHashedAsset(path: string): boolean { + return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); +} + +function isHtmlContentType(contentType: string): boolean { + return contentType.startsWith("text/html"); +} + +function getSetupHtml(): string { + return ` + + + + + Convex Static Hosting + + + +

Almost there!

+

Your Convex backend is running, but no static files have been deployed yet.

+ +
+ 1 + Build your frontend +
npm run build
+
+ +
+ 2 + Deploy your static files +
npx @convex-dev/static-hosting deploy
+
+ +

Or deploy everything in one command:

+
npm run deploy
+ +

+ Learn more at github.com/get-convex/static-hosting +

+ +`; +} + +// CONVEX_SITE_URL reflects the component's mount point (including any httpPrefix). +// We use it to strip the prefix from incoming request paths so lookups in +// the asset table remain relative to the component (e.g. `/index.html`). +function getMountPrefix(): string { + const siteUrl = process.env.CONVEX_SITE_URL; + if (!siteUrl) return ""; + try { + const pathname = new URL(siteUrl).pathname; + return pathname === "/" ? "" : pathname.replace(/\/$/, ""); + } catch { + return ""; + } +} + +const serveStaticFile = httpAction(async (ctx, request) => { + const url = new URL(request.url); + let path = url.pathname; + + const mountPrefix = getMountPrefix(); + if (mountPrefix && path.startsWith(mountPrefix)) { + path = path.slice(mountPrefix.length) || "/"; + } + + if (path === "" || path === "/") { + path = "/index.html"; + } + + let asset = await ctx.runQuery(internal.lib.getByPath, { path }); + + if (!asset && !hasFileExtension(path)) { + asset = await ctx.runQuery(internal.lib.getByPath, { path: "/index.html" }); + } + + if (!asset) { + if (path === "/index.html") { + return new Response(getSetupHtml(), { + status: 200, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + return new Response("Not Found", { + status: 404, + headers: { "Content-Type": "text/plain" }, + }); + } + + const contentType = asset.contentType || getMimeType(path); + + // CDN redirect: blobs are served by the platform at /fs/blobs/{id}, which + // lives at the deployment root (not under the component's prefix). + if (asset.blobId && !isHtmlContentType(contentType)) { + const redirectUrl = `${url.origin}/fs/blobs/${asset.blobId}`; + const cacheControl = isHashedAsset(path) + ? "public, max-age=31536000, immutable" + : "public, max-age=0, must-revalidate"; + return new Response(null, { + status: 302, + headers: { Location: redirectUrl, "Cache-Control": cacheControl }, + }); + } + + if (!asset.storageId) { + return new Response("Asset not available", { + status: 500, + headers: { "Content-Type": "text/plain" }, + }); + } + + const etag = `"${asset.storageId}"`; + const ifNoneMatch = request.headers.get("If-None-Match"); + const cacheControl = isHashedAsset(path) + ? "public, max-age=31536000, immutable" + : "public, max-age=0, must-revalidate"; + + if (ifNoneMatch === etag) { + return new Response(null, { + status: 304, + headers: { ETag: etag, "Cache-Control": cacheControl }, + }); + } + + const blob = await ctx.storage.get(asset.storageId); + if (!blob) { + return new Response("Storage error", { + status: 500, + headers: { "Content-Type": "text/plain" }, + }); + } + + return new Response(blob, { + status: 200, + headers: { + "Content-Type": contentType, + "Cache-Control": cacheControl, + ETag: etag, + "X-Content-Type-Options": "nosniff", + }, + }); +}); + +const http = httpRouter(); + +http.route({ + pathPrefix: "/", + method: "GET", + handler: serveStaticFile, +}); + +export default http; diff --git a/src/component/lib.test.ts b/src/component/lib.test.ts index 445b47e..bf86731 100644 --- a/src/component/lib.test.ts +++ b/src/component/lib.test.ts @@ -1,7 +1,7 @@ /// import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { api } from "./_generated/api.js"; +import { api, internal } from "./_generated/api.js"; import { initConvexTest } from "./setup.test.js"; describe("component lib", () => { @@ -12,99 +12,105 @@ describe("component lib", () => { vi.useRealTimers(); }); - test("can record and retrieve assets", async () => { + test("generates upload URLs", async () => { const t = initConvexTest(); - - // First upload a file to storage (mock with a fake storageId) - const uploadUrl = await t.mutation(api.lib.generateUploadUrl, {}); - expect(uploadUrl).toBeDefined(); + const uploadUrl = await t.mutation(internal.lib.generateUploadUrl, {}); expect(typeof uploadUrl).toBe("string"); + expect(uploadUrl.length).toBeGreaterThan(0); + + const urls = await t.mutation(internal.lib.generateUploadUrls, { count: 3 }); + expect(urls).toHaveLength(3); + for (const u of urls) { + expect(typeof u).toBe("string"); + } }); - test("can look up assets by path", async () => { + test("getByPath returns null when absent", async () => { const t = initConvexTest(); - - // Look up a non-existent path - const asset = await t.query(api.lib.getByPath, { path: "/index.html" }); + const asset = await t.query(internal.lib.getByPath, { path: "/index.html" }); expect(asset).toBeNull(); }); - test("can list assets", async () => { + test("listAssets is empty by default", async () => { const t = initConvexTest(); - - const assets = await t.query(api.lib.listAssets, {}); + const assets = await t.query(internal.lib.listAssets, {}); expect(assets).toHaveLength(0); }); - test("gc removes old assets", async () => { + test("gcOldAssets on empty db returns zero", async () => { const t = initConvexTest(); - - // GC with no assets should return empty arrays - const result = await t.mutation(api.lib.gcOldAssets, { - currentDeploymentId: "test-deployment", + const result = await t.mutation(internal.lib.gcOldAssets, { + currentDeploymentId: "deploy-1", }); - expect(result.storageIds).toHaveLength(0); + expect(result.deleted).toBe(0); expect(result.blobIds).toHaveLength(0); }); - test("recordAsset returns old IDs when replacing", async () => { + test("recordAsset stores and replaces blob assets", async () => { const t = initConvexTest(); - // Record a new asset (no previous) - should return nulls - const first = await t.mutation(api.lib.recordAsset, { + await t.mutation(internal.lib.recordAsset, { path: "/test.js", blobId: "blob-123", contentType: "application/javascript; charset=utf-8", deploymentId: "deploy-1", }); - expect(first.oldStorageId).toBeNull(); - expect(first.oldBlobId).toBeNull(); - // Replace with a new blobId - should return the old one - const second = await t.mutation(api.lib.recordAsset, { + const first = await t.query(internal.lib.getByPath, { path: "/test.js" }); + expect(first?.blobId).toBe("blob-123"); + + await t.mutation(internal.lib.recordAsset, { path: "/test.js", blobId: "blob-456", contentType: "application/javascript; charset=utf-8", deploymentId: "deploy-2", }); - expect(second.oldStorageId).toBeNull(); - expect(second.oldBlobId).toBe("blob-123"); + + const second = await t.query(internal.lib.getByPath, { path: "/test.js" }); + expect(second?.blobId).toBe("blob-456"); }); - test("gc returns blobIds for CDN assets", async () => { + test("gcOldAssets returns blobIds for old CDN assets and bumps deployment", async () => { const t = initConvexTest(); - // Record a CDN asset - await t.mutation(api.lib.recordAsset, { + await t.mutation(internal.lib.recordAsset, { path: "/assets/main.js", blobId: "blob-abc", contentType: "application/javascript; charset=utf-8", deploymentId: "deploy-old", }); - // GC should return the blobId - const result = await t.mutation(api.lib.gcOldAssets, { + const result = await t.mutation(internal.lib.gcOldAssets, { currentDeploymentId: "deploy-new", }); - expect(result.storageIds).toHaveLength(0); + expect(result.deleted).toBe(0); expect(result.blobIds).toEqual(["blob-abc"]); + + const deployment = await t.query(api.lib.getCurrentDeployment, {}); + expect(deployment?.currentDeploymentId).toBe("deploy-new"); }); - test("asset with blobId can be looked up by path", async () => { + test("recordAssets batches multiple inserts", async () => { const t = initConvexTest(); - await t.mutation(api.lib.recordAsset, { - path: "/assets/style.css", - blobId: "blob-xyz", - contentType: "text/css; charset=utf-8", - deploymentId: "deploy-1", + await t.mutation(internal.lib.recordAssets, { + assets: [ + { + path: "/a.js", + blobId: "blob-a", + contentType: "application/javascript; charset=utf-8", + deploymentId: "deploy-1", + }, + { + path: "/b.css", + blobId: "blob-b", + contentType: "text/css; charset=utf-8", + deploymentId: "deploy-1", + }, + ], }); - const asset = await t.query(api.lib.getByPath, { - path: "/assets/style.css", - }); - expect(asset).not.toBeNull(); - expect(asset!.blobId).toBe("blob-xyz"); - expect(asset!.storageId).toBeUndefined(); + const all = await t.query(internal.lib.listAssets, {}); + expect(all).toHaveLength(2); }); }); diff --git a/src/component/lib.ts b/src/component/lib.ts index 711efe6..8d0fdef 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -1,7 +1,10 @@ import { v } from "convex/values"; -import { mutation, query, internalMutation } from "./_generated/server.js"; +import { + internalMutation, + internalQuery, + query, +} from "./_generated/server.js"; -// Validator for static asset documents (including system fields) const staticAssetValidator = v.object({ _id: v.id("staticAssets"), _creationTime: v.number(), @@ -12,10 +15,22 @@ const staticAssetValidator = v.object({ deploymentId: v.string(), }); -/** - * Look up an asset by its URL path. - */ -export const getByPath = query({ +const deploymentInfoValidator = v.object({ + _id: v.id("deploymentInfo"), + _creationTime: v.number(), + currentDeploymentId: v.string(), + deployedAt: v.number(), +}); + +export const getCurrentDeployment = query({ + args: {}, + returns: v.union(deploymentInfoValidator, v.null()), + handler: async (ctx) => { + return await ctx.db.query("deploymentInfo").first(); + }, +}); + +export const getByPath = internalQuery({ args: { path: v.string() }, returns: v.union(staticAssetValidator, v.null()), handler: async (ctx, args) => { @@ -26,12 +41,18 @@ export const getByPath = query({ }, }); -/** - * Generate a signed URL for uploading a file to Convex storage. - * Note: This is kept for backwards compatibility but the recommended approach - * is to use the app's storage directly via exposeUploadApi(). - */ -export const generateUploadUrl = mutation({ +export const listAssets = internalQuery({ + args: { limit: v.optional(v.number()) }, + returns: v.array(staticAssetValidator), + handler: async (ctx, args) => { + return await ctx.db + .query("staticAssets") + .order("asc") + .take(args.limit ?? 100); + }, +}); + +export const generateUploadUrl = internalMutation({ args: {}, returns: v.string(), handler: async (ctx) => { @@ -39,42 +60,40 @@ export const generateUploadUrl = mutation({ }, }); -/** - * Record an asset in the database after uploading to storage. - * If an asset already exists at this path, returns the old storageId for cleanup. - * - * Note: Storage files are stored in the app's storage, not the component's storage. - * The caller is responsible for deleting the returned storageId from app storage. - */ -export const recordAsset = mutation({ - args: { - path: v.string(), - storageId: v.optional(v.id("_storage")), - blobId: v.optional(v.string()), - contentType: v.string(), - deploymentId: v.string(), +export const generateUploadUrls = internalMutation({ + args: { count: v.number() }, + returns: v.array(v.string()), + handler: async (ctx, { count }) => { + const urls: string[] = []; + for (let i = 0; i < count; i++) { + urls.push(await ctx.storage.generateUploadUrl()); + } + return urls; }, - returns: v.object({ - oldStorageId: v.union(v.id("_storage"), v.null()), - oldBlobId: v.union(v.string(), v.null()), - }), +}); + +const recordAssetFields = { + path: v.string(), + storageId: v.optional(v.id("_storage")), + blobId: v.optional(v.string()), + contentType: v.string(), + deploymentId: v.string(), +}; + +export const recordAsset = internalMutation({ + args: recordAssetFields, + returns: v.null(), handler: async (ctx, args) => { - // Check if asset already exists at this path const existing = await ctx.db .query("staticAssets") .withIndex("by_path", (q) => q.eq("path", args.path)) .unique(); - - let oldStorageId = null; - let oldBlobId = null; if (existing) { - oldStorageId = existing.storageId ?? null; - oldBlobId = existing.blobId ?? null; - // Delete old record + if (existing.storageId) { + await ctx.storage.delete(existing.storageId); + } await ctx.db.delete("staticAssets", existing._id); } - - // Insert new asset await ctx.db.insert("staticAssets", { path: args.path, ...(args.storageId ? { storageId: args.storageId } : {}), @@ -82,83 +101,52 @@ export const recordAsset = mutation({ contentType: args.contentType, deploymentId: args.deploymentId, }); - - // Return old IDs so caller can clean up - return { oldStorageId, oldBlobId }; + return null; }, }); -/** - * Garbage collect assets from old deployments. - * Returns the storageIds that need to be deleted from app storage. - */ -export const gcOldAssets = mutation({ - args: { - currentDeploymentId: v.string(), - }, - returns: v.object({ - storageIds: v.array(v.id("_storage")), - blobIds: v.array(v.string()), - }), - handler: async (ctx, args) => { - const oldAssets = await ctx.db.query("staticAssets").collect(); - const storageIds: Array = []; - const blobIds: Array = []; - - for (const asset of oldAssets) { - if (asset.deploymentId !== args.currentDeploymentId) { - if (asset.storageId) { - storageIds.push(asset.storageId as unknown as string); - } - if (asset.blobId) { - blobIds.push(asset.blobId); +export const recordAssets = internalMutation({ + args: { assets: v.array(v.object(recordAssetFields)) }, + returns: v.null(), + handler: async (ctx, { assets }) => { + for (const asset of assets) { + const existing = await ctx.db + .query("staticAssets") + .withIndex("by_path", (q) => q.eq("path", asset.path)) + .unique(); + if (existing) { + if (existing.storageId) { + await ctx.storage.delete(existing.storageId); } - // Delete database record - await ctx.db.delete("staticAssets", asset._id); + await ctx.db.delete("staticAssets", existing._id); } + await ctx.db.insert("staticAssets", { + path: asset.path, + ...(asset.storageId ? { storageId: asset.storageId } : {}), + ...(asset.blobId ? { blobId: asset.blobId } : {}), + contentType: asset.contentType, + deploymentId: asset.deploymentId, + }); } - - return { - storageIds: storageIds as unknown as Array>["type"]>, - blobIds, - }; - }, -}); - -/** - * List all assets (useful for debugging). - */ -export const listAssets = query({ - args: { - limit: v.optional(v.number()), - }, - returns: v.array(staticAssetValidator), - handler: async (ctx, args) => { - return await ctx.db - .query("staticAssets") - .order("asc") - .take(args.limit ?? 100); + return null; }, }); -/** - * Delete all assets records (useful for cleanup). - * Returns storageIds that need to be deleted from app storage. - */ -export const deleteAllAssets = internalMutation({ - args: {}, +export const gcOldAssets = internalMutation({ + args: { currentDeploymentId: v.string() }, returns: v.object({ - storageIds: v.array(v.id("_storage")), + deleted: v.number(), blobIds: v.array(v.string()), }), - handler: async (ctx) => { - const assets = await ctx.db.query("staticAssets").collect(); - const storageIds: Array = []; - const blobIds: Array = []; - - for (const asset of assets) { + handler: async (ctx, args) => { + const oldAssets = await ctx.db.query("staticAssets").collect(); + const blobIds: string[] = []; + let deleted = 0; + for (const asset of oldAssets) { + if (asset.deploymentId === args.currentDeploymentId) continue; if (asset.storageId) { - storageIds.push(asset.storageId as unknown as string); + await ctx.storage.delete(asset.storageId); + deleted++; } if (asset.blobId) { blobIds.push(asset.blobId); @@ -166,63 +154,18 @@ export const deleteAllAssets = internalMutation({ await ctx.db.delete("staticAssets", asset._id); } - return { - storageIds: storageIds as unknown as Array>["type"]>, - blobIds, - }; - }, -}); - -// ============================================================================ -// Deployment Tracking - for live reload on deploy -// ============================================================================ - -const deploymentInfoValidator = v.object({ - _id: v.id("deploymentInfo"), - _creationTime: v.number(), - currentDeploymentId: v.string(), - deployedAt: v.number(), -}); - -/** - * Get the current deployment info. - * Clients subscribe to this to detect when a new deployment happens. - */ -export const getCurrentDeployment = query({ - args: {}, - returns: v.union(deploymentInfoValidator, v.null()), - handler: async (ctx) => { - return await ctx.db.query("deploymentInfo").first(); - }, -}); - -/** - * Update the current deployment ID. - * Called after a successful deployment to notify all connected clients. - */ -export const setCurrentDeployment = mutation({ - args: { - deploymentId: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - // Get existing deployment info const existing = await ctx.db.query("deploymentInfo").first(); - if (existing) { - // Update existing record await ctx.db.patch("deploymentInfo", existing._id, { - currentDeploymentId: args.deploymentId, + currentDeploymentId: args.currentDeploymentId, deployedAt: Date.now(), }); } else { - // Create new record await ctx.db.insert("deploymentInfo", { - currentDeploymentId: args.deploymentId, + currentDeploymentId: args.currentDeploymentId, deployedAt: Date.now(), }); } - - return null; + return { deleted, blobIds }; }, }); diff --git a/src/react/index.tsx b/src/react/index.tsx index a56a2cf..3e75081 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -1,8 +1,8 @@ "use client"; -import { useQuery } from "convex/react"; +import { useQuery_experimental } from "convex/react"; import { useState, useMemo, type CSSProperties, type JSX } from "react"; -import type { FunctionReference } from "convex/server"; +import { makeFunctionReference, type FunctionReference } from "convex/server"; type DeploymentInfo = { _id: string; @@ -11,61 +11,70 @@ type DeploymentInfo = { deployedAt: number; } | null; +type DeploymentQueryRef = FunctionReference< + "query", + "public", + Record, + DeploymentInfo +>; + +const DEFAULT_QUERY_REF = makeFunctionReference< + "query", + Record, + DeploymentInfo +>("staticHosting:getCurrentDeployment") as DeploymentQueryRef; + +/* eslint-disable react-refresh/only-export-components */ + /** * Hook to detect when a new deployment is available. - * Shows a prompt to the user instead of auto-reloading. * - * @param getCurrentDeployment - The query function reference from exposeDeploymentQuery - * @returns Object with update status and reload function + * @param getCurrentDeployment - Optional query reference. Defaults to + * `api.staticHosting.getCurrentDeployment` (resolved by path). If you've + * re-exported the deployment query under a different module name, pass the + * reference explicitly. * * @example * ```tsx * import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; - * import { api } from "../convex/_generated/api"; * * function App() { - * const { updateAvailable, reload } = useDeploymentUpdates( - * api.staticHosting.getCurrentDeployment - * ); - * - * return ( - *
- * {updateAvailable && ( - *
- * A new version is available! - * - *
- * )} - * {/* rest of your app *\/} - *
- * ); + * const { updateAvailable, reload } = useDeploymentUpdates(); + * return updateAvailable ? : null; * } * ``` */ export function useDeploymentUpdates( - getCurrentDeployment: FunctionReference<"query", "public", Record, DeploymentInfo>, + getCurrentDeployment?: DeploymentQueryRef, ) { - const deployment = useQuery(getCurrentDeployment, {}); - const [initialDeploymentId, setInitialDeploymentId] = useState(null); - const [dismissedDeploymentId, setDismissedDeploymentId] = useState(null); + const result = useQuery_experimental({ + query: getCurrentDeployment ?? DEFAULT_QUERY_REF, + args: {}, + }); + + const deployment = result.status === "success" ? result.data : null; + const setupError = + result.status === "error" + ? deploymentQuerySetupHelp(getCurrentDeployment) + : null; + + const [initialDeploymentId, setInitialDeploymentId] = useState( + null, + ); + const [dismissedDeploymentId, setDismissedDeploymentId] = useState< + string | null + >(null); - // Capture the initial deployment ID on first load - // Using useState with functional update to avoid stale closure issues if (deployment && initialDeploymentId === null) { - // This is safe - we're setting initial state based on first data load - // It only runs once when deployment first becomes available setInitialDeploymentId(deployment.currentDeploymentId); } - // Derive updateAvailable from current state const updateAvailable = useMemo(() => { - if (!deployment || initialDeploymentId === null) { - return false; - } - // Show update if deployment changed from initial AND user hasn't dismissed this one - const hasNewDeployment = deployment.currentDeploymentId !== initialDeploymentId; - const isDismissed = deployment.currentDeploymentId === dismissedDeploymentId; - return hasNewDeployment && !isDismissed; + if (!deployment || initialDeploymentId === null) return false; + const hasNew = deployment.currentDeploymentId !== initialDeploymentId; + const isDismissed = + deployment.currentDeploymentId === dismissedDeploymentId; + return hasNew && !isDismissed; }, [deployment, initialDeploymentId, dismissedDeploymentId]); const reload = () => { @@ -79,36 +88,42 @@ export function useDeploymentUpdates( }; return { - /** True when a new deployment is available */ updateAvailable, - /** Reload the page to get the new version */ reload, - /** Dismiss the update notification (until next deploy) */ dismiss, - /** The current deployment info (or null if not yet loaded) */ deployment, + /** + * When set, the deployment query isn't exported from the app — the banner + * won't work until the user wires it up. The string is suitable to render + * during local development so the missing setup is obvious. + */ + setupError, }; } +function deploymentQuerySetupHelp(ref: DeploymentQueryRef | undefined): string { + if (ref) { + return "Deployment query failed. Verify the function you passed to useDeploymentUpdates is deployed."; + } + return [ + "@convex-dev/static-hosting: UpdateBanner requires you to expose the deployment query.", + "Create convex/staticHosting.ts:", + "", + ' import { exposeDeploymentQuery } from "@convex-dev/static-hosting";', + ' import { components } from "./_generated/api";', + " export const { getCurrentDeployment } = exposeDeploymentQuery(", + " components.staticHosting,", + " );", + ].join("\n"); +} + /** - * A ready-to-use update banner component. - * Displays a notification when a new deployment is available. + * Ready-to-use banner shown when a new deployment is available. * * @example * ```tsx * import { UpdateBanner } from "@convex-dev/static-hosting/react"; - * import { api } from "../convex/_generated/api"; - * - * function App() { - * return ( - *
- * - * {/* rest of your app *\/} - *
- * ); - * } + * * ``` */ export function UpdateBanner({ @@ -119,14 +134,22 @@ export function UpdateBanner({ className, style, }: { - getCurrentDeployment: FunctionReference<"query", "public", Record, DeploymentInfo>; + getCurrentDeployment?: DeploymentQueryRef; message?: string; buttonText?: string; dismissable?: boolean; className?: string; style?: CSSProperties; -}): JSX.Element | null { - const { updateAvailable, reload, dismiss } = useDeploymentUpdates(getCurrentDeployment); +} = {}): JSX.Element | null { + const { updateAvailable, reload, dismiss, setupError } = + useDeploymentUpdates(getCurrentDeployment); + + if (setupError) { + if (typeof console !== "undefined") { + console.warn(setupError); + } + return null; + } if (!updateAvailable) return null; diff --git a/src/test.ts b/src/test.ts index 5f443e9..51eccf8 100644 --- a/src/test.ts +++ b/src/test.ts @@ -11,7 +11,7 @@ const modules = import.meta.glob("./component/**/*.ts"); */ export function register( t: TestConvex>, - name: string = "selfHosting", + name: string = "staticHosting", ) { t.registerComponent(name, schema, modules); } From 67f1b9d1a775758e717af27f5c26e4614bcfd7fd Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 02:26:33 -0700 Subject: [PATCH 02/29] stop tracking dist - use prepare instead --- .gitignore | 4 +- dist/cli/deploy.d.ts | 16 - dist/cli/deploy.d.ts.map | 1 - dist/cli/index.d.ts | 14 - dist/cli/index.d.ts.map | 1 - dist/cli/index.js | 88 ---- dist/cli/index.js.map | 1 - dist/cli/init.js.map | 1 - dist/cli/setup.d.ts | 9 - dist/cli/setup.d.ts.map | 1 - dist/cli/setup.js | 78 --- dist/cli/setup.js.map | 1 - dist/cli/upload.d.ts.map | 1 - dist/client/_generated/_ignore.d.ts | 1 - dist/client/_generated/_ignore.d.ts.map | 1 - dist/client/_generated/_ignore.js | 3 - dist/client/_generated/_ignore.js.map | 1 - dist/client/index.d.ts | 142 ------ dist/client/index.d.ts.map | 1 - dist/client/index.js | 475 ------------------- dist/client/index.js.map | 1 - dist/component/_generated/api.d.ts | 34 -- dist/component/_generated/api.d.ts.map | 1 - dist/component/_generated/api.js | 31 -- dist/component/_generated/api.js.map | 1 - dist/component/_generated/component.d.ts | 73 --- dist/component/_generated/component.d.ts.map | 1 - dist/component/_generated/component.js | 11 - dist/component/_generated/component.js.map | 1 - dist/component/_generated/dataModel.d.ts | 46 -- dist/component/_generated/dataModel.d.ts.map | 1 - dist/component/_generated/dataModel.js | 11 - dist/component/_generated/dataModel.js.map | 1 - dist/component/_generated/server.d.ts | 121 ----- dist/component/_generated/server.d.ts.map | 1 - dist/component/_generated/server.js | 78 --- dist/component/_generated/server.js.map | 1 - dist/component/convex.config.d.ts | 3 - dist/component/convex.config.d.ts.map | 1 - dist/component/convex.config.js | 3 - dist/component/convex.config.js.map | 1 - dist/component/lib.d.ts.map | 1 - dist/component/lib.js | 210 -------- dist/component/lib.js.map | 1 - dist/component/schema.d.ts | 27 -- dist/component/schema.d.ts.map | 1 - dist/component/schema.js | 20 - dist/component/schema.js.map | 1 - dist/react/index.d.ts | 80 ---- dist/react/index.d.ts.map | 1 - dist/react/index.js | 138 ------ dist/react/index.js.map | 1 - package.json | 1 + 53 files changed, 3 insertions(+), 1741 deletions(-) delete mode 100644 dist/cli/deploy.d.ts delete mode 100644 dist/cli/deploy.d.ts.map delete mode 100644 dist/cli/index.d.ts delete mode 100644 dist/cli/index.d.ts.map delete mode 100644 dist/cli/index.js delete mode 100644 dist/cli/index.js.map delete mode 100644 dist/cli/init.js.map delete mode 100644 dist/cli/setup.d.ts delete mode 100644 dist/cli/setup.d.ts.map delete mode 100644 dist/cli/setup.js delete mode 100644 dist/cli/setup.js.map delete mode 100644 dist/cli/upload.d.ts.map delete mode 100644 dist/client/_generated/_ignore.d.ts delete mode 100644 dist/client/_generated/_ignore.d.ts.map delete mode 100644 dist/client/_generated/_ignore.js delete mode 100644 dist/client/_generated/_ignore.js.map delete mode 100644 dist/client/index.d.ts delete mode 100644 dist/client/index.d.ts.map delete mode 100644 dist/client/index.js delete mode 100644 dist/client/index.js.map delete mode 100644 dist/component/_generated/api.d.ts delete mode 100644 dist/component/_generated/api.d.ts.map delete mode 100644 dist/component/_generated/api.js delete mode 100644 dist/component/_generated/api.js.map delete mode 100644 dist/component/_generated/component.d.ts delete mode 100644 dist/component/_generated/component.d.ts.map delete mode 100644 dist/component/_generated/component.js delete mode 100644 dist/component/_generated/component.js.map delete mode 100644 dist/component/_generated/dataModel.d.ts delete mode 100644 dist/component/_generated/dataModel.d.ts.map delete mode 100644 dist/component/_generated/dataModel.js delete mode 100644 dist/component/_generated/dataModel.js.map delete mode 100644 dist/component/_generated/server.d.ts delete mode 100644 dist/component/_generated/server.d.ts.map delete mode 100644 dist/component/_generated/server.js delete mode 100644 dist/component/_generated/server.js.map delete mode 100644 dist/component/convex.config.d.ts delete mode 100644 dist/component/convex.config.d.ts.map delete mode 100644 dist/component/convex.config.js delete mode 100644 dist/component/convex.config.js.map delete mode 100644 dist/component/lib.d.ts.map delete mode 100644 dist/component/lib.js delete mode 100644 dist/component/lib.js.map delete mode 100644 dist/component/schema.d.ts delete mode 100644 dist/component/schema.d.ts.map delete mode 100644 dist/component/schema.js delete mode 100644 dist/component/schema.js.map delete mode 100644 dist/react/index.d.ts delete mode 100644 dist/react/index.d.ts.map delete mode 100644 dist/react/index.js delete mode 100644 dist/react/index.js.map diff --git a/.gitignore b/.gitignore index c66c730..f039dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,6 @@ node_modules *.swp .mcp.json next-example/ +dist/ +exmple/dist/ -# Note: dist/ is NOT ignored - it's committed for GitHub installs -# (npm install github:get-convex/static-hosting#main) diff --git a/dist/cli/deploy.d.ts b/dist/cli/deploy.d.ts deleted file mode 100644 index f23e96f..0000000 --- a/dist/cli/deploy.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env node -/** - * One-shot deployment command that deploys both Convex backend and static files. - * - * Usage: - * npx @convex-dev/static-hosting deploy [options] - * - * This command: - * 1. Builds the frontend with the correct VITE_CONVEX_URL - * 2. Deploys the Convex backend (npx convex deploy) - * 3. Deploys static files to Convex storage - * - * The goal is to minimize the inconsistency window between backend and frontend. - */ -export {}; -//# sourceMappingURL=deploy.d.ts.map \ No newline at end of file diff --git a/dist/cli/deploy.d.ts.map b/dist/cli/deploy.d.ts.map deleted file mode 100644 index 0dcf477..0000000 --- a/dist/cli/deploy.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/cli/deploy.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG"} \ No newline at end of file diff --git a/dist/cli/index.d.ts b/dist/cli/index.d.ts deleted file mode 100644 index 7a9e4fd..0000000 --- a/dist/cli/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -/** - * CLI for Convex Static Hosting - * - * Commands: - * deploy One-shot deployment (Convex backend + static files) - * upload Upload static files to Convex storage - * init Print setup instructions - */ -declare const command: string; -declare function main(): Promise; -declare function printHelp(): void; -declare function printInitInstructions(): void; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/cli/index.d.ts.map b/dist/cli/index.d.ts.map deleted file mode 100644 index e3c242e..0000000 --- a/dist/cli/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAC;AAEhC,iBAAe,IAAI,kBAkClB;AAED,iBAAS,SAAS,SAyBjB;AAED,iBAAS,qBAAqB,SAe7B"} \ No newline at end of file diff --git a/dist/cli/index.js b/dist/cli/index.js deleted file mode 100644 index 89923a9..0000000 --- a/dist/cli/index.js +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env node -"use strict"; -/** - * CLI for Convex Static Hosting - * - * Commands: - * deploy One-shot deployment (Convex backend + static files) - * upload Upload static files to Convex storage - * init Print setup instructions - */ -const command = process.argv[2]; -async function main() { - switch (command) { - case "setup": - await import("./setup.js"); - break; - case "deploy": - // Pass remaining args to deploy command - process.argv.splice(2, 1); - await import("./deploy.js"); - break; - case "upload": - // Pass remaining args to upload command - process.argv.splice(2, 1); - await import("./upload.js"); - break; - case "init": - printInitInstructions(); - break; - case "--help": - case "-h": - case undefined: - printHelp(); - break; - default: - console.error(`Unknown command: ${command}`); - console.log(""); - printHelp(); - process.exit(1); - } -} -function printHelp() { - console.log(` -Convex Static Hosting CLI - -Usage: - npx @convex-dev/static-hosting [options] - -Commands: - setup Interactive setup wizard (creates files, configures deployment) - deploy One-shot deployment (Convex backend + static files) - upload Upload static files to Convex storage - init Print setup instructions for integration - -Examples: - # Interactive setup (recommended for first-time users) - npx @convex-dev/static-hosting setup - - # One-shot deployment - npx @convex-dev/static-hosting deploy - - # Upload only (no Convex backend deploy) - npx @convex-dev/static-hosting upload --build --prod - -Run ' --help' for more information on a specific command. -`); -} -function printInitInstructions() { - console.log(` -📦 Convex Static Hosting - -Quick Start: - npx @convex-dev/static-hosting setup # Interactive setup wizard - -For LLMs: - Read INTEGRATION.md in this package for complete integration instructions - -Manual Setup: - See README.md at https://github.com/get-convex/static-hosting#readme - -This component hosts your static files in Convex storage and serves them via HTTP actions. -`); -} -main().catch((err) => { - console.error("Error:", err); - process.exit(1); -}); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/cli/index.js.map b/dist/cli/index.js.map deleted file mode 100644 index 3d557c6..0000000 --- a/dist/cli/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";;AACA;;;;;;;GAOG;AAEH,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEhC,KAAK,UAAU,IAAI;IACjB,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO;YACV,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YAC3B,MAAM;QAER,KAAK,QAAQ;YACX,wCAAwC;YACxC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC1B,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;YAC5B,MAAM;QAER,KAAK,QAAQ;YACX,wCAAwC;YACxC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC1B,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;YAC5B,MAAM;QAER,KAAK,MAAM;YACT,qBAAqB,EAAE,CAAC;YACxB,MAAM;QAER,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI,CAAC;QACV,KAAK,SAAS;YACZ,SAAS,EAAE,CAAC;YACZ,MAAM;QAER;YACE,OAAO,CAAC,KAAK,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChB,SAAS,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAuBb,CAAC,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB;IAC5B,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;CAab,CAAC,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/cli/init.js.map b/dist/cli/init.js.map deleted file mode 100644 index e72d6a4..0000000 --- a/dist/cli/init.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":";;AACA;;;;;GAKG;AAEH,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0KpB,CAAC;AAEF,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/cli/setup.d.ts b/dist/cli/setup.d.ts deleted file mode 100644 index 60d3435..0000000 --- a/dist/cli/setup.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node -/** - * Setup wizard for Convex Static Hosting. - * - * Usage: - * npx @convex-dev/static-hosting setup - */ -export {}; -//# sourceMappingURL=setup.d.ts.map \ No newline at end of file diff --git a/dist/cli/setup.d.ts.map b/dist/cli/setup.d.ts.map deleted file mode 100644 index a1524f4..0000000 --- a/dist/cli/setup.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/cli/setup.ts"],"names":[],"mappings":";AACA;;;;;GAKG"} \ No newline at end of file diff --git a/dist/cli/setup.js b/dist/cli/setup.js deleted file mode 100644 index 15222f4..0000000 --- a/dist/cli/setup.js +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env node -/** - * Setup wizard for Convex Static Hosting. - * - * Usage: - * npx @convex-dev/static-hosting setup - */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { join } from "path"; -function success(msg) { - console.log(`✓ ${msg}`); -} -function skip(msg) { - console.log(`· ${msg}`); -} -function createConvexConfig() { - const configPath = join(process.cwd(), "convex", "convex.config.ts"); - if (existsSync(configPath)) { - const existing = readFileSync(configPath, "utf-8"); - if (existing.includes("@convex-dev/static-hosting")) { - skip("convex/convex.config.ts (already configured)"); - return; - } - console.log("\n⚠️ convex/convex.config.ts exists. Add manually:"); - console.log(' import staticHosting from "@convex-dev/static-hosting/convex.config";'); - console.log(' app.use(staticHosting, { httpPrefix: "/" });\n'); - return; - } - writeFileSync(configPath, `import { defineApp } from "convex/server"; -import staticHosting from "@convex-dev/static-hosting/convex.config"; - -const app = defineApp(); -app.use(staticHosting, { httpPrefix: "/" }); - -export default app; -`); - success("Created convex/convex.config.ts"); -} -function updatePackageJson() { - const pkgPath = join(process.cwd(), "package.json"); - if (!existsSync(pkgPath)) { - console.log("⚠️ No package.json found"); - return; - } - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - if (!pkg.scripts) - pkg.scripts = {}; - if (pkg.scripts.deploy) { - skip("package.json deploy script (already exists)"); - return; - } - pkg.scripts.deploy = "npx @convex-dev/static-hosting deploy"; - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); - success("Added deploy script to package.json"); -} -function main() { - console.log("\n🚀 Convex Static Hosting Setup\n"); - if (!existsSync("convex")) { - mkdirSync("convex"); - success("Created convex/ directory"); - } - createConvexConfig(); - updatePackageJson(); - console.log("\n✨ Setup complete!\n"); - console.log("Next steps:\n"); - console.log(" 1. npx convex dev # Generate types"); - console.log(" 2. npm run deploy # Build and deploy\n"); - console.log("Your app will be at: https://.convex.site\n"); - console.log("Optional: to use from @convex-dev/static-hosting/react,"); - console.log("create convex/staticHosting.ts:\n"); - console.log(' import { exposeDeploymentQuery } from "@convex-dev/static-hosting";'); - console.log(' import { components } from "./_generated/api";'); - console.log(" export const { getCurrentDeployment } = exposeDeploymentQuery("); - console.log(" components.staticHosting,"); - console.log(" );\n"); -} -main(); -//# sourceMappingURL=setup.js.map \ No newline at end of file diff --git a/dist/cli/setup.js.map b/dist/cli/setup.js.map deleted file mode 100644 index c9029e3..0000000 --- a/dist/cli/setup.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/cli/setup.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli/upload.d.ts.map b/dist/cli/upload.d.ts.map deleted file mode 100644 index d0833fb..0000000 --- a/dist/cli/upload.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/cli/upload.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG"} \ No newline at end of file diff --git a/dist/client/_generated/_ignore.d.ts b/dist/client/_generated/_ignore.d.ts deleted file mode 100644 index 47d45e2..0000000 --- a/dist/client/_generated/_ignore.d.ts +++ /dev/null @@ -1 +0,0 @@ -//# sourceMappingURL=_ignore.d.ts.map \ No newline at end of file diff --git a/dist/client/_generated/_ignore.d.ts.map b/dist/client/_generated/_ignore.d.ts.map deleted file mode 100644 index d63db28..0000000 --- a/dist/client/_generated/_ignore.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"_ignore.d.ts","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/client/_generated/_ignore.js b/dist/client/_generated/_ignore.js deleted file mode 100644 index 4830897..0000000 --- a/dist/client/_generated/_ignore.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -// This is only here so convex-test can detect a _generated folder -//# sourceMappingURL=_ignore.js.map \ No newline at end of file diff --git a/dist/client/_generated/_ignore.js.map b/dist/client/_generated/_ignore.js.map deleted file mode 100644 index 03e5eac..0000000 --- a/dist/client/_generated/_ignore.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"_ignore.js","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":";AAAA,kEAAkE"} \ No newline at end of file diff --git a/dist/client/index.d.ts b/dist/client/index.d.ts deleted file mode 100644 index 61f3f43..0000000 --- a/dist/client/index.d.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { HttpRouter } from "convex/server"; -import type { ComponentApi } from "../component/_generated/component.js"; -/** - * Get MIME type for a file path based on its extension. - */ -export declare function getMimeType(path: string): string; -export declare function registerStaticRoutes(http: HttpRouter, component: ComponentApi, { pathPrefix, spaFallback, cdnBaseUrl, }?: { - pathPrefix?: string; - spaFallback?: boolean; - /** Base URL for CDN blob redirects (e.g., `(req) => \`${new URL(req.url).origin}/fs/blobs\``). - * When set, assets with a blobId (non-HTML) will return a 302 redirect to `{cdnBaseUrl}/{blobId}`. */ - cdnBaseUrl?: string | ((request: Request) => string); -}): void; -/** - * Expose the upload API as INTERNAL functions for secure deployments. - * These functions can only be called via `npx convex run` or from other Convex functions. - * - * @param component - The component API reference - * - * @example - * ```typescript - * // In your convex/staticHosting.ts - * import { exposeUploadApi } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - * exposeUploadApi(components.selfHosting); - * ``` - * - * Then deploy using: - * ```bash - * npm run deploy:static - * ``` - */ -export declare function exposeUploadApi(component: ComponentApi): { - /** - * Generate a signed URL for uploading a file. - * Files are stored in the app's storage (not the component's). - */ - generateUploadUrl: import("convex/server").RegisteredMutation<"internal", {}, Promise>; - /** - * Record an uploaded asset in the database. - * Automatically cleans up old storage files when replacing. - * Pass storageId for Convex storage assets, or blobId for CDN assets. - */ - recordAsset: import("convex/server").RegisteredMutation<"internal", { - blobId?: string | undefined; - storageId?: string | undefined; - path: string; - contentType: string; - deploymentId: string; - }, Promise>; - /** - * Garbage collect old assets and notify clients of the new deployment. - * Returns the count of deleted assets. - * Also triggers connected clients to reload via the deployment subscription. - */ - gcOldAssets: import("convex/server").RegisteredMutation<"internal", { - currentDeploymentId: string; - }, Promise<{ - deleted: number; - blobIds: string[]; - }>>; - /** - * Generate multiple signed upload URLs in one call. - * Much faster than calling generateUploadUrl N times. - */ - generateUploadUrls: import("convex/server").RegisteredMutation<"internal", { - count: number; - }, Promise>; - /** - * Record multiple uploaded assets in one call. - */ - recordAssets: import("convex/server").RegisteredMutation<"internal", { - assets: { - path: string; - contentType: string; - deploymentId: string; - storageId: string; - }[]; - }, Promise>; - /** - * List all static assets (for debugging). - */ - listAssets: import("convex/server").RegisteredQuery<"internal", { - limit?: number | undefined; - }, Promise<{ - _creationTime: number; - _id: string; - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - }[]>>; -}; -/** - * Expose a query that clients can subscribe to for live reload on deploy. - * When a new deployment happens, subscribed clients will be notified. - * - * @param component - The component API reference - * - * @example - * ```typescript - * // In your convex/staticHosting.ts - * import { exposeUploadApi, exposeDeploymentQuery } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - * exposeUploadApi(components.selfHosting); - * - * export const { getCurrentDeployment } = exposeDeploymentQuery(components.selfHosting); - * ``` - */ -export declare function exposeDeploymentQuery(component: ComponentApi): { - /** - * Get the current deployment info. - * Subscribe to this query to detect when a new deployment happens. - */ - getCurrentDeployment: import("convex/server").RegisteredQuery<"public", {}, Promise<{ - _creationTime: number; - _id: string; - currentDeploymentId: string; - deployedAt: number; - } | null>>; -}; -/** - * Derive the Convex cloud URL from a .convex.site hostname. - * Useful for client-side code that needs to connect to the Convex backend - * when hosted on Convex static hosting. - * - * @example - * ```typescript - * // In your React app's main.tsx - * import { getConvexUrl } from "@convex-dev/static-hosting"; - * - * const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); - * const convex = new ConvexReactClient(convexUrl); - * ``` - */ -export declare function getConvexUrl(): string; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/client/index.d.ts.map b/dist/client/index.d.ts.map deleted file mode 100644 index 26a3353..0000000 --- a/dist/client/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAwGzE;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhD;AAwDD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,YAAY,EACvB,EACE,UAAgB,EAChB,WAAkB,EAClB,UAAU,GACX,GAAE;IACD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;0GACsG;IACtG,UAAU,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CACjD,QA6IP;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,YAAY;IAEnD;;;OAGG;;IAQH;;;;OAIG;;;;;;;;IAgCH;;;;OAIG;;;;;;;IA8BH;;;OAGG;;;;IAYH;;OAEG;;;;;;;;;IAwBH;;OAEG;;;;;;;;;;;;EAYN;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,YAAY;IAEzD;;;OAGG;;;;;;;EAQN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAarC"} \ No newline at end of file diff --git a/dist/client/index.js b/dist/client/index.js deleted file mode 100644 index 6e79be8..0000000 --- a/dist/client/index.js +++ /dev/null @@ -1,475 +0,0 @@ -import { httpActionGeneric, internalMutationGeneric, internalQueryGeneric, queryGeneric, } from "convex/server"; -import { v } from "convex/values"; -// MIME type mapping for common file types -const MIME_TYPES = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", - ".webmanifest": "application/manifest+json", - ".xml": "application/xml", -}; -/** - * Generate HTML page shown when no assets have been deployed yet. - */ -function getSetupHtml() { - return ` - - - - - Convex Static Hosting - - - -

Almost there!

-

Your Convex backend is running, but no static files have been deployed yet.

- -
- 1 - Build your frontend -
npm run build
-
- -
- 2 - Deploy your static files -
npx @convex-dev/static-hosting deploy
-
- -

Or deploy everything in one command:

-
npm run deploy
- -

- Learn more at github.com/get-convex/static-hosting -

- -`; -} -/** - * Get MIME type for a file path based on its extension. - */ -export function getMimeType(path) { - const ext = path.substring(path.lastIndexOf(".")).toLowerCase(); - return MIME_TYPES[ext] || "application/octet-stream"; -} -/** - * Check if a path has a file extension. - */ -function hasFileExtension(path) { - const lastSegment = path.split("/").pop() || ""; - return lastSegment.includes(".") && !lastSegment.startsWith("."); -} -/** - * Check if asset is a hashed asset (for cache control). - * Vite produces: index-lj_vq_aF.js, style-B71cUw87.css - */ -function isHashedAsset(path) { - return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); -} -/** - * Register HTTP routes for serving static files. - * This creates a catch-all route that serves files from Convex storage - * with SPA fallback support. - * - * @param http - The HTTP router to register routes on - * @param component - The component API reference - * @param options - Configuration options - * @param options.pathPrefix - URL prefix for static files (default: "/") - * @param options.spaFallback - Enable SPA fallback to index.html (default: true) - * - * @example - * ```typescript - * // In your convex/http.ts - * import { httpRouter } from "convex/server"; - * import { registerStaticRoutes } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * const http = httpRouter(); - * - * // Serve static files at root - * registerStaticRoutes(http, components.selfHosting); - * - * // Or serve at a specific path prefix - * registerStaticRoutes(http, components.selfHosting, { - * pathPrefix: "/app", - * }); - * - * export default http; - * ``` - */ -/** - * Check if a content type is HTML. - */ -function isHtmlContentType(contentType) { - return contentType.startsWith("text/html"); -} -export function registerStaticRoutes(http, component, { pathPrefix = "/", spaFallback = true, cdnBaseUrl, } = {}) { - // Normalize pathPrefix - ensure it starts with / and doesn't end with / - const normalizedPrefix = pathPrefix === "/" ? "" : pathPrefix.replace(/\/$/, ""); - const serveStaticFile = httpActionGeneric(async (ctx, request) => { - const url = new URL(request.url); - let path = url.pathname; - // Remove prefix if present - if (normalizedPrefix && path.startsWith(normalizedPrefix)) { - path = path.slice(normalizedPrefix.length) || "/"; - } - // Normalize: serve index.html for root - if (path === "" || path === "/") { - path = "/index.html"; - } - let asset = await ctx.runQuery(component.lib.getByPath, { path }); - // SPA fallback: if not found and no file extension, serve index.html - if (!asset && spaFallback && !hasFileExtension(path)) { - asset = await ctx.runQuery(component.lib.getByPath, { - path: "/index.html", - }); - } - // 404 if still not found - if (!asset) { - // If looking for index.html and it's not there, show setup instructions - if (path === "/index.html") { - return new Response(getSetupHtml(), { - status: 200, - headers: { "Content-Type": "text/html; charset=utf-8" }, - }); - } - return new Response("Not Found", { - status: 404, - headers: { "Content-Type": "text/plain" }, - }); - } - // CDN redirect: if asset has blobId, is not HTML, and cdnBaseUrl is configured - if (asset.blobId && cdnBaseUrl && !isHtmlContentType(asset.contentType)) { - const baseUrl = typeof cdnBaseUrl === "function" ? cdnBaseUrl(request) : cdnBaseUrl; - const redirectUrl = `${baseUrl.replace(/\/$/, "")}/${asset.blobId}`; - // Cache control for redirect: hashed assets can cache the redirect itself - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; - return new Response(null, { - status: 302, - headers: { - Location: redirectUrl, - "Cache-Control": cacheControl, - }, - }); - } - // Serve from Convex storage - if (!asset.storageId) { - return new Response("Asset not available", { - status: 500, - headers: { "Content-Type": "text/plain" }, - }); - } - // Use storageId as ETag (unique per file content) - const etag = `"${asset.storageId}"`; - // Check for conditional request (If-None-Match) - const ifNoneMatch = request.headers.get("If-None-Match"); - if (ifNoneMatch === etag) { - // Client has current version - return 304 Not Modified - return new Response(null, { - status: 304, - headers: { - ETag: etag, - "Cache-Control": isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate", - }, - }); - } - // Get file from storage - const blob = await ctx.storage.get(asset.storageId); - if (!blob) { - return new Response("Storage error", { - status: 500, - headers: { "Content-Type": "text/plain" }, - }); - } - // Cache control: hashed assets can be cached forever - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; - return new Response(blob, { - status: 200, - headers: { - "Content-Type": asset.contentType, - "Cache-Control": cacheControl, - ETag: etag, - "X-Content-Type-Options": "nosniff", - }, - }); - }); - // Use pathPrefix routing - http.route({ - pathPrefix: pathPrefix === "/" ? "/" : `${normalizedPrefix}/`, - method: "GET", - handler: serveStaticFile, - }); - // Also handle exact prefix without trailing slash - if (normalizedPrefix) { - http.route({ - path: normalizedPrefix, - method: "GET", - handler: serveStaticFile, - }); - } -} -/** - * Expose the upload API as INTERNAL functions for secure deployments. - * These functions can only be called via `npx convex run` or from other Convex functions. - * - * @param component - The component API reference - * - * @example - * ```typescript - * // In your convex/staticHosting.ts - * import { exposeUploadApi } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - * exposeUploadApi(components.selfHosting); - * ``` - * - * Then deploy using: - * ```bash - * npm run deploy:static - * ``` - */ -export function exposeUploadApi(component) { - return { - /** - * Generate a signed URL for uploading a file. - * Files are stored in the app's storage (not the component's). - */ - generateUploadUrl: internalMutationGeneric({ - args: {}, - handler: async (ctx) => { - return await ctx.storage.generateUploadUrl(); - }, - }), - /** - * Record an uploaded asset in the database. - * Automatically cleans up old storage files when replacing. - * Pass storageId for Convex storage assets, or blobId for CDN assets. - */ - recordAsset: internalMutationGeneric({ - args: { - path: v.string(), - storageId: v.optional(v.string()), - blobId: v.optional(v.string()), - contentType: v.string(), - deploymentId: v.string(), - }, - handler: async (ctx, args) => { - const { oldStorageId, oldBlobId } = await ctx.runMutation(component.lib.recordAsset, { - path: args.path, - ...(args.storageId ? { storageId: args.storageId } : {}), - ...(args.blobId ? { blobId: args.blobId } : {}), - contentType: args.contentType, - deploymentId: args.deploymentId, - }); - if (oldStorageId) { - try { - await ctx.storage.delete(oldStorageId); - } - catch { - // Ignore - old file may have been in different storage - } - } - // Return oldBlobId so caller can clean up CDN blobs if needed - return oldBlobId ?? null; - }, - }), - /** - * Garbage collect old assets and notify clients of the new deployment. - * Returns the count of deleted assets. - * Also triggers connected clients to reload via the deployment subscription. - */ - gcOldAssets: internalMutationGeneric({ - args: { - currentDeploymentId: v.string(), - }, - handler: async (ctx, args) => { - const { storageIds, blobIds } = await ctx.runMutation(component.lib.gcOldAssets, { - currentDeploymentId: args.currentDeploymentId, - }); - for (const storageId of storageIds) { - try { - await ctx.storage.delete(storageId); - } - catch { - // Ignore - old file may have been in different storage - } - } - // Update deployment info to trigger client reloads - await ctx.runMutation(component.lib.setCurrentDeployment, { - deploymentId: args.currentDeploymentId, - }); - // Return both counts and blobIds for CDN cleanup - return { deleted: storageIds.length, blobIds }; - }, - }), - /** - * Generate multiple signed upload URLs in one call. - * Much faster than calling generateUploadUrl N times. - */ - generateUploadUrls: internalMutationGeneric({ - args: { count: v.number() }, - handler: async (ctx, { count }) => { - const urls = []; - for (let i = 0; i < count; i++) { - urls.push(await ctx.storage.generateUploadUrl()); - } - return urls; - }, - }), - /** - * Record multiple uploaded assets in one call. - */ - recordAssets: internalMutationGeneric({ - args: { - assets: v.array(v.object({ - path: v.string(), - storageId: v.string(), - contentType: v.string(), - deploymentId: v.string(), - })), - }, - handler: async (ctx, { assets }) => { - for (const asset of assets) { - await ctx.runMutation(component.lib.recordAsset, { - path: asset.path, - storageId: asset.storageId, - contentType: asset.contentType, - deploymentId: asset.deploymentId, - }); - } - }, - }), - /** - * List all static assets (for debugging). - */ - listAssets: internalQueryGeneric({ - args: { - limit: v.optional(v.number()), - }, - handler: async (ctx, args) => { - return await ctx.runQuery(component.lib.listAssets, { - limit: args.limit, - }); - }, - }), - }; -} -/** - * Expose a query that clients can subscribe to for live reload on deploy. - * When a new deployment happens, subscribed clients will be notified. - * - * @param component - The component API reference - * - * @example - * ```typescript - * // In your convex/staticHosting.ts - * import { exposeUploadApi, exposeDeploymentQuery } from "@convex-dev/static-hosting"; - * import { components } from "./_generated/api"; - * - * export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - * exposeUploadApi(components.selfHosting); - * - * export const { getCurrentDeployment } = exposeDeploymentQuery(components.selfHosting); - * ``` - */ -export function exposeDeploymentQuery(component) { - return { - /** - * Get the current deployment info. - * Subscribe to this query to detect when a new deployment happens. - */ - getCurrentDeployment: queryGeneric({ - args: {}, - handler: async (ctx) => { - return await ctx.runQuery(component.lib.getCurrentDeployment, {}); - }, - }), - }; -} -/** - * Derive the Convex cloud URL from a .convex.site hostname. - * Useful for client-side code that needs to connect to the Convex backend - * when hosted on Convex static hosting. - * - * @example - * ```typescript - * // In your React app's main.tsx - * import { getConvexUrl } from "@convex-dev/static-hosting"; - * - * const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); - * const convex = new ConvexReactClient(convexUrl); - * ``` - */ -export function getConvexUrl() { - if (typeof window === "undefined") { - throw new Error("getConvexUrl() can only be called in a browser context"); - } - // If hosted on Convex (.convex.site), derive API URL (.convex.cloud) - if (window.location.hostname.endsWith(".convex.site")) { - return `https://${window.location.hostname.replace(".convex.site", ".convex.cloud")}`; - } - throw new Error("Unable to derive Convex URL. Please set VITE_CONVEX_URL environment variable."); -} -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/client/index.js.map b/dist/client/index.js.map deleted file mode 100644 index 983189a..0000000 --- a/dist/client/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,YAAY,GACb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAGlC,0CAA0C;AAC1C,MAAM,UAAU,GAA2B;IACzC,OAAO,EAAE,0BAA0B;IACnC,KAAK,EAAE,uCAAuC;IAC9C,MAAM,EAAE,uCAAuC;IAC/C,MAAM,EAAE,yBAAyB;IACjC,OAAO,EAAE,iCAAiC;IAC1C,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,cAAc;IACtB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,WAAW;IACpB,QAAQ,EAAE,YAAY;IACtB,MAAM,EAAE,UAAU;IAClB,MAAM,EAAE,2BAA2B;IACnC,MAAM,EAAE,kBAAkB;IAC1B,cAAc,EAAE,2BAA2B;IAC3C,MAAM,EAAE,iBAAiB;CAC1B,CAAC;AAEF;;GAEG;AACH,SAAS,YAAY;IACnB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAwED,CAAC;AACT,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAChE,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;AACvD,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAChD,OAAO,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH;;GAEG;AACH,SAAS,iBAAiB,CAAC,WAAmB;IAC5C,OAAO,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAgB,EAChB,SAAuB,EACvB,EACE,UAAU,GAAG,GAAG,EAChB,WAAW,GAAG,IAAI,EAClB,UAAU,MAOR,EAAE;IAEN,wEAAwE;IACxE,MAAM,gBAAgB,GACpB,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE1D,MAAM,eAAe,GAAG,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;QAC/D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;QAExB,2BAA2B;QAC3B,IAAI,gBAAgB,IAAI,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC1D,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC;QACpD,CAAC;QAED,uCAAuC;QACvC,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YAChC,IAAI,GAAG,aAAa,CAAC;QACvB,CAAC;QAaD,IAAI,KAAK,GAAa,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAE5E,qEAAqE;QACrE,IAAI,CAAC,KAAK,IAAI,WAAW,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrD,KAAK,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE;gBAClD,IAAI,EAAE,aAAa;aACpB,CAAC,CAAC;QACL,CAAC;QAED,yBAAyB;QACzB,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,wEAAwE;YACxE,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;gBAC3B,OAAO,IAAI,QAAQ,CAAC,YAAY,EAAE,EAAE;oBAClC,MAAM,EAAE,GAAG;oBACX,OAAO,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE;iBACxD,CAAC,CAAC;YACL,CAAC;YACD,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE;gBAC/B,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAED,+EAA+E;QAC/E,IAAI,KAAK,CAAC,MAAM,IAAI,UAAU,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACxE,MAAM,OAAO,GACX,OAAO,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;YACtE,MAAM,WAAW,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAEpE,0EAA0E;YAC1E,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC;gBACtC,CAAC,CAAC,qCAAqC;gBACvC,CAAC,CAAC,oCAAoC,CAAC;YAEzC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;gBACxB,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACP,QAAQ,EAAE,WAAW;oBACrB,eAAe,EAAE,YAAY;iBAC9B;aACF,CAAC,CAAC;QACL,CAAC;QAED,4BAA4B;QAC5B,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;YACrB,OAAO,IAAI,QAAQ,CAAC,qBAAqB,EAAE;gBACzC,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAED,kDAAkD;QAClD,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,GAAG,CAAC;QAEpC,gDAAgD;QAChD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACzD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YACzB,uDAAuD;YACvD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;gBACxB,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACP,IAAI,EAAE,IAAI;oBACV,eAAe,EAAE,aAAa,CAAC,IAAI,CAAC;wBAClC,CAAC,CAAC,qCAAqC;wBACvC,CAAC,CAAC,oCAAoC;iBACzC;aACF,CAAC,CAAC;QACL,CAAC;QAED,wBAAwB;QACxB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE;gBACnC,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAED,qDAAqD;QACrD,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC;YACtC,CAAC,CAAC,qCAAqC;YACvC,CAAC,CAAC,oCAAoC,CAAC;QAEzC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,cAAc,EAAE,KAAK,CAAC,WAAW;gBACjC,eAAe,EAAE,YAAY;gBAC7B,IAAI,EAAE,IAAI;gBACV,wBAAwB,EAAE,SAAS;aACpC;SACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,yBAAyB;IACzB,IAAI,CAAC,KAAK,CAAC;QACT,UAAU,EAAE,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,gBAAgB,GAAG;QAC7D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,eAAe;KACzB,CAAC,CAAC;IAEH,kDAAkD;IAClD,IAAI,gBAAgB,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,CAAC;YACT,IAAI,EAAE,gBAAgB;YACtB,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,SAAuB;IACrD,OAAO;QACL;;;WAGG;QACH,iBAAiB,EAAE,uBAAuB,CAAC;YACzC,IAAI,EAAE,EAAE;YACR,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBACrB,OAAO,MAAM,GAAG,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;YAC/C,CAAC;SACF,CAAC;QAEF;;;;WAIG;QACH,WAAW,EAAE,uBAAuB,CAAC;YACnC,IAAI,EAAE;gBACJ,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;gBAChB,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;gBACjC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC9B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;gBACvB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;aACzB;YACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;gBAC3B,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,CAAC,WAAW,CACvD,SAAS,CAAC,GAAG,CAAC,WAAW,EACzB;oBACE,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACxD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC/C,WAAW,EAAE,IAAI,CAAC,WAAW;oBAC7B,YAAY,EAAE,IAAI,CAAC,YAAY;iBAChC,CACF,CAAC;gBACF,IAAI,YAAY,EAAE,CAAC;oBACjB,IAAI,CAAC;wBACH,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;oBACzC,CAAC;oBAAC,MAAM,CAAC;wBACP,uDAAuD;oBACzD,CAAC;gBACH,CAAC;gBACD,8DAA8D;gBAC9D,OAAO,SAAS,IAAI,IAAI,CAAC;YAC3B,CAAC;SACF,CAAC;QAEF;;;;WAIG;QACH,WAAW,EAAE,uBAAuB,CAAC;YACnC,IAAI,EAAE;gBACJ,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE;aAChC;YACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;gBAC3B,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,CAAC,WAAW,CACnD,SAAS,CAAC,GAAG,CAAC,WAAW,EACzB;oBACE,mBAAmB,EAAE,IAAI,CAAC,mBAAmB;iBAC9C,CACF,CAAC;gBACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,IAAI,CAAC;wBACH,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;oBACtC,CAAC;oBAAC,MAAM,CAAC;wBACP,uDAAuD;oBACzD,CAAC;gBACH,CAAC;gBAED,mDAAmD;gBACnD,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,oBAAoB,EAAE;oBACxD,YAAY,EAAE,IAAI,CAAC,mBAAmB;iBACvC,CAAC,CAAC;gBAEH,iDAAiD;gBACjD,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YACjD,CAAC;SACF,CAAC;QAEF;;;WAGG;QACH,kBAAkB,EAAE,uBAAuB,CAAC;YAC1C,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE;YAC3B,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;gBAChC,MAAM,IAAI,GAAa,EAAE,CAAC;gBAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC/B,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;gBACnD,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;SACF,CAAC;QAEF;;WAEG;QACH,YAAY,EAAE,uBAAuB,CAAC;YACpC,IAAI,EAAE;gBACJ,MAAM,EAAE,CAAC,CAAC,KAAK,CACb,CAAC,CAAC,MAAM,CAAC;oBACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;oBAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;oBACrB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;oBACvB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;iBACzB,CAAC,CACH;aACF;YACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;gBACjC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;oBAC3B,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE;wBAC/C,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,SAAS,EAAE,KAAK,CAAC,SAAS;wBAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;wBAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;qBACjC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;QAEF;;WAEG;QACH,UAAU,EAAE,oBAAoB,CAAC;YAC/B,IAAI,EAAE;gBACJ,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;aAC9B;YACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;gBAC3B,OAAO,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE;oBAClD,KAAK,EAAE,IAAI,CAAC,KAAK;iBAClB,CAAC,CAAC;YACL,CAAC;SACF,CAAC;KACH,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAuB;IAC3D,OAAO;QACL;;;WAGG;QACH,oBAAoB,EAAE,YAAY,CAAC;YACjC,IAAI,EAAE,EAAE;YACR,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBACrB,OAAO,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;YACpE,CAAC;SACF,CAAC;KACH,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IAED,qEAAqE;IACrE,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACtD,OAAO,WAAW,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,eAAe,CAAC,EAAE,CAAC;IACxF,CAAC;IAED,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/api.d.ts b/dist/component/_generated/api.d.ts deleted file mode 100644 index 890d363..0000000 --- a/dist/component/_generated/api.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Generated `api` utility. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import type * as lib from "../lib.js"; -import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server"; -declare const fullApi: ApiFromModules<{ - lib: typeof lib; -}>; -/** - * A utility for referencing Convex functions in your app's public API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ -export declare const api: FilterApi>; -/** - * A utility for referencing Convex functions in your app's internal API. - * - * Usage: - * ```js - * const myFunctionReference = internal.myModule.myFunction; - * ``` - */ -export declare const internal: FilterApi>; -export declare const components: {}; -export {}; -//# sourceMappingURL=api.d.ts.map \ No newline at end of file diff --git a/dist/component/_generated/api.d.ts.map b/dist/component/_generated/api.d.ts.map deleted file mode 100644 index 6c74bed..0000000 --- a/dist/component/_generated/api.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/api.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,KAAK,GAAG,MAAM,WAAW,CAAC;AAEtC,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,iBAAiB,EAClB,MAAM,eAAe,CAAC;AAGvB,QAAA,MAAM,OAAO,EAAE,cAAc,CAAC;IAC5B,GAAG,EAAE,OAAO,GAAG,CAAC;CACjB,CAAiB,CAAC;AAEnB;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG,EAAE,SAAS,CACzB,OAAO,OAAO,EACd,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CACjB,CAAC;AAElB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAC9B,OAAO,OAAO,EACd,iBAAiB,CAAC,GAAG,EAAE,UAAU,CAAC,CACnB,CAAC;AAElB,eAAO,MAAM,UAAU,EAAqC,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/api.js b/dist/component/_generated/api.js deleted file mode 100644 index 591ee61..0000000 --- a/dist/component/_generated/api.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable */ -/** - * Generated `api` utility. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import { anyApi, componentsGeneric } from "convex/server"; -const fullApi = anyApi; -/** - * A utility for referencing Convex functions in your app's public API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ -export const api = anyApi; -/** - * A utility for referencing Convex functions in your app's internal API. - * - * Usage: - * ```js - * const myFunctionReference = internal.myModule.myFunction; - * ``` - */ -export const internal = anyApi; -export const components = componentsGeneric(); -//# sourceMappingURL=api.js.map \ No newline at end of file diff --git a/dist/component/_generated/api.js.map b/dist/component/_generated/api.js.map deleted file mode 100644 index 531ca5b..0000000 --- a/dist/component/_generated/api.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/component/_generated/api.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AASH,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE1D,MAAM,OAAO,GAER,MAAa,CAAC;AAEnB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,GAAG,GAGZ,MAAa,CAAC;AAElB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAGjB,MAAa,CAAC;AAElB,MAAM,CAAC,MAAM,UAAU,GAAG,iBAAiB,EAAmB,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/component.d.ts b/dist/component/_generated/component.d.ts deleted file mode 100644 index ea9226f..0000000 --- a/dist/component/_generated/component.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Generated `ComponentApi` utility. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import type { FunctionReference } from "convex/server"; -/** - * A utility for referencing a Convex component's exposed API. - * - * Useful when expecting a parameter like `components.myComponent`. - * Usage: - * ```ts - * async function myFunction(ctx: QueryCtx, component: ComponentApi) { - * return ctx.runQuery(component.someFile.someQuery, { ...args }); - * } - * ``` - */ -export type ComponentApi = { - lib: { - gcOldAssets: FunctionReference<"mutation", "internal", { - currentDeploymentId: string; - }, { - blobIds: Array; - storageIds: Array; - }, Name>; - generateUploadUrl: FunctionReference<"mutation", "internal", {}, string, Name>; - getByPath: FunctionReference<"query", "internal", { - path: string; - }, { - _creationTime: number; - _id: string; - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - } | null, Name>; - getCurrentDeployment: FunctionReference<"query", "internal", {}, { - _creationTime: number; - _id: string; - currentDeploymentId: string; - deployedAt: number; - } | null, Name>; - listAssets: FunctionReference<"query", "internal", { - limit?: number; - }, Array<{ - _creationTime: number; - _id: string; - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - }>, Name>; - recordAsset: FunctionReference<"mutation", "internal", { - blobId?: string; - contentType: string; - deploymentId: string; - path: string; - storageId?: string; - }, { - oldBlobId: string | null; - oldStorageId: string | null; - }, Name>; - setCurrentDeployment: FunctionReference<"mutation", "internal", { - deploymentId: string; - }, null, Name>; - }; -}; -//# sourceMappingURL=component.d.ts.map \ No newline at end of file diff --git a/dist/component/_generated/component.d.ts.map b/dist/component/_generated/component.d.ts.map deleted file mode 100644 index 60a698f..0000000 --- a/dist/component/_generated/component.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/component.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,YAAY,CAAC,IAAI,SAAS,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,IAC3E;IACE,GAAG,EAAE;QACH,WAAW,EAAE,iBAAiB,CAC5B,UAAU,EACV,UAAU,EACV;YAAE,mBAAmB,EAAE,MAAM,CAAA;SAAE,EAC/B;YAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;YAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;SAAE,EACrD,IAAI,CACL,CAAC;QACF,iBAAiB,EAAE,iBAAiB,CAClC,UAAU,EACV,UAAU,EACV,EAAE,EACF,MAAM,EACN,IAAI,CACL,CAAC;QACF,SAAS,EAAE,iBAAiB,CAC1B,OAAO,EACP,UAAU,EACV;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAChB;YACE,aAAa,EAAE,MAAM,CAAC;YACtB,GAAG,EAAE,MAAM,CAAC;YACZ,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,WAAW,EAAE,MAAM,CAAC;YACpB,YAAY,EAAE,MAAM,CAAC;YACrB,IAAI,EAAE,MAAM,CAAC;YACb,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,GAAG,IAAI,EACR,IAAI,CACL,CAAC;QACF,oBAAoB,EAAE,iBAAiB,CACrC,OAAO,EACP,UAAU,EACV,EAAE,EACF;YACE,aAAa,EAAE,MAAM,CAAC;YACtB,GAAG,EAAE,MAAM,CAAC;YACZ,mBAAmB,EAAE,MAAM,CAAC;YAC5B,UAAU,EAAE,MAAM,CAAC;SACpB,GAAG,IAAI,EACR,IAAI,CACL,CAAC;QACF,UAAU,EAAE,iBAAiB,CAC3B,OAAO,EACP,UAAU,EACV;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,EAClB,KAAK,CAAC;YACJ,aAAa,EAAE,MAAM,CAAC;YACtB,GAAG,EAAE,MAAM,CAAC;YACZ,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,WAAW,EAAE,MAAM,CAAC;YACpB,YAAY,EAAE,MAAM,CAAC;YACrB,IAAI,EAAE,MAAM,CAAC;YACb,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC,EACF,IAAI,CACL,CAAC;QACF,WAAW,EAAE,iBAAiB,CAC5B,UAAU,EACV,UAAU,EACV;YACE,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,WAAW,EAAE,MAAM,CAAC;YACpB,YAAY,EAAE,MAAM,CAAC;YACrB,IAAI,EAAE,MAAM,CAAC;YACb,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,EACD;YAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;YAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,EACzD,IAAI,CACL,CAAC;QACF,oBAAoB,EAAE,iBAAiB,CACrC,UAAU,EACV,UAAU,EACV;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,EACxB,IAAI,EACJ,IAAI,CACL,CAAC;KACH,CAAC;CACH,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/component.js b/dist/component/_generated/component.js deleted file mode 100644 index 92c00d6..0000000 --- a/dist/component/_generated/component.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -/** - * Generated `ComponentApi` utility. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -export {}; -//# sourceMappingURL=component.js.map \ No newline at end of file diff --git a/dist/component/_generated/component.js.map b/dist/component/_generated/component.js.map deleted file mode 100644 index 0177537..0000000 --- a/dist/component/_generated/component.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"component.js","sourceRoot":"","sources":["../../../src/component/_generated/component.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG"} \ No newline at end of file diff --git a/dist/component/_generated/dataModel.d.ts b/dist/component/_generated/dataModel.d.ts deleted file mode 100644 index 684a3b1..0000000 --- a/dist/component/_generated/dataModel.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Generated data model types. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import type { DataModelFromSchemaDefinition, DocumentByName, TableNamesInDataModel, SystemTableNames } from "convex/server"; -import type { GenericId } from "convex/values"; -import schema from "../schema.js"; -/** - * The names of all of your Convex tables. - */ -export type TableNames = TableNamesInDataModel; -/** - * The type of a document stored in Convex. - * - * @typeParam TableName - A string literal type of the table name (like "users"). - */ -export type Doc = DocumentByName; -/** - * An identifier for a document in Convex. - * - * Convex documents are uniquely identified by their `Id`, which is accessible - * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). - * - * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. - * - * IDs are just strings at runtime, but this type can be used to distinguish them from other - * strings when type checking. - * - * @typeParam TableName - A string literal type of the table name (like "users"). - */ -export type Id = GenericId; -/** - * A type describing your Convex data model. - * - * This type includes information about what tables you have, the type of - * documents stored in those tables, and the indexes defined on them. - * - * This type is used to parameterize methods like `queryGeneric` and - * `mutationGeneric` to make them type-safe. - */ -export type DataModel = DataModelFromSchemaDefinition; -//# sourceMappingURL=dataModel.d.ts.map \ No newline at end of file diff --git a/dist/component/_generated/dataModel.d.ts.map b/dist/component/_generated/dataModel.d.ts.map deleted file mode 100644 index f8369b1..0000000 --- a/dist/component/_generated/dataModel.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"dataModel.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/dataModel.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,6BAA6B,EAC7B,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,MAAM,MAAM,cAAc,CAAC;AAElC;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AAE1D;;;;GAIG;AACH,MAAM,MAAM,GAAG,CAAC,SAAS,SAAS,UAAU,IAAI,cAAc,CAC5D,SAAS,EACT,SAAS,CACV,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,EAAE,CAAC,SAAS,SAAS,UAAU,GAAG,gBAAgB,IAC5D,SAAS,CAAC,SAAS,CAAC,CAAC;AAEvB;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,6BAA6B,CAAC,OAAO,MAAM,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/dataModel.js b/dist/component/_generated/dataModel.js deleted file mode 100644 index 3ca9892..0000000 --- a/dist/component/_generated/dataModel.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -/** - * Generated data model types. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import schema from "../schema.js"; -//# sourceMappingURL=dataModel.js.map \ No newline at end of file diff --git a/dist/component/_generated/dataModel.js.map b/dist/component/_generated/dataModel.js.map deleted file mode 100644 index 440dc01..0000000 --- a/dist/component/_generated/dataModel.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"dataModel.js","sourceRoot":"","sources":["../../../src/component/_generated/dataModel.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AASH,OAAO,MAAM,MAAM,cAAc,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/server.d.ts b/dist/component/_generated/server.d.ts deleted file mode 100644 index e3e98cf..0000000 --- a/dist/component/_generated/server.d.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Generated utilities for implementing server-side Convex query and mutation functions. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import type { ActionBuilder, HttpActionBuilder, MutationBuilder, QueryBuilder, GenericActionCtx, GenericMutationCtx, GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter } from "convex/server"; -import type { DataModel } from "./dataModel.js"; -/** - * Define a query in this Convex app's public API. - * - * This function will be allowed to read your Convex database and will be accessible from the client. - * - * @param func - The query function. It receives a {@link QueryCtx} as its first argument. - * @returns The wrapped query. Include this as an `export` to name it and make it accessible. - */ -export declare const query: QueryBuilder; -/** - * Define a query that is only accessible from other Convex functions (but not from the client). - * - * This function will be allowed to read from your Convex database. It will not be accessible from the client. - * - * @param func - The query function. It receives a {@link QueryCtx} as its first argument. - * @returns The wrapped query. Include this as an `export` to name it and make it accessible. - */ -export declare const internalQuery: QueryBuilder; -/** - * Define a mutation in this Convex app's public API. - * - * This function will be allowed to modify your Convex database and will be accessible from the client. - * - * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. - * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. - */ -export declare const mutation: MutationBuilder; -/** - * Define a mutation that is only accessible from other Convex functions (but not from the client). - * - * This function will be allowed to modify your Convex database. It will not be accessible from the client. - * - * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. - * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. - */ -export declare const internalMutation: MutationBuilder; -/** - * Define an action in this Convex app's public API. - * - * An action is a function which can execute any JavaScript code, including non-deterministic - * code and code with side-effects, like calling third-party services. - * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. - * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. - * - * @param func - The action. It receives an {@link ActionCtx} as its first argument. - * @returns The wrapped action. Include this as an `export` to name it and make it accessible. - */ -export declare const action: ActionBuilder; -/** - * Define an action that is only accessible from other Convex functions (but not from the client). - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument. - * @returns The wrapped function. Include this as an `export` to name it and make it accessible. - */ -export declare const internalAction: ActionBuilder; -/** - * Define an HTTP action. - * - * The wrapped function will be used to respond to HTTP requests received - * by a Convex deployment if the requests matches the path and method where - * this action is routed. Be sure to route your httpAction in `convex/http.js`. - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument - * and a Fetch API `Request` object as its second. - * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. - */ -export declare const httpAction: HttpActionBuilder; -/** - * A set of services for use within Convex query functions. - * - * The query context is passed as the first argument to any Convex query - * function run on the server. - * - * If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead. - */ -export type QueryCtx = GenericQueryCtx; -/** - * A set of services for use within Convex mutation functions. - * - * The mutation context is passed as the first argument to any Convex mutation - * function run on the server. - * - * If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead. - */ -export type MutationCtx = GenericMutationCtx; -/** - * A set of services for use within Convex action functions. - * - * The action context is passed as the first argument to any Convex action - * function run on the server. - */ -export type ActionCtx = GenericActionCtx; -/** - * An interface to read from the database within Convex query functions. - * - * The two entry points are {@link DatabaseReader.get}, which fetches a single - * document by its {@link Id}, or {@link DatabaseReader.query}, which starts - * building a query. - */ -export type DatabaseReader = GenericDatabaseReader; -/** - * An interface to read from and write to the database within Convex mutation - * functions. - * - * Convex guarantees that all writes within a single mutation are - * executed atomically, so you never have to worry about partial writes leaving - * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) - * for the guarantees Convex provides your functions. - */ -export type DatabaseWriter = GenericDatabaseWriter; -//# sourceMappingURL=server.d.ts.map \ No newline at end of file diff --git a/dist/component/_generated/server.d.ts.map b/dist/component/_generated/server.d.ts.map deleted file mode 100644 index fd09414..0000000 --- a/dist/component/_generated/server.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/server.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAUvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,EAAE,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAgB,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,YAAY,CAAC,SAAS,EAAE,UAAU,CACxC,CAAC;AAEvB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,eAAe,CAAC,SAAS,EAAE,UAAU,CAC3C,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,MAAM,EAAE,aAAa,CAAC,SAAS,EAAE,QAAQ,CAAiB,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,SAAS,EAAE,UAAU,CACzC,CAAC;AAExB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,EAAE,iBAAqC,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/component/_generated/server.js b/dist/component/_generated/server.js deleted file mode 100644 index daf1c86..0000000 --- a/dist/component/_generated/server.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable */ -/** - * Generated utilities for implementing server-side Convex query and mutation functions. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ -import { actionGeneric, httpActionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalMutationGeneric, internalQueryGeneric, } from "convex/server"; -/** - * Define a query in this Convex app's public API. - * - * This function will be allowed to read your Convex database and will be accessible from the client. - * - * @param func - The query function. It receives a {@link QueryCtx} as its first argument. - * @returns The wrapped query. Include this as an `export` to name it and make it accessible. - */ -export const query = queryGeneric; -/** - * Define a query that is only accessible from other Convex functions (but not from the client). - * - * This function will be allowed to read from your Convex database. It will not be accessible from the client. - * - * @param func - The query function. It receives a {@link QueryCtx} as its first argument. - * @returns The wrapped query. Include this as an `export` to name it and make it accessible. - */ -export const internalQuery = internalQueryGeneric; -/** - * Define a mutation in this Convex app's public API. - * - * This function will be allowed to modify your Convex database and will be accessible from the client. - * - * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. - * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. - */ -export const mutation = mutationGeneric; -/** - * Define a mutation that is only accessible from other Convex functions (but not from the client). - * - * This function will be allowed to modify your Convex database. It will not be accessible from the client. - * - * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. - * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. - */ -export const internalMutation = internalMutationGeneric; -/** - * Define an action in this Convex app's public API. - * - * An action is a function which can execute any JavaScript code, including non-deterministic - * code and code with side-effects, like calling third-party services. - * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. - * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. - * - * @param func - The action. It receives an {@link ActionCtx} as its first argument. - * @returns The wrapped action. Include this as an `export` to name it and make it accessible. - */ -export const action = actionGeneric; -/** - * Define an action that is only accessible from other Convex functions (but not from the client). - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument. - * @returns The wrapped function. Include this as an `export` to name it and make it accessible. - */ -export const internalAction = internalActionGeneric; -/** - * Define an HTTP action. - * - * The wrapped function will be used to respond to HTTP requests received - * by a Convex deployment if the requests matches the path and method where - * this action is routed. Be sure to route your httpAction in `convex/http.js`. - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument - * and a Fetch API `Request` object as its second. - * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. - */ -export const httpAction = httpActionGeneric; -//# sourceMappingURL=server.js.map \ No newline at end of file diff --git a/dist/component/_generated/server.js.map b/dist/component/_generated/server.js.map deleted file mode 100644 index 21e2407..0000000 --- a/dist/component/_generated/server.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/component/_generated/server.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AAaH,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAGvB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsC,YAAY,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GACxB,oBAAoB,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAyC,eAAe,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,uBAAuB,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,MAAM,GAAuC,aAAa,CAAC;AAExE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GACzB,qBAAqB,CAAC;AAExB;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAsB,iBAAiB,CAAC"} \ No newline at end of file diff --git a/dist/component/convex.config.d.ts b/dist/component/convex.config.d.ts deleted file mode 100644 index bda44cf..0000000 --- a/dist/component/convex.config.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const _default: import("convex/server").ComponentDefinition; -export default _default; -//# sourceMappingURL=convex.config.d.ts.map \ No newline at end of file diff --git a/dist/component/convex.config.d.ts.map b/dist/component/convex.config.d.ts.map deleted file mode 100644 index 6bee52c..0000000 --- a/dist/component/convex.config.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"convex.config.d.ts","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":";AAEA,wBAA8C"} \ No newline at end of file diff --git a/dist/component/convex.config.js b/dist/component/convex.config.js deleted file mode 100644 index f619490..0000000 --- a/dist/component/convex.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { defineComponent } from "convex/server"; -export default defineComponent("selfHosting"); -//# sourceMappingURL=convex.config.js.map \ No newline at end of file diff --git a/dist/component/convex.config.js.map b/dist/component/convex.config.js.map deleted file mode 100644 index 5a4635c..0000000 --- a/dist/component/convex.config.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"convex.config.js","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,eAAe,eAAe,CAAC,aAAa,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/component/lib.d.ts.map b/dist/component/lib.d.ts.map deleted file mode 100644 index 7386b29..0000000 --- a/dist/component/lib.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAclC;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;UASpB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,2EAM5B,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,WAAW;;;;;;;;;GAwCtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,WAAW;;;gBA2BmB,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;;GAI3F,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;KAWrB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;gBAsBe,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;;GAI3F,CAAC;AAaH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;UAM/B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;iBAyB/B,CAAC"} \ No newline at end of file diff --git a/dist/component/lib.js b/dist/component/lib.js deleted file mode 100644 index 96bb4fc..0000000 --- a/dist/component/lib.js +++ /dev/null @@ -1,210 +0,0 @@ -import { v } from "convex/values"; -import { mutation, query, internalMutation } from "./_generated/server.js"; -// Validator for static asset documents (including system fields) -const staticAssetValidator = v.object({ - _id: v.id("staticAssets"), - _creationTime: v.number(), - path: v.string(), - storageId: v.optional(v.id("_storage")), - blobId: v.optional(v.string()), - contentType: v.string(), - deploymentId: v.string(), -}); -/** - * Look up an asset by its URL path. - */ -export const getByPath = query({ - args: { path: v.string() }, - returns: v.union(staticAssetValidator, v.null()), - handler: async (ctx, args) => { - return await ctx.db - .query("staticAssets") - .withIndex("by_path", (q) => q.eq("path", args.path)) - .unique(); - }, -}); -/** - * Generate a signed URL for uploading a file to Convex storage. - * Note: This is kept for backwards compatibility but the recommended approach - * is to use the app's storage directly via exposeUploadApi(). - */ -export const generateUploadUrl = mutation({ - args: {}, - returns: v.string(), - handler: async (ctx) => { - return await ctx.storage.generateUploadUrl(); - }, -}); -/** - * Record an asset in the database after uploading to storage. - * If an asset already exists at this path, returns the old storageId for cleanup. - * - * Note: Storage files are stored in the app's storage, not the component's storage. - * The caller is responsible for deleting the returned storageId from app storage. - */ -export const recordAsset = mutation({ - args: { - path: v.string(), - storageId: v.optional(v.id("_storage")), - blobId: v.optional(v.string()), - contentType: v.string(), - deploymentId: v.string(), - }, - returns: v.object({ - oldStorageId: v.union(v.id("_storage"), v.null()), - oldBlobId: v.union(v.string(), v.null()), - }), - handler: async (ctx, args) => { - // Check if asset already exists at this path - const existing = await ctx.db - .query("staticAssets") - .withIndex("by_path", (q) => q.eq("path", args.path)) - .unique(); - let oldStorageId = null; - let oldBlobId = null; - if (existing) { - oldStorageId = existing.storageId ?? null; - oldBlobId = existing.blobId ?? null; - // Delete old record - await ctx.db.delete("staticAssets", existing._id); - } - // Insert new asset - await ctx.db.insert("staticAssets", { - path: args.path, - ...(args.storageId ? { storageId: args.storageId } : {}), - ...(args.blobId ? { blobId: args.blobId } : {}), - contentType: args.contentType, - deploymentId: args.deploymentId, - }); - // Return old IDs so caller can clean up - return { oldStorageId, oldBlobId }; - }, -}); -/** - * Garbage collect assets from old deployments. - * Returns the storageIds that need to be deleted from app storage. - */ -export const gcOldAssets = mutation({ - args: { - currentDeploymentId: v.string(), - }, - returns: v.object({ - storageIds: v.array(v.id("_storage")), - blobIds: v.array(v.string()), - }), - handler: async (ctx, args) => { - const oldAssets = await ctx.db.query("staticAssets").collect(); - const storageIds = []; - const blobIds = []; - for (const asset of oldAssets) { - if (asset.deploymentId !== args.currentDeploymentId) { - if (asset.storageId) { - storageIds.push(asset.storageId); - } - if (asset.blobId) { - blobIds.push(asset.blobId); - } - // Delete database record - await ctx.db.delete("staticAssets", asset._id); - } - } - return { - storageIds: storageIds, - blobIds, - }; - }, -}); -/** - * List all assets (useful for debugging). - */ -export const listAssets = query({ - args: { - limit: v.optional(v.number()), - }, - returns: v.array(staticAssetValidator), - handler: async (ctx, args) => { - return await ctx.db - .query("staticAssets") - .order("asc") - .take(args.limit ?? 100); - }, -}); -/** - * Delete all assets records (useful for cleanup). - * Returns storageIds that need to be deleted from app storage. - */ -export const deleteAllAssets = internalMutation({ - args: {}, - returns: v.object({ - storageIds: v.array(v.id("_storage")), - blobIds: v.array(v.string()), - }), - handler: async (ctx) => { - const assets = await ctx.db.query("staticAssets").collect(); - const storageIds = []; - const blobIds = []; - for (const asset of assets) { - if (asset.storageId) { - storageIds.push(asset.storageId); - } - if (asset.blobId) { - blobIds.push(asset.blobId); - } - await ctx.db.delete("staticAssets", asset._id); - } - return { - storageIds: storageIds, - blobIds, - }; - }, -}); -// ============================================================================ -// Deployment Tracking - for live reload on deploy -// ============================================================================ -const deploymentInfoValidator = v.object({ - _id: v.id("deploymentInfo"), - _creationTime: v.number(), - currentDeploymentId: v.string(), - deployedAt: v.number(), -}); -/** - * Get the current deployment info. - * Clients subscribe to this to detect when a new deployment happens. - */ -export const getCurrentDeployment = query({ - args: {}, - returns: v.union(deploymentInfoValidator, v.null()), - handler: async (ctx) => { - return await ctx.db.query("deploymentInfo").first(); - }, -}); -/** - * Update the current deployment ID. - * Called after a successful deployment to notify all connected clients. - */ -export const setCurrentDeployment = mutation({ - args: { - deploymentId: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - // Get existing deployment info - const existing = await ctx.db.query("deploymentInfo").first(); - if (existing) { - // Update existing record - await ctx.db.patch("deploymentInfo", existing._id, { - currentDeploymentId: args.deploymentId, - deployedAt: Date.now(), - }); - } - else { - // Create new record - await ctx.db.insert("deploymentInfo", { - currentDeploymentId: args.deploymentId, - deployedAt: Date.now(), - }); - } - return null; - }, -}); -//# sourceMappingURL=lib.js.map \ No newline at end of file diff --git a/dist/component/lib.js.map b/dist/component/lib.js.map deleted file mode 100644 index 261b767..0000000 --- a/dist/component/lib.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"lib.js","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/component/schema.d.ts b/dist/component/schema.d.ts deleted file mode 100644 index 06ad3b8..0000000 --- a/dist/component/schema.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -declare const _default: import("convex/server").SchemaDefinition<{ - staticAssets: import("convex/server").TableDefinition | undefined; - path: string; - contentType: string; - deploymentId: string; - }, { - path: import("convex/values").VString; - storageId: import("convex/values").VId | undefined, "optional">; - blobId: import("convex/values").VString; - contentType: import("convex/values").VString; - deploymentId: import("convex/values").VString; - }, "required", "path" | "blobId" | "contentType" | "deploymentId" | "storageId">, { - by_path: ["path", "_creationTime"]; - by_deploymentId: ["deploymentId", "_creationTime"]; - }, {}, {}>; - deploymentInfo: import("convex/server").TableDefinition; - deployedAt: import("convex/values").VFloat64; - }, "required", "currentDeploymentId" | "deployedAt">, {}, {}, {}>; -}, true>; -export default _default; -//# sourceMappingURL=schema.d.ts.map \ No newline at end of file diff --git a/dist/component/schema.d.ts.map b/dist/component/schema.d.ts.map deleted file mode 100644 index 2423b45..0000000 --- a/dist/component/schema.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAGA,wBAiBG"} \ No newline at end of file diff --git a/dist/component/schema.js b/dist/component/schema.js deleted file mode 100644 index e8eed88..0000000 --- a/dist/component/schema.js +++ /dev/null @@ -1,20 +0,0 @@ -import { defineSchema, defineTable } from "convex/server"; -import { v } from "convex/values"; -export default defineSchema({ - staticAssets: defineTable({ - path: v.string(), // URL path, e.g., "/index.html", "/assets/main-abc123.js" - storageId: v.optional(v.id("_storage")), // Reference to Convex file storage (used for HTML + non-CDN assets) - blobId: v.optional(v.string()), // convex-fs blob ID (used for CDN-served assets) - contentType: v.string(), // MIME type, e.g., "text/html; charset=utf-8" - deploymentId: v.string(), // UUID for garbage collection - }) - .index("by_path", ["path"]) - .index("by_deploymentId", ["deploymentId"]), - // Singleton table to track the current deployment - // Clients subscribe to this to know when to reload - deploymentInfo: defineTable({ - currentDeploymentId: v.string(), - deployedAt: v.number(), // timestamp - }), -}); -//# sourceMappingURL=schema.js.map \ No newline at end of file diff --git a/dist/component/schema.js.map b/dist/component/schema.js.map deleted file mode 100644 index 5de2066..0000000 --- a/dist/component/schema.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAElC,eAAe,YAAY,CAAC;IAC1B,YAAY,EAAE,WAAW,CAAC;QACxB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,0DAA0D;QAC5E,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,oEAAoE;QAC7G,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,iDAAiD;QACjF,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,8CAA8C;QACvE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,8BAA8B;KACzD,CAAC;SACC,KAAK,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC;SAC1B,KAAK,CAAC,iBAAiB,EAAE,CAAC,cAAc,CAAC,CAAC;IAE7C,kDAAkD;IAClD,mDAAmD;IACnD,cAAc,EAAE,WAAW,CAAC;QAC1B,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE;QAC/B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,YAAY;KACrC,CAAC;CACH,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/react/index.d.ts b/dist/react/index.d.ts deleted file mode 100644 index 10bfe03..0000000 --- a/dist/react/index.d.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { type CSSProperties, type JSX } from "react"; -import type { FunctionReference } from "convex/server"; -type DeploymentInfo = { - _id: string; - _creationTime: number; - currentDeploymentId: string; - deployedAt: number; -} | null; -/** - * Hook to detect when a new deployment is available. - * Shows a prompt to the user instead of auto-reloading. - * - * @param getCurrentDeployment - The query function reference from exposeDeploymentQuery - * @returns Object with update status and reload function - * - * @example - * ```tsx - * import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; - * import { api } from "../convex/_generated/api"; - * - * function App() { - * const { updateAvailable, reload } = useDeploymentUpdates( - * api.staticHosting.getCurrentDeployment - * ); - * - * return ( - *
- * {updateAvailable && ( - *
- * A new version is available! - * - *
- * )} - * {/* rest of your app *\/} - *
- * ); - * } - * ``` - */ -export declare function useDeploymentUpdates(getCurrentDeployment: FunctionReference<"query", "public", Record, DeploymentInfo>): { - /** True when a new deployment is available */ - updateAvailable: boolean; - /** Reload the page to get the new version */ - reload: () => void; - /** Dismiss the update notification (until next deploy) */ - dismiss: () => void; - /** The current deployment info (or null if not yet loaded) */ - deployment: DeploymentInfo | undefined; -}; -/** - * A ready-to-use update banner component. - * Displays a notification when a new deployment is available. - * - * @example - * ```tsx - * import { UpdateBanner } from "@convex-dev/static-hosting/react"; - * import { api } from "../convex/_generated/api"; - * - * function App() { - * return ( - *
- * - * {/* rest of your app *\/} - *
- * ); - * } - * ``` - */ -export declare function UpdateBanner({ getCurrentDeployment, message, buttonText, dismissable, className, style, }: { - getCurrentDeployment: FunctionReference<"query", "public", Record, DeploymentInfo>; - message?: string; - buttonText?: string; - dismissable?: boolean; - className?: string; - style?: CSSProperties; -}): JSX.Element | null; -export {}; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/react/index.d.ts.map b/dist/react/index.d.ts.map deleted file mode 100644 index 974cfa1..0000000 --- a/dist/react/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAqB,KAAK,aAAa,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,KAAK,cAAc,GAAG;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,IAAI,CAAC;AAET;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CAClC,oBAAoB,EAAE,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,cAAc,CAAC;IAoC/F,8CAA8C;;IAE9C,6CAA6C;;IAE7C,0DAA0D;;IAE1D,8DAA8D;;EAGjE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,oBAAoB,EACpB,OAAuC,EACvC,UAAqB,EACrB,WAAkB,EAClB,SAAS,EACT,KAAK,GACN,EAAE;IACD,oBAAoB,EAAE,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,cAAc,CAAC,CAAC;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAwDrB"} \ No newline at end of file diff --git a/dist/react/index.js b/dist/react/index.js deleted file mode 100644 index 8890b0a..0000000 --- a/dist/react/index.js +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useQuery } from "convex/react"; -import { useState, useMemo } from "react"; -/** - * Hook to detect when a new deployment is available. - * Shows a prompt to the user instead of auto-reloading. - * - * @param getCurrentDeployment - The query function reference from exposeDeploymentQuery - * @returns Object with update status and reload function - * - * @example - * ```tsx - * import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; - * import { api } from "../convex/_generated/api"; - * - * function App() { - * const { updateAvailable, reload } = useDeploymentUpdates( - * api.staticHosting.getCurrentDeployment - * ); - * - * return ( - *
- * {updateAvailable && ( - *
- * A new version is available! - * - *
- * )} - * {/* rest of your app *\/} - *
- * ); - * } - * ``` - */ -export function useDeploymentUpdates(getCurrentDeployment) { - const deployment = useQuery(getCurrentDeployment, {}); - const [initialDeploymentId, setInitialDeploymentId] = useState(null); - const [dismissedDeploymentId, setDismissedDeploymentId] = useState(null); - // Capture the initial deployment ID on first load - // Using useState with functional update to avoid stale closure issues - if (deployment && initialDeploymentId === null) { - // This is safe - we're setting initial state based on first data load - // It only runs once when deployment first becomes available - setInitialDeploymentId(deployment.currentDeploymentId); - } - // Derive updateAvailable from current state - const updateAvailable = useMemo(() => { - if (!deployment || initialDeploymentId === null) { - return false; - } - // Show update if deployment changed from initial AND user hasn't dismissed this one - const hasNewDeployment = deployment.currentDeploymentId !== initialDeploymentId; - const isDismissed = deployment.currentDeploymentId === dismissedDeploymentId; - return hasNewDeployment && !isDismissed; - }, [deployment, initialDeploymentId, dismissedDeploymentId]); - const reload = () => { - window.location.reload(); - }; - const dismiss = () => { - if (deployment) { - setDismissedDeploymentId(deployment.currentDeploymentId); - } - }; - return { - /** True when a new deployment is available */ - updateAvailable, - /** Reload the page to get the new version */ - reload, - /** Dismiss the update notification (until next deploy) */ - dismiss, - /** The current deployment info (or null if not yet loaded) */ - deployment, - }; -} -/** - * A ready-to-use update banner component. - * Displays a notification when a new deployment is available. - * - * @example - * ```tsx - * import { UpdateBanner } from "@convex-dev/static-hosting/react"; - * import { api } from "../convex/_generated/api"; - * - * function App() { - * return ( - *
- * - * {/* rest of your app *\/} - *
- * ); - * } - * ``` - */ -export function UpdateBanner({ getCurrentDeployment, message = "A new version is available!", buttonText = "Reload", dismissable = true, className, style, }) { - const { updateAvailable, reload, dismiss } = useDeploymentUpdates(getCurrentDeployment); - if (!updateAvailable) - return null; - const defaultStyle = { - position: "fixed", - bottom: "1rem", - right: "1rem", - backgroundColor: "#1a1a2e", - color: "#fff", - padding: "1rem 1.5rem", - borderRadius: "8px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)", - display: "flex", - alignItems: "center", - gap: "1rem", - zIndex: 9999, - fontFamily: "system-ui, -apple-system, sans-serif", - fontSize: "14px", - ...style, - }; - const buttonStyle = { - backgroundColor: "#4f46e5", - color: "#fff", - border: "none", - padding: "0.5rem 1rem", - borderRadius: "4px", - cursor: "pointer", - fontWeight: 500, - }; - const dismissStyle = { - background: "none", - border: "none", - color: "#888", - cursor: "pointer", - padding: "0.25rem", - fontSize: "18px", - lineHeight: 1, - }; - return (_jsxs("div", { className: className, style: defaultStyle, children: [_jsx("span", { children: message }), _jsx("button", { onClick: reload, style: buttonStyle, children: buttonText }), dismissable && (_jsx("button", { onClick: dismiss, style: dismissStyle, "aria-label": "Dismiss", children: "\u00D7" }))] })); -} -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/react/index.js.map b/dist/react/index.js.map deleted file mode 100644 index 1c52a00..0000000 --- a/dist/react/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.tsx"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 071faa3..2c763fd 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "test:watch": "vitest --typecheck --clearScreen false", "test:debug": "vitest --inspect-brk --no-file-parallelism", "test:coverage": "vitest run --coverage --coverage.reporter=text", + "prepare": "npm run build", "preversion": "npm ci && npm run build:clean && run-p test lint typecheck", "alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags", "release": "npm version patch && npm publish && git push --follow-tags", From dd33a988209185c378d62f008e101f626dce4396 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 02:47:39 -0700 Subject: [PATCH 03/29] update readme/integration guide --- INTEGRATION.md | 149 ++++++++++++++++++++++++------------------------ README.md | 150 ++++++++++++++++++++++++------------------------- 2 files changed, 151 insertions(+), 148 deletions(-) diff --git a/INTEGRATION.md b/INTEGRATION.md index ccb588d..ced335b 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -1,8 +1,19 @@ # Integration Guide: @convex-dev/static-hosting -A Convex component that hosts static React/Vite apps directly on Convex — -serving from the component's own HTTP endpoints and storage. No external -hosting, and minimal app-side wiring. +This guide walks you through hosting a static React/Vite app on Convex using +the `@convex-dev/static-hosting` component. Your frontend ends up at +`https://.convex.site`, served alongside your Convex backend with +SPA routing and smart caching. + +## What this component gives you + +- A drop-in HTTP handler that serves your static files from Convex storage +- One CLI command (`deploy`) that builds, deploys the backend, and uploads + files +- SPA fallback to `index.html` for client-side routing +- Long-term cache headers on hashed assets and ETag-based revalidation on HTML +- Optional live-reload notifications when a new deploy ships +- Authenticated uploads via the Convex CLI (no public upload endpoint) ## Quick Start @@ -11,8 +22,12 @@ npm install @convex-dev/static-hosting npx @convex-dev/static-hosting setup ``` -The setup script creates `convex/convex.config.ts` and adds a `deploy` script -to `package.json`. Then: +The setup script: + +- Adds the component to `convex/convex.config.ts` +- Adds a `deploy` script to `package.json` + +Then: ```bash npm run deploy @@ -22,7 +37,7 @@ npm run deploy ### `convex/convex.config.ts` -This is the only file you need to touch in your app for the basic case. +This is the only required app-side file. ```typescript import { defineApp } from "convex/server"; @@ -34,10 +49,9 @@ app.use(staticHosting, { httpPrefix: "/" }); export default app; ``` -`httpPrefix: "/"` mounts the component at the deployment root. If your app -needs to serve its own HTTP routes at the root, mount the component at a -sub-path like `httpPrefix: "/app/"` and set your bundler's base path to match -(e.g. `base: "/app/"` in `vite.config.ts`). +`httpPrefix: "/"` serves the static site at the deployment root. To host under +a sub-path instead (e.g. if you have your own HTTP routes at the root), use +`httpPrefix: "/app/"` and set your bundler's base path to match. > Run `npx convex dev` after editing `convex.config.ts` so codegen picks up the > component. @@ -52,19 +66,17 @@ sub-path like `httpPrefix: "/app/"` and set your bundler's base path to match } ``` -That's all that's required. - ## Deployment ```bash -# Login (first time) +# First time npx convex login -# One-shot: build + deploy backend + upload static files +# Build + backend deploy + upload static files npx @convex-dev/static-hosting deploy ``` -Or two-step: +Two-step alternative: ```bash npx convex deploy @@ -73,13 +85,9 @@ npx @convex-dev/static-hosting upload --build --prod Your app is live at `https://.convex.site`. -The CLI authenticates with `convex run --component staticHosting lib:...` to -talk to the component directly — your app does not need to expose -`generateUploadUrl`, `recordAsset`, or any other upload functions. - ## Live reload banner (optional) -If you want to prompt users to reload when a new deployment ships: +Show users a banner when a new version is deployed: ### `convex/staticHosting.ts` @@ -107,17 +115,14 @@ function App() { } ``` -`UpdateBanner` resolves `api.staticHosting.getCurrentDeployment` by default. -For a different module name, pass the reference explicitly: +`UpdateBanner` resolves `api.staticHosting.getCurrentDeployment` by default. To +re-export the query under a different module, pass the reference explicitly: ```tsx import { api } from "../convex/_generated/api"; ``` -If `UpdateBanner` is used without exposing the query, it logs a setup hint to -the console and stays hidden. - For custom UI, use the hook: ```tsx @@ -126,24 +131,29 @@ import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; const { updateAvailable, reload, dismiss } = useDeploymentUpdates(); ``` -## CDN mode (optional) +## Connecting to Convex from the frontend -By default, every file is served from the component's HTTP handler reading the -component's storage. Non-HTML assets can be redirected to a CDN edge network -via [convex-fs](https://convexfs.dev) for lower bandwidth and better -edge-cache performance. +When served from `*.convex.site`, derive the backend URL automatically: -The component already issues a 302 redirect to `${origin}/fs/blobs/` -when an asset has a `blobId` — those endpoints are served by `convex-fs` at the -deployment root. +```typescript +import { getConvexUrl } from "@convex-dev/static-hosting"; -### 1. Install convex-fs +const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); +``` + +## CDN mode (optional) + +Non-HTML assets can be served from a CDN edge network via +[convex-fs](https://convexfs.dev) for lower bandwidth use and faster edge +caching. HTML stays in Convex so SPA routing keeps working. + +### Setup ```bash npm install convex-fs ``` -### 2. `convex/convex.config.ts` +`convex/convex.config.ts`: ```typescript import { defineApp } from "convex/server"; @@ -157,7 +167,7 @@ app.use(fs); export default app; ``` -### 3. Deploy with `--cdn` +### Deploy with `--cdn` ```bash npx @convex-dev/static-hosting deploy --cdn @@ -165,9 +175,8 @@ npx @convex-dev/static-hosting deploy --cdn ### CDN garbage collection (optional) -Old CDN blobs aren't auto-deleted (the component can't reach the -deployment-root `/fs/blobs/` endpoint with auth). Expose a delete function -in your app and pass it to the CLI: +Old CDN blobs aren't auto-deleted by default. To clean them up, expose a small +delete action in your app and pass its path to the CLI: `convex/cdn.ts`: @@ -189,21 +198,22 @@ export const deleteCdnBlobs = internalAction({ }); ``` -Then deploy with: - ```bash npx @convex-dev/static-hosting deploy --cdn \ --cdn-delete-function cdn:deleteCdnBlobs ``` -## Connecting to Convex from the static frontend +## Non-Vite bundlers -When served from `*.convex.site`, derive the matching backend URL: +The CLI's `--build` flag sets `VITE_CONVEX_URL` when running your build. To +forward it to another env var (Expo, Next.js, etc.), wrap your build script: -```typescript -import { getConvexUrl } from "@convex-dev/static-hosting"; +```json +// Expo +"build": "EXPO_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$EXPO_PUBLIC_CONVEX_URL} npx expo export --platform web" -const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); +// Next.js +"build": "NEXT_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$NEXT_PUBLIC_CONVEX_URL} next build" ``` ## Security @@ -218,7 +228,7 @@ This means unauthorized users cannot upload files, even if they know your Convex ```bash npx @convex-dev/static-hosting setup - # Creates convex/convex.config.ts and adds a deploy script. + # Adds the component to convex.config.ts and a deploy script to package.json. npx @convex-dev/static-hosting deploy [options] -d, --dist Path to dist directory (default: ./dist) @@ -237,26 +247,25 @@ npx @convex-dev/static-hosting upload [options] -j, --concurrency Parallel upload workers (default: 5) ``` -## Important Notes +## Mounting under a sub-path -1. **Storage lives in the component.** Uploaded files are stored in the - component's `_storage` table, not your app's. If you mount multiple - instances of the component, each has its own storage. -2. Upload functions are **internal** — only reachable via the authenticated - `npx convex run --component` CLI flow. -3. Hashed assets get `immutable, max-age=31536000`; HTML uses ETag - revalidation. -4. Paths without a file extension fall back to `/index.html` (SPA mode). -5. Always pass `--build` to the upload CLI so `VITE_CONVEX_URL` matches the - target deployment. +To mount under `/app/` (for example, if you have other HTTP routes at the +root): + +```typescript +app.use(staticHosting, { httpPrefix: "/app/" }); +``` + +Set your bundler's base path to match — `base: "/app/"` in `vite.config.ts`, +`publicPath: "/app/"` for webpack, `assetPrefix: "/app"` for Next.js — so the +emitted HTML references the right URLs. ## Troubleshooting ### 404s on every path -Make sure `convex.config.ts` mounts the component and you've run `npx convex -dev` (or `npx convex deploy`) since adding it. The component's own -`http.ts` is auto-detected during codegen. +Run `npx convex dev` (or `npx convex deploy`) after adding the component to +`convex.config.ts` so codegen picks up the new HTTP routes. ### Wrong `VITE_CONVEX_URL` in the built bundle @@ -270,26 +279,20 @@ npm run build && npx @convex-dev/static-hosting upload --prod ### Component name mismatch -If you've renamed the component instance (`app.use(staticHosting, { name: -"custom" })`), pass it to the CLI: +If you've renamed the component instance with `app.use(staticHosting, { name: +"custom" })`, pass it to the CLI: ```bash npx @convex-dev/static-hosting upload --component custom ``` -### Mounting under a sub-path - -If you set `httpPrefix: "/app/"`, also set `base: "/app/"` in -`vite.config.ts` (or your bundler's equivalent) so emitted assets reference -the right URLs. - ## API Reference ### `exposeDeploymentQuery(component)` -Returns `{ getCurrentDeployment }` — a public query that wraps the -component's deployment singleton. Add this to your app only if you use -`` or `useDeploymentUpdates`. +Returns `{ getCurrentDeployment }` — a public query that wraps the component's +deployment singleton. Add this only if you use `` or +`useDeploymentUpdates`. ### `getConvexUrl()` @@ -298,6 +301,6 @@ served from `.convex.site`. ## Additional Resources -- [README.md](./README.md) — Full documentation +- [README.md](./README.md) - [`example/`](./example) — Working example app - [Component source](./src/component) diff --git a/README.md b/README.md index 4d2a8a5..44a6bcc 100644 --- a/README.md +++ b/README.md @@ -2,53 +2,44 @@ [![npm version](https://badge.fury.io/js/@convex-dev%2Fstatic-hosting.svg)](https://badge.fury.io/js/@convex-dev/static-hosting) -A Convex component that enables hosting static React/Vite apps using Convex -HTTP actions and file storage. No external hosting provider required! +A Convex component for hosting static React/Vite apps directly on Convex — no +separate hosting provider, no DNS to wire up, no second deploy target. Run one +command and your frontend is live at `https://.convex.site` +alongside your backend. -## Quick Start +## Features + +- 🚀 **One-command deploy** — build, push backend, and upload static files in a + single step. +- 🔄 **SPA routing** — paths without an extension fall back to `index.html`. +- ⚡ **Smart caching** — hashed assets get long-term immutable caching; HTML + uses ETag revalidation so updates land instantly. +- 🔔 **Live reload notifications** — connected clients can subscribe to a query + that fires when a new version ships, with a drop-in ``. +- 🔒 **Authenticated uploads** — uploads go through the Convex CLI's + authenticated session; there's no public upload endpoint. +- 🧹 **Automatic cleanup** — files from previous deployments are garbage + collected on every deploy. + +https://github.com/user-attachments/assets/5eaf781f-87da-4292-9f96-38070c86cd39 -### Automated Setup (Recommended) +## Quick Start ```bash npm install @convex-dev/static-hosting npx @convex-dev/static-hosting setup ``` -The setup command creates `convex/convex.config.ts` (or shows you what to add) -and registers a `deploy` script. Then: +The setup command adds the component to `convex/convex.config.ts` and creates a +`deploy` script in `package.json`. Then: ```bash npm run deploy ``` -### For LLMs / AI Assistants - -If you're an LLM helping a user integrate this component, read [INTEGRATION.md](./INTEGRATION.md) for complete integration instructions optimized for AI consumption. - -### Manual Setup - -See [Manual Setup](#manual-setup-1) section below for step-by-step instructions. - -## Features - -- 🚀 **Simple deployment** - Upload your built files directly to Convex storage -- 🔒 **Secure by default** - Upload API uses internal functions (not publicly - accessible) -- 🔄 **SPA support** - Automatic fallback to index.html for client-side routing -- ⚡ **Smart caching** - Hashed assets get long-term caching, HTML is always - fresh with ETag support -- 🧹 **Auto cleanup** - Old deployment files are automatically garbage collected -- 📦 **Zero config** - Works out of the box with Vite, Create React App, and - other bundlers - - - -https://github.com/user-attachments/assets/5eaf781f-87da-4292-9f96-38070c86cd39 - - - +Your app is live at `https://.convex.site`. -## Manual Setup +## Setup ### 1. Install @@ -56,7 +47,7 @@ https://github.com/user-attachments/assets/5eaf781f-87da-4292-9f96-38070c86cd39 npm install @convex-dev/static-hosting ``` -### 2. Wire up the component +### 2. Register the component `convex/convex.config.ts`: @@ -70,14 +61,9 @@ app.use(staticHosting, { httpPrefix: "/" }); export default app; ``` -That's it for required wiring. The component owns its own HTTP routes and file -storage — you don't register routes, expose functions, or re-export an upload -API from your app. - -> `httpPrefix: "/"` mounts the static site at the deployment root. If your app -> already serves its own HTTP routes there, either change those to a sub-path -> (e.g. `/api/...`) or mount the component at a sub-path instead (e.g. -> `httpPrefix: "/app/"`). +`httpPrefix: "/"` serves your static site at the deployment root. If you have +other HTTP routes in your app, you can mount the static site under a sub-path +(see [Mounting under a sub-path](#mounting-under-a-sub-path) below). ### 3. Add a deploy script @@ -89,6 +75,8 @@ API from your app. } ``` +That's it. + ### Using Non-Vite Bundlers The CLI's `--build` flag sets `VITE_CONVEX_URL` when running your build command. @@ -122,15 +110,10 @@ functional. ## Deployment -### One-Shot Deployment (Recommended) - Deploy both Convex backend and static files with a single command: ```bash -# Make sure you're logged in -npx convex login - -# Deploy everything +npx convex init # first time only npx @convex-dev/static-hosting deploy ``` @@ -138,9 +121,9 @@ The `deploy` command: 1. Builds your frontend with the production `VITE_CONVEX_URL`. 2. Deploys the Convex backend. -3. Uploads `dist/` to the component's storage. +3. Uploads `dist/` to Convex. -### Manual Two-Step Deployment +For more control, you can run the two halves separately: ```bash npx convex deploy @@ -149,6 +132,20 @@ npx @convex-dev/static-hosting upload --build --prod Your app is live at `https://.convex.site`. +### Non-Vite bundlers + +The `--build` flag sets `VITE_CONVEX_URL` for your build. To use a different +env var (Expo, Next.js, etc.), wrap your build script so the value passes +through: + +```json +// Expo +"build": "EXPO_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$EXPO_PUBLIC_CONVEX_URL} npx expo export --platform web" + +// Next.js +"build": "NEXT_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$NEXT_PUBLIC_CONVEX_URL} next build" +``` + ### CLI options ```bash @@ -163,16 +160,14 @@ npx @convex-dev/static-hosting upload [options] -d, --dist Path to dist directory (default: ./dist) -c, --component Component instance name (default: staticHosting) --prod Deploy to production deployment - -b, --build Run 'npm run build' with correct VITE_CONVEX_URL + -b, --build Run 'npm run build' with VITE_CONVEX_URL set --cdn Upload non-HTML assets to convex-fs CDN --cdn-delete-function App function path that deletes CDN blobs (opt-in) -j, --concurrency Parallel upload workers (default: 5) ``` -The CLI runs against the component directly (`npx convex run --component -staticHosting lib:...`) — your app does not need to export `generateUploadUrl`, -`recordAsset`, etc. If you mount the component under a different name, pass -`--component `. +If you mount the component under a different name with `app.use(staticHosting, +{ name: "..." })`, pass it with `--component`. ## Security @@ -184,7 +179,7 @@ The upload API uses **internal functions** in the Component that can only be cal This means unauthorized users **cannot** upload files to your site, even if they know your Convex URL. -## Live Reload on Deploy (optional) +## Live reload on deploy (optional) If you want a banner that prompts users to reload when a new deployment ships, expose the deployment query in your app and drop in ``: @@ -215,8 +210,8 @@ function App() { } ``` -`UpdateBanner` resolves `api.staticHosting.getCurrentDeployment` by default. If -you re-export the query under a different module name, pass it explicitly: +`UpdateBanner` resolves `api.staticHosting.getCurrentDeployment` automatically. +If you re-export the query under a different module name, pass it explicitly: ```tsx import { api } from "../convex/_generated/api"; @@ -231,13 +226,10 @@ import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; const { updateAvailable, reload, dismiss } = useDeploymentUpdates(); ``` -If `UpdateBanner` is used without exposing the query, a setup warning is logged -to the console and the banner stays hidden. - -## Connecting to Convex from the static frontend +## Connecting to Convex from the frontend -When your frontend is served from `*.convex.site`, you can derive the matching -backend URL without an env var: +When your frontend is served from `*.convex.site`, you can derive the backend +URL without an env var: ```ts import { getConvexUrl } from "@convex-dev/static-hosting"; @@ -245,23 +237,31 @@ import { getConvexUrl } from "@convex-dev/static-hosting"; const convexUrl = import.meta.env.VITE_CONVEX_URL ?? getConvexUrl(); ``` -## How it works +## Mounting under a sub-path -The static-hosting component owns both the HTTP handler and the file storage: +Mount the static site under a sub-path if you have other routes at the root: -1. **Build phase** — your bundler produces `dist/`. -2. **Upload phase** — the CLI authenticates with `npx convex run --component`, - calls the component's internal `generateUploadUrls`, uploads each file to the - component's storage, records metadata, and garbage-collects old deployments. -3. **Serve phase** — the component's HTTP action looks up assets by path, - strips its mount prefix from the URL, and streams from storage with smart - caching. Paths without an extension fall back to `/index.html` for SPAs. +```ts +app.use(staticHosting, { httpPrefix: "/app/" }); +``` + +You'll also need to tell your bundler about the base path so the emitted HTML +references the right URLs — `base: "/app/"` in `vite.config.ts`, or the +equivalent for your bundler. + +## How it works -Because everything lives inside the component, your app code stays one line. +1. **Build** — your bundler emits `dist/`. +2. **Upload** — the CLI uses your authenticated Convex session to generate + signed upload URLs, push files to Convex storage, record metadata, and GC + old deployments. +3. **Serve** — an HTTP action looks up the requested path, streams the file + with the right `Content-Type`, applies long-term caching for hashed assets, + and falls back to `index.html` for SPA routes. ## Example -See [`example/`](./example) for a complete Vite + React app integration. +See [`example/`](./example) for a complete Vite + React app. ```bash npm install From 5a3b19bf196dfad2ca1cae756bfc8b78b41c3701 Mon Sep 17 00:00:00 2001 From: sethconvex Date: Wed, 13 May 2026 10:50:28 -0600 Subject: [PATCH 04/29] fix typo --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f039dd3..670e7f4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,5 @@ node_modules .mcp.json next-example/ dist/ -exmple/dist/ +example/dist/ From 21668e3c09c0d0a95b391905cf00ee598c88d43b Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 02:56:54 -0700 Subject: [PATCH 05/29] better support for other base path --- INTEGRATION.md | 17 ++++++++++++++--- README.md | 16 ++++++++++++++-- example/vite.config.ts | 1 + src/cli/deploy.ts | 21 +++++++++++++++++++++ src/cli/upload.ts | 18 ++++++++++++++++++ src/component/lib.ts | 15 +++++++++++++++ 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/INTEGRATION.md b/INTEGRATION.md index ced335b..c38ce24 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -256,9 +256,20 @@ root): app.use(staticHosting, { httpPrefix: "/app/" }); ``` -Set your bundler's base path to match — `base: "/app/"` in `vite.config.ts`, -`publicPath: "/app/"` for webpack, `assetPrefix: "/app"` for Next.js — so the -emitted HTML references the right URLs. +The bundler also needs to know the base path so the emitted HTML references +the right URLs. The CLI sets a `STATIC_HOSTING_BASE_PATH` env var matching the +component's mount when it runs your build, so `vite.config.ts` can read it: + +```typescript +import { defineConfig } from "vite"; + +export default defineConfig({ + base: process.env.STATIC_HOSTING_BASE_PATH ?? "/", +}); +``` + +Root-mounted apps don't need this — the default is `/`. Webpack/Next.js +equivalents: `publicPath` and `assetPrefix`. ## Troubleshooting diff --git a/README.md b/README.md index 44a6bcc..c343c68 100644 --- a/README.md +++ b/README.md @@ -246,8 +246,20 @@ app.use(staticHosting, { httpPrefix: "/app/" }); ``` You'll also need to tell your bundler about the base path so the emitted HTML -references the right URLs — `base: "/app/"` in `vite.config.ts`, or the -equivalent for your bundler. +references the right URLs. The CLI sets a `STATIC_HOSTING_BASE_PATH` env var +matching the component's mount when it runs your build, so `vite.config.ts` +can read it directly: + +```ts +import { defineConfig } from "vite"; + +export default defineConfig({ + base: process.env.STATIC_HOSTING_BASE_PATH ?? "/", +}); +``` + +Root-mounted apps don't need this — the default is `/`. For webpack use +`publicPath`, for Next.js `assetPrefix`. ## How it works diff --git a/example/vite.config.ts b/example/vite.config.ts index 8bd1a85..501a75e 100644 --- a/example/vite.config.ts +++ b/example/vite.config.ts @@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ envDir: "../", + base: process.env.STATIC_HOSTING_BASE_PATH ?? "/", plugins: [react()], resolve: { conditions: ["@convex-dev/component-source"], diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index 6c9597e..b9f144d 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -95,6 +95,23 @@ Examples: `); } +/** + * Resolve the component's mount prefix from its CONVEX_SITE_URL. Returns "/" + * if the component isn't deployed yet or the query is unavailable. + */ +function fetchBasePath(componentName: string): string { + try { + const out = execSync( + `npx convex run --component ${componentName} lib:getBasePath '{}' --prod --typecheck=disable --codegen=disable`, + { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }, + ).trim(); + const value = JSON.parse(out); + return typeof value === "string" && value.length > 0 ? value : "/"; + } catch { + return "/"; + } +} + /** * Get the production Convex URL */ @@ -223,12 +240,16 @@ async function main(): Promise { process.exit(1); } + const basePath = fetchBasePath(args.component); + console.log(` Building with VITE_CONVEX_URL=${convexUrl}`); + console.log(` STATIC_HOSTING_BASE_PATH=${basePath}`); console.log(""); const buildResult = spawnNpmRun("build", { ...process.env, VITE_CONVEX_URL: convexUrl, + STATIC_HOSTING_BASE_PATH: basePath, }); if (buildResult !== 0) { diff --git a/src/cli/upload.ts b/src/cli/upload.ts index 5c0dcea..bd043ea 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -142,6 +142,20 @@ function getConvexEnv(name: string, prod: boolean): string | null { } } +/** + * Resolve the component's mount prefix from CONVEX_SITE_URL. Returns "/" if + * the component isn't deployed yet or the query is unavailable. + */ +async function fetchBasePath(componentName: string): Promise { + try { + const out = await convexRunComponentAsync(componentName, "lib:getBasePath"); + const value = JSON.parse(out); + return typeof value === "string" && value.length > 0 ? value : "/"; + } catch { + return "/"; + } +} + function convexRunAsync( componentName: string | undefined, functionPath: string, @@ -366,14 +380,18 @@ async function main(): Promise { process.exit(1); } + const basePath = await fetchBasePath(args.component); + const envLabel = useProd ? "production" : "development"; console.log(`🔨 Building for ${envLabel}...`); console.log(` VITE_CONVEX_URL=${convexUrl}`); + console.log(` STATIC_HOSTING_BASE_PATH=${basePath}`); console.log(""); const buildResult = spawnNpmRun("build", { ...process.env, VITE_CONVEX_URL: convexUrl, + STATIC_HOSTING_BASE_PATH: basePath, }); if (buildResult !== 0) { diff --git a/src/component/lib.ts b/src/component/lib.ts index 8d0fdef..ff5a96c 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -30,6 +30,21 @@ export const getCurrentDeployment = query({ }, }); +export const getBasePath = internalQuery({ + args: {}, + returns: v.string(), + handler: async () => { + const siteUrl = process.env.CONVEX_SITE_URL; + if (!siteUrl) return "/"; + try { + const pathname = new URL(siteUrl).pathname; + return pathname || "/"; + } catch { + return "/"; + } + }, +}); + export const getByPath = internalQuery({ args: { path: v.string() }, returns: v.union(staticAssetValidator, v.null()), From c259f3cd340e386fcf51cc31badf8e57f545b91f Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 17:34:48 -0700 Subject: [PATCH 06/29] host app under /api --- example/convex/convex.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/convex/convex.config.ts b/example/convex/convex.config.ts index 4f84e0b..bd52092 100644 --- a/example/convex/convex.config.ts +++ b/example/convex/convex.config.ts @@ -1,7 +1,7 @@ import { defineApp } from "convex/server"; import staticHosting from "@convex-dev/static-hosting/convex.config.js"; -const app = defineApp({ httpPrefix: "/app" }); +const app = defineApp({ httpPrefix: "/api" }); app.use(staticHosting, { httpPrefix: "/" }); export default app; From d13d2c577d33cce667aa8f4eb0d6b5d5aadd8d3d Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 17:35:08 -0700 Subject: [PATCH 07/29] remove more dist --- example/dist/assets/index-BKUd3l-d.css | 1 - example/dist/index.html | 13 ------------- 2 files changed, 14 deletions(-) delete mode 100644 example/dist/assets/index-BKUd3l-d.css delete mode 100644 example/dist/index.html diff --git a/example/dist/assets/index-BKUd3l-d.css b/example/dist/assets/index-BKUd3l-d.css deleted file mode 100644 index 8f4ea45..0000000 --- a/example/dist/assets/index-BKUd3l-d.css +++ /dev/null @@ -1 +0,0 @@ -:root{--bg-primary: #0f1729;--bg-secondary: #1a2744;--bg-card: #1e2d4d;--text-primary: #f0f4f8;--text-secondary: #94a3b8;--accent: #ff6b6b;--accent-hover: #ff8787;--border: #2d3f5f}*{box-sizing:border-box;margin:0;padding:0}body{font-family:Bricolage Grotesque,DM Sans,system-ui,-apple-system,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.6}.app{min-height:100vh;display:flex;flex-direction:column;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(255,107,107,.15),transparent),radial-gradient(ellipse 60% 40% at 80% 80%,rgba(99,102,241,.1),transparent),var(--bg-primary)}.header{text-align:center;padding:4rem 2rem 2rem;animation:fadeInDown .6s ease-out}@keyframes fadeInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}.header h1{font-size:clamp(2rem,5vw,3.5rem);font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem;background:linear-gradient(135deg,var(--text-primary) 0%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.subtitle{font-size:1.25rem;color:var(--text-secondary);font-weight:400}.main{flex:1;max-width:1000px;margin:0 auto;padding:2rem;width:100%}.card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:2rem;margin-bottom:2rem;animation:fadeInUp .6s ease-out;animation-fill-mode:both}.card:nth-child(1){animation-delay:.1s}.card:nth-child(2){animation-delay:.2s}.card:nth-child(3){animation-delay:.3s}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.card h2{font-size:1.75rem;margin-bottom:.75rem;color:var(--text-primary)}.card h3{font-size:1.25rem;margin-bottom:1rem;color:var(--text-primary)}.card p{color:var(--text-secondary);font-size:1.1rem}.card ol{color:var(--text-secondary);padding-left:1.5rem}.card ol li{margin-bottom:.5rem}.counter{display:flex;align-items:center;justify-content:center;gap:1.5rem;margin-top:1.5rem}.counter button{width:48px;height:48px;border-radius:12px;border:2px solid var(--accent);background:transparent;color:var(--accent);font-size:1.5rem;font-weight:600;cursor:pointer;transition:all .2s ease}.counter button:hover{background:var(--accent);color:var(--bg-primary);transform:scale(1.05)}.counter button:active{transform:scale(.95)}.count{font-size:2.5rem;font-weight:700;min-width:80px;text-align:center;color:var(--accent);font-variant-numeric:tabular-nums}.features{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1.5rem;margin-bottom:2rem}.feature{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:1.5rem;animation:fadeInUp .6s ease-out;animation-fill-mode:both;transition:transform .2s ease,border-color .2s ease}.feature:nth-child(1){animation-delay:.15s}.feature:nth-child(2){animation-delay:.25s}.feature:nth-child(3){animation-delay:.35s}.feature:hover{transform:translateY(-4px);border-color:var(--accent)}.feature .icon{font-size:2rem;margin-bottom:.75rem;display:block}.feature h3{font-size:1.1rem;margin-bottom:.5rem;color:var(--text-primary)}.feature p{font-size:.95rem;color:var(--text-secondary);margin:0}code{font-family:JetBrains Mono,Fira Code,monospace;background:var(--bg-primary);padding:.2em .5em;border-radius:6px;font-size:.9em;color:var(--accent)}.footer{text-align:center;padding:2rem;color:var(--text-secondary);font-size:.95rem}.footer a{color:var(--accent);text-decoration:none;transition:color .2s ease}.footer a:hover{color:var(--accent-hover);text-decoration:underline}@media (max-width: 640px){.header{padding:3rem 1.5rem 1.5rem}.main{padding:1rem}.card{padding:1.5rem}.features{grid-template-columns:1fr}}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}} diff --git a/example/dist/index.html b/example/dist/index.html deleted file mode 100644 index 240c775..0000000 --- a/example/dist/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Example App - - - - -
- - From 633c87bbc3b28ba84bd08730b8a34528106df097 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 13 May 2026 17:35:45 -0700 Subject: [PATCH 08/29] allow --build-cmd --- dist/cli/upload.js | 657 +++++++++++++++++++++++--------------------- src/cli/commands.ts | 8 +- src/cli/deploy.ts | 39 ++- src/cli/upload.ts | 18 +- 4 files changed, 390 insertions(+), 332 deletions(-) diff --git a/dist/cli/upload.js b/dist/cli/upload.js index 86e95d7..2401c32 100644 --- a/dist/cli/upload.js +++ b/dist/cli/upload.js @@ -14,79 +14,70 @@ import { readFileSync, readdirSync, existsSync } from "fs"; import { join, relative, extname, resolve } from "path"; import { randomUUID } from "crypto"; -import { runConvex, runConvexAsync, spawnNpmRun } from "./commands.js"; +import { runConvex, runConvexAsync, spawnShell } from "./commands.js"; // MIME type mapping const MIME_TYPES = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", - ".webmanifest": "application/manifest+json", - ".xml": "application/xml", + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".txt": "text/plain; charset=utf-8", + ".map": "application/json", + ".webmanifest": "application/manifest+json", + ".xml": "application/xml", }; function getMimeType(path) { - return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream"; + return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream"; } function parseArgs(args) { - const result = { - dist: "./dist", - component: "staticHosting", - prod: false, // Default to dev, use --prod for production - build: false, - cdn: false, - cdnDeleteFunction: "", - concurrency: 5, - help: false, - }; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === "--help" || arg === "-h") { - result.help = true; - } - else if (arg === "--dist" || arg === "-d") { - result.dist = args[++i] || result.dist; - } - else if (arg === "--component" || arg === "-c") { - result.component = args[++i] || result.component; - } - else if (arg === "--prod") { - result.prod = true; - } - else if (arg === "--no-prod" || arg === "--dev") { - result.prod = false; - } - else if (arg === "--build" || arg === "-b") { - result.build = true; - } - else if (arg === "--cdn") { - result.cdn = true; - } - else if (arg === "--cdn-delete-function") { - result.cdnDeleteFunction = args[++i] || result.cdnDeleteFunction; - } - else if (arg === "--concurrency" || arg === "-j") { - const val = parseInt(args[++i], 10); - if (val > 0) - result.concurrency = val; - } + const result = { + dist: "./dist", + component: "staticHosting", + prod: false, // Default to dev, use --prod for production + build: false, + cdn: false, + cdnDeleteFunction: "", + concurrency: 5, + help: false, + }; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--help" || arg === "-h") { + result.help = true; + } else if (arg === "--dist" || arg === "-d") { + result.dist = args[++i] || result.dist; + } else if (arg === "--component" || arg === "-c") { + result.component = args[++i] || result.component; + } else if (arg === "--prod") { + result.prod = true; + } else if (arg === "--no-prod" || arg === "--dev") { + result.prod = false; + } else if (arg === "--build" || arg === "-b") { + result.build = true; + } else if (arg === "--cdn") { + result.cdn = true; + } else if (arg === "--cdn-delete-function") { + result.cdnDeleteFunction = args[++i] || result.cdnDeleteFunction; + } else if (arg === "--concurrency" || arg === "-j") { + const val = parseInt(args[++i], 10); + if (val > 0) result.concurrency = val; } - return result; + } + return result; } function showHelp() { - console.log(` + console.log(` Usage: npx @convex-dev/static-hosting upload [options] Upload static files from a dist directory to Convex storage. @@ -116,288 +107,324 @@ Examples: // Global flag for production mode let useProd = true; function getEnvFileConvexUrl() { - if (!existsSync(".env.local")) { - return null; - } - const envContent = readFileSync(".env.local", "utf-8"); - const match = envContent.match(/(?:VITE_)?CONVEX_URL=(.+)/); - return match?.[1]?.trim() || null; + if (!existsSync(".env.local")) { + return null; + } + const envContent = readFileSync(".env.local", "utf-8"); + const match = envContent.match(/(?:VITE_)?CONVEX_URL=(.+)/); + return match?.[1]?.trim() || null; } function getConvexEnv(name, prod) { - try { - return runConvex(["env", "get", name, ...(prod ? ["--prod"] : [])]) || null; - } - catch { - return null; - } + try { + return runConvex(["env", "get", name, ...(prod ? ["--prod"] : [])]) || null; + } catch { + return null; + } } function convexRunAsync(functionPath, args = {}) { - return runConvexAsync([ - "run", - functionPath, - JSON.stringify(args), - "--typecheck=disable", - "--codegen=disable", - ...(useProd ? ["--prod"] : []), - ]); + return runConvexAsync([ + "run", + functionPath, + JSON.stringify(args), + "--typecheck=disable", + "--codegen=disable", + ...(useProd ? ["--prod"] : []), + ]); } -async function uploadWithConcurrency(files, componentName, deploymentId, useCdn, siteUrl, concurrency) { - const total = files.length; - // Separate CDN and storage files - const cdnFiles = []; - const storageFiles = []; - for (const file of files) { +async function uploadWithConcurrency( + files, + componentName, + deploymentId, + useCdn, + siteUrl, + concurrency, +) { + const total = files.length; + // Separate CDN and storage files + const cdnFiles = []; + const storageFiles = []; + for (const file of files) { + const isHtml = file.contentType.startsWith("text/html"); + if (useCdn && !isHtml && siteUrl) { + cdnFiles.push(file); + } else { + storageFiles.push(file); + } + } + // Upload storage files using batch operations + let completed = 0; + const allAssets = []; + if (storageFiles.length > 0) { + // Step 1: Generate all upload URLs in one batch call + console.log(` Generating ${storageFiles.length} upload URLs...`); + const urlsOutput = await convexRunAsync( + `${componentName}:generateUploadUrls`, + { count: storageFiles.length }, + ); + const uploadUrls = JSON.parse(urlsOutput); + // Step 2: Upload all files in parallel via fetch + const storageIds = new Array(storageFiles.length); + const pending = new Set(); + for (let i = 0; i < storageFiles.length; i++) { + const idx = i; + const file = storageFiles[idx]; + const task = (async () => { + const content = readFileSync(file.localPath); + const response = await fetch(uploadUrls[idx], { + method: "POST", + headers: { "Content-Type": file.contentType }, + body: content, + }); + const { storageId } = await response.json(); + storageIds[idx] = storageId; + completed++; const isHtml = file.contentType.startsWith("text/html"); - if (useCdn && !isHtml && siteUrl) { - cdnFiles.push(file); - } - else { - storageFiles.push(file); - } + console.log( + ` [${completed}/${total}] ${file.path} (${isHtml ? "storage/html" : "storage"})`, + ); + })().then(() => { + pending.delete(task); + }); + pending.add(task); + if (pending.size >= concurrency) { + await Promise.race(pending); + } } - // Upload storage files using batch operations - let completed = 0; - const allAssets = []; - if (storageFiles.length > 0) { - // Step 1: Generate all upload URLs in one batch call - console.log(` Generating ${storageFiles.length} upload URLs...`); - const urlsOutput = await convexRunAsync(`${componentName}:generateUploadUrls`, { count: storageFiles.length }); - const uploadUrls = JSON.parse(urlsOutput); - // Step 2: Upload all files in parallel via fetch - const storageIds = new Array(storageFiles.length); - const pending = new Set(); - for (let i = 0; i < storageFiles.length; i++) { - const idx = i; - const file = storageFiles[idx]; - const task = (async () => { - const content = readFileSync(file.localPath); - const response = await fetch(uploadUrls[idx], { - method: "POST", - headers: { "Content-Type": file.contentType }, - body: content, - }); - const { storageId } = (await response.json()); - storageIds[idx] = storageId; - completed++; - const isHtml = file.contentType.startsWith("text/html"); - console.log(` [${completed}/${total}] ${file.path} (${isHtml ? "storage/html" : "storage"})`); - })().then(() => { pending.delete(task); }); - pending.add(task); - if (pending.size >= concurrency) { - await Promise.race(pending); - } - } - await Promise.all(pending); - for (let i = 0; i < storageFiles.length; i++) { - allAssets.push({ - path: storageFiles[i].path, - storageId: storageIds[i], - contentType: storageFiles[i].contentType, - deploymentId, - }); - } + await Promise.all(pending); + for (let i = 0; i < storageFiles.length; i++) { + allAssets.push({ + path: storageFiles[i].path, + storageId: storageIds[i], + contentType: storageFiles[i].contentType, + deploymentId, + }); } - // Upload CDN files (still uses per-file calls since CDN has its own upload endpoint) - if (cdnFiles.length > 0 && siteUrl) { - const pending = new Set(); - for (const file of cdnFiles) { - const task = (async () => { - const content = readFileSync(file.localPath); - const uploadResponse = await fetch(`${siteUrl}/fs/upload`, { - method: "POST", - headers: { "Content-Type": file.contentType }, - body: content, - }); - if (!uploadResponse.ok) { - throw new Error(`CDN upload failed for ${file.path}: ${uploadResponse.status}`); - } - const { blobId } = (await uploadResponse.json()); - allAssets.push({ - path: file.path, - blobId, - contentType: file.contentType, - deploymentId, - }); - completed++; - console.log(` [${completed}/${total}] ${file.path} (cdn)`); - })().then(() => { pending.delete(task); }); - pending.add(task); - if (pending.size >= concurrency) { - await Promise.race(pending); - } + } + // Upload CDN files (still uses per-file calls since CDN has its own upload endpoint) + if (cdnFiles.length > 0 && siteUrl) { + const pending = new Set(); + for (const file of cdnFiles) { + const task = (async () => { + const content = readFileSync(file.localPath); + const uploadResponse = await fetch(`${siteUrl}/fs/upload`, { + method: "POST", + headers: { "Content-Type": file.contentType }, + body: content, + }); + if (!uploadResponse.ok) { + throw new Error( + `CDN upload failed for ${file.path}: ${uploadResponse.status}`, + ); } - await Promise.all(pending); + const { blobId } = await uploadResponse.json(); + allAssets.push({ + path: file.path, + blobId, + contentType: file.contentType, + deploymentId, + }); + completed++; + console.log(` [${completed}/${total}] ${file.path} (cdn)`); + })().then(() => { + pending.delete(task); + }); + pending.add(task); + if (pending.size >= concurrency) { + await Promise.race(pending); + } } - // Step 3: Record all assets in one batch call - if (allAssets.length > 0) { - console.log(" Recording assets..."); - // recordAssets only handles storageId assets; CDN assets need individual recording - const storageAssets = allAssets.filter((a) => a.storageId); - const cdnAssets = allAssets.filter((a) => a.blobId); - if (storageAssets.length > 0) { - await convexRunAsync(`${componentName}:recordAssets`, { - assets: storageAssets.map((a) => ({ - path: a.path, - storageId: a.storageId, - contentType: a.contentType, - deploymentId: a.deploymentId, - })), - }); - } - // CDN assets still need individual recording (they use blobId not storageId) - for (const asset of cdnAssets) { - await convexRunAsync(`${componentName}:recordAsset`, { - path: asset.path, - blobId: asset.blobId, - contentType: asset.contentType, - deploymentId: asset.deploymentId, - }); - } + await Promise.all(pending); + } + // Step 3: Record all assets in one batch call + if (allAssets.length > 0) { + console.log(" Recording assets..."); + // recordAssets only handles storageId assets; CDN assets need individual recording + const storageAssets = allAssets.filter((a) => a.storageId); + const cdnAssets = allAssets.filter((a) => a.blobId); + if (storageAssets.length > 0) { + await convexRunAsync(`${componentName}:recordAssets`, { + assets: storageAssets.map((a) => ({ + path: a.path, + storageId: a.storageId, + contentType: a.contentType, + deploymentId: a.deploymentId, + })), + }); + } + // CDN assets still need individual recording (they use blobId not storageId) + for (const asset of cdnAssets) { + await convexRunAsync(`${componentName}:recordAsset`, { + path: asset.path, + blobId: asset.blobId, + contentType: asset.contentType, + deploymentId: asset.deploymentId, + }); } + } } function collectFiles(dir, baseDir) { - const files = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...collectFiles(fullPath, baseDir)); - } - else if (entry.isFile()) { - files.push({ - path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"), - localPath: fullPath, - contentType: getMimeType(fullPath), - }); - } + const files = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectFiles(fullPath, baseDir)); + } else if (entry.isFile()) { + files.push({ + path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"), + localPath: fullPath, + contentType: getMimeType(fullPath), + }); } - return files; + } + return files; } async function main() { - const args = parseArgs(process.argv.slice(2)); - if (args.help) { - showHelp(); - process.exit(0); - } - // Set global prod flag - useProd = args.prod; - // Run build if requested - if (args.build) { - let convexUrl = null; - if (useProd) { - convexUrl = getConvexEnv("CONVEX_CLOUD_URL", true); - if (!convexUrl) { - console.error("Could not get production Convex URL."); - console.error("Make sure you have deployed to production: npx convex deploy"); - process.exit(1); - } - } - else { - convexUrl = - getConvexEnv("CONVEX_CLOUD_URL", false) ?? getEnvFileConvexUrl(); - } - if (!convexUrl) { - console.error("Could not determine Convex URL for build."); - process.exit(1); - } - const envLabel = useProd ? "production" : "development"; - console.log(`🔨 Building for ${envLabel}...`); - console.log(` VITE_CONVEX_URL=${convexUrl}`); - console.log(""); - const buildResult = spawnNpmRun("build", { - ...process.env, - VITE_CONVEX_URL: convexUrl, - }); - if (buildResult !== 0) { - console.error("Build failed."); - process.exit(1); - } - console.log(""); - } - const distDir = resolve(args.dist); - const componentName = args.component; - const useCdn = args.cdn; - // Convex storage deployment - if (!existsSync(distDir)) { - console.error(`Error: dist directory not found: ${distDir}`); - console.error("Run your build command first (e.g., 'npm run build' or add --build flag)"); + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + showHelp(); + process.exit(0); + } + // Set global prod flag + useProd = args.prod; + // Run build if requested + if (args.build) { + let convexUrl = null; + if (useProd) { + convexUrl = getConvexEnv("CONVEX_CLOUD_URL", true); + if (!convexUrl) { + console.error("Could not get production Convex URL."); + console.error( + "Make sure you have deployed to production: npx convex deploy", + ); process.exit(1); + } + } else { + convexUrl = + getConvexEnv("CONVEX_CLOUD_URL", false) ?? getEnvFileConvexUrl(); } - // If CDN mode, we need the site URL for uploading to convex-fs - let siteUrl = null; - if (useCdn) { - siteUrl = getConvexSiteUrl(useProd); - if (!siteUrl) { - console.error("Error: Could not determine Convex site URL for CDN uploads."); - console.error("Make sure your Convex deployment is running."); - process.exit(1); - } + if (!convexUrl) { + console.error("Could not determine Convex URL for build."); + process.exit(1); } - const deploymentId = randomUUID(); - const files = collectFiles(distDir, distDir); const envLabel = useProd ? "production" : "development"; - console.log(`🚀 Deploying to ${envLabel} environment`); - if (useCdn) { - console.log("☁️ CDN mode: non-HTML assets will be uploaded to convex-fs"); - } - console.log("🔒 Using secure internal functions (requires Convex CLI auth)"); - console.log(`Uploading ${files.length} files with deployment ID: ${deploymentId}`); - console.log(`Component: ${componentName}`); + console.log(`🔨 Building for ${envLabel}...`); + console.log(` VITE_CONVEX_URL=${convexUrl}`); console.log(""); - try { - await uploadWithConcurrency(files, componentName, deploymentId, useCdn, siteUrl, args.concurrency); - } - catch { - console.error("Upload failed."); - process.exit(1); + const buildResult = spawnShell("build", { + ...process.env, + VITE_CONVEX_URL: convexUrl, + }); + if (buildResult !== 0) { + console.error("Build failed."); + process.exit(1); } console.log(""); - // Garbage collect old files - const gcOutput = await convexRunAsync(`${componentName}:gcOldAssets`, { - currentDeploymentId: deploymentId, - }); - const gcResult = JSON.parse(gcOutput); - // Handle both old format (number) and new format ({ deleted, blobIds }) - const deletedCount = typeof gcResult === "number" ? gcResult : gcResult.deleted; - const oldBlobIds = typeof gcResult === "object" && gcResult.blobIds ? gcResult.blobIds : []; - if (deletedCount > 0) { - console.log(`Cleaned up ${deletedCount} old storage file(s) from previous deployments`); + } + const distDir = resolve(args.dist); + const componentName = args.component; + const useCdn = args.cdn; + // Convex storage deployment + if (!existsSync(distDir)) { + console.error(`Error: dist directory not found: ${distDir}`); + console.error( + "Run your build command first (e.g., 'npm run build' or add --build flag)", + ); + process.exit(1); + } + // If CDN mode, we need the site URL for uploading to convex-fs + let siteUrl = null; + if (useCdn) { + siteUrl = getConvexSiteUrl(useProd); + if (!siteUrl) { + console.error( + "Error: Could not determine Convex site URL for CDN uploads.", + ); + console.error("Make sure your Convex deployment is running."); + process.exit(1); } - // Clean up old CDN blobs if any - if (oldBlobIds.length > 0) { - const cdnDeleteFn = args.cdnDeleteFunction || `${componentName}:deleteCdnBlobs`; - try { - await convexRunAsync(cdnDeleteFn, { blobIds: oldBlobIds }); - console.log(`Cleaned up ${oldBlobIds.length} old CDN blob(s) from previous deployments`); - } - catch { - console.warn(`Warning: Could not delete old CDN blobs. Make sure ${cdnDeleteFn} is defined.`); - } + } + const deploymentId = randomUUID(); + const files = collectFiles(distDir, distDir); + const envLabel = useProd ? "production" : "development"; + console.log(`🚀 Deploying to ${envLabel} environment`); + if (useCdn) { + console.log("☁️ CDN mode: non-HTML assets will be uploaded to convex-fs"); + } + console.log("🔒 Using secure internal functions (requires Convex CLI auth)"); + console.log( + `Uploading ${files.length} files with deployment ID: ${deploymentId}`, + ); + console.log(`Component: ${componentName}`); + console.log(""); + try { + await uploadWithConcurrency( + files, + componentName, + deploymentId, + useCdn, + siteUrl, + args.concurrency, + ); + } catch { + console.error("Upload failed."); + process.exit(1); + } + console.log(""); + // Garbage collect old files + const gcOutput = await convexRunAsync(`${componentName}:gcOldAssets`, { + currentDeploymentId: deploymentId, + }); + const gcResult = JSON.parse(gcOutput); + // Handle both old format (number) and new format ({ deleted, blobIds }) + const deletedCount = + typeof gcResult === "number" ? gcResult : gcResult.deleted; + const oldBlobIds = + typeof gcResult === "object" && gcResult.blobIds ? gcResult.blobIds : []; + if (deletedCount > 0) { + console.log( + `Cleaned up ${deletedCount} old storage file(s) from previous deployments`, + ); + } + // Clean up old CDN blobs if any + if (oldBlobIds.length > 0) { + const cdnDeleteFn = + args.cdnDeleteFunction || `${componentName}:deleteCdnBlobs`; + try { + await convexRunAsync(cdnDeleteFn, { blobIds: oldBlobIds }); + console.log( + `Cleaned up ${oldBlobIds.length} old CDN blob(s) from previous deployments`, + ); + } catch { + console.warn( + `Warning: Could not delete old CDN blobs. Make sure ${cdnDeleteFn} is defined.`, + ); } + } + console.log(""); + console.log("✨ Upload complete!"); + // Show the deployment URL + const deployedSiteUrl = getConvexSiteUrl(useProd); + if (deployedSiteUrl) { console.log(""); - console.log("✨ Upload complete!"); - // Show the deployment URL - const deployedSiteUrl = getConvexSiteUrl(useProd); - if (deployedSiteUrl) { - console.log(""); - console.log(`Your app is now available at: ${deployedSiteUrl}`); - } + console.log(`Your app is now available at: ${deployedSiteUrl}`); + } } /** * Get the Convex site URL (.convex.site) */ function getConvexSiteUrl(prod) { - const siteUrl = getConvexEnv("CONVEX_SITE_URL", prod); - if (siteUrl) { - return siteUrl; - } - const cloudUrl = getConvexEnv("CONVEX_CLOUD_URL", prod); - if (cloudUrl?.includes(".convex.cloud")) { - return cloudUrl.replace(".convex.cloud", ".convex.site"); - } - return null; + const siteUrl = getConvexEnv("CONVEX_SITE_URL", prod); + if (siteUrl) { + return siteUrl; + } + const cloudUrl = getConvexEnv("CONVEX_CLOUD_URL", prod); + if (cloudUrl?.includes(".convex.cloud")) { + return cloudUrl.replace(".convex.cloud", ".convex.site"); + } + return null; } main().catch((error) => { - console.error("Upload failed:", error); - process.exit(1); + console.error("Upload failed:", error); + process.exit(1); }); -//# sourceMappingURL=upload.js.map \ No newline at end of file +//# sourceMappingURL=upload.js.map diff --git a/src/cli/commands.ts b/src/cli/commands.ts index d9402a8..a7d8b72 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -47,12 +47,12 @@ export function spawnConvex(args: string[]): number | null { }).status; } -export function spawnNpmRun( - script: string, +export function spawnShell( + command: string, env: NodeJS.ProcessEnv = process.env, ): number | null { - const command = shellCommand(`npm run ${script}`); - return spawnSync(command.command, command.args, { + const cmd = shellCommand(command); + return spawnSync(cmd.command, cmd.args, { env, stdio: "inherit", }).status; diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index b9f144d..f271c6c 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -18,7 +18,7 @@ import { resolve } from "path"; import { runConvex, spawnConvex, - spawnNpmRun, + spawnShell, spawnStaticHostingCli, } from "./commands.js"; @@ -29,6 +29,7 @@ interface ParsedArgs { skipBuild: boolean; skipConvex: boolean; cdn: boolean; + buildCommand: string; } function parseArgs(args: string[]): ParsedArgs { @@ -39,6 +40,7 @@ function parseArgs(args: string[]): ParsedArgs { skipBuild: false, skipConvex: false, cdn: false, + buildCommand: "npm run build", }; for (let i = 0; i < args.length; i++) { @@ -55,6 +57,9 @@ function parseArgs(args: string[]): ParsedArgs { result.skipConvex = true; } else if (arg === "--cdn") { result.cdn = true; + } else if (arg === "--build-command") { + const cmd = args[++i]; + if (cmd) result.buildCommand = cmd; } } @@ -75,6 +80,7 @@ Options: the registered component name from convex.config.ts. --skip-build Skip the build step (use existing dist) --skip-convex Skip Convex backend deployment + --build-command Build command to run (default: 'npm run build') --cdn Upload non-HTML assets to convex-fs CDN -h, --help Show this help message @@ -154,9 +160,11 @@ async function uploadToConvexStorage( useCdn: boolean, ): Promise { console.log(""); - console.log(useCdn - ? "📦 Uploading static files (HTML to Convex, assets to CDN)..." - : "📦 Uploading static files to Convex storage..."); + console.log( + useCdn + ? "📦 Uploading static files (HTML to Convex, assets to CDN)..." + : "📦 Uploading static files to Convex storage...", + ); console.log(""); const uploadArgs = [ @@ -198,7 +206,9 @@ async function main(): Promise { let convexUrl = getConvexProdUrl(); if (!convexUrl && !args.skipConvex) { - console.log(" No production deployment found. Will get URL after deploying backend."); + console.log( + " No production deployment found. Will get URL after deploying backend.", + ); } else if (convexUrl) { console.log(` ✓ ${convexUrl}`); } @@ -225,7 +235,9 @@ async function main(): Promise { convexUrl = getConvexProdUrl(); if (!convexUrl) { console.error(""); - console.error("❌ Could not get production Convex URL after deployment"); + console.error( + "❌ Could not get production Convex URL after deployment", + ); process.exit(1); } console.log(""); @@ -242,11 +254,12 @@ async function main(): Promise { const basePath = fetchBasePath(args.component); - console.log(` Building with VITE_CONVEX_URL=${convexUrl}`); + console.log(` Build command: ${args.buildCommand}`); + console.log(` VITE_CONVEX_URL=${convexUrl}`); console.log(` STATIC_HOSTING_BASE_PATH=${basePath}`); console.log(""); - const buildResult = spawnNpmRun("build", { + const buildResult = spawnShell(args.buildCommand, { ...process.env, VITE_CONVEX_URL: convexUrl, STATIC_HOSTING_BASE_PATH: basePath, @@ -283,7 +296,9 @@ async function main(): Promise { console.log(" ✓ Convex backend deployed"); } else { console.log(""); - console.log("Step 3: Skipping Convex deployment (--skip-convex or already deployed)"); + console.log( + "Step 3: Skipping Convex deployment (--skip-convex or already deployed)", + ); } // Step 4: Deploy static files @@ -299,7 +314,11 @@ async function main(): Promise { process.exit(1); } - const staticDeploySuccess = await uploadToConvexStorage(distDir, args.component, args.cdn); + const staticDeploySuccess = await uploadToConvexStorage( + distDir, + args.component, + args.cdn, + ); if (!staticDeploySuccess) { console.error(""); diff --git a/src/cli/upload.ts b/src/cli/upload.ts index bd043ea..eb73d13 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -15,7 +15,7 @@ import { readFileSync, readdirSync, existsSync } from "fs"; import { join, relative, extname, resolve } from "path"; import { randomUUID } from "crypto"; -import { runConvex, runConvexAsync, spawnNpmRun } from "./commands.js"; +import { runConvex, runConvexAsync, spawnShell } from "./commands.js"; // MIME type mapping const MIME_TYPES: Record = { @@ -49,6 +49,7 @@ interface ParsedArgs { component: string; prod: boolean; build: boolean; + buildCommand: string; cdn: boolean; cdnDeleteFunction: string; concurrency: number; @@ -61,6 +62,7 @@ function parseArgs(args: string[]): ParsedArgs { component: "staticHosting", prod: false, // Default to dev, use --prod for production build: false, + buildCommand: "npm run build", cdn: false, cdnDeleteFunction: "", concurrency: 5, @@ -81,6 +83,12 @@ function parseArgs(args: string[]): ParsedArgs { result.prod = false; } else if (arg === "--build" || arg === "-b") { result.build = true; + } else if (arg === "--build-command") { + const cmd = args[++i]; + if (cmd) { + result.buildCommand = cmd; + result.build = true; + } } else if (arg === "--cdn") { result.cdn = true; } else if (arg === "--cdn-delete-function") { @@ -104,7 +112,10 @@ Options: -d, --dist Path to dist directory (default: ./dist) -c, --component Static-hosting component instance name (default: staticHosting) --prod Deploy to production deployment - -b, --build Run 'npm run build' with correct VITE_CONVEX_URL before uploading + -b, --build Run the build command with VITE_CONVEX_URL + + STATIC_HOSTING_BASE_PATH set before uploading + --build-command Build command to run (default: 'npm run build'). + Implies --build. --cdn Upload non-HTML assets to convex-fs CDN instead of Convex storage --cdn-delete-function App function to delete CDN blobs (e.g. staticHosting:deleteCdnBlobs) -j, --concurrency Number of parallel uploads (default: 5) @@ -384,11 +395,12 @@ async function main(): Promise { const envLabel = useProd ? "production" : "development"; console.log(`🔨 Building for ${envLabel}...`); + console.log(` Build command: ${args.buildCommand}`); console.log(` VITE_CONVEX_URL=${convexUrl}`); console.log(` STATIC_HOSTING_BASE_PATH=${basePath}`); console.log(""); - const buildResult = spawnNpmRun("build", { + const buildResult = spawnShell(args.buildCommand, { ...process.env, VITE_CONVEX_URL: convexUrl, STATIC_HOSTING_BASE_PATH: basePath, From d3cd241f77c84d35f58ca2f3837638873dc4707f Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Fri, 15 May 2026 02:39:49 -0700 Subject: [PATCH 09/29] use full path everywhere --- example/convex/convex.config.ts | 1 + package.json | 2 +- src/cli/deploy.ts | 27 ++++++------ src/cli/upload.ts | 74 ++++++++++++--------------------- src/component/lib.ts | 16 ++++--- 5 files changed, 49 insertions(+), 71 deletions(-) diff --git a/example/convex/convex.config.ts b/example/convex/convex.config.ts index bd52092..cb02407 100644 --- a/example/convex/convex.config.ts +++ b/example/convex/convex.config.ts @@ -2,6 +2,7 @@ import { defineApp } from "convex/server"; import staticHosting from "@convex-dev/static-hosting/convex.config.js"; const app = defineApp({ httpPrefix: "/api" }); + app.use(staticHosting, { httpPrefix: "/" }); export default app; diff --git a/package.json b/package.json index 2c763fd..d93ea41 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dev:frontend": "cd example && vite --clearScreen false", "build:example": "cd example && vite build", "upload:static": "node dist/cli/index.js upload --dist ./example/dist --component staticHosting", - "deploy:static": "npm run build:example && npm run upload:static", + "deploy:static": "node dist/cli/index.js upload --dist ./example/dist --component staticHosting --build --build-command 'npm run build:example'", "dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial", "predev": "path-exists .env.local dist || (npm run build && convex dev --once)", "build": "tsc --project ./tsconfig.build.json", diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index f271c6c..b4d3a6f 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -102,19 +102,21 @@ Examples: } /** - * Resolve the component's mount prefix from its CONVEX_SITE_URL. Returns "/" - * if the component isn't deployed yet or the query is unavailable. + * Resolve the component's full site URL (CONVEX_SITE_URL with mount prefix). + * Bails the CLI if the component isn't deployed. */ -function fetchBasePath(componentName: string): string { +function fetchSiteUrl(componentName: string): string { try { const out = execSync( - `npx convex run --component ${componentName} lib:getBasePath '{}' --prod --typecheck=disable --codegen=disable`, + `npx convex run --component ${componentName} lib:getSiteUrl '{}' --prod --typecheck=disable --codegen=disable`, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }, ).trim(); - const value = JSON.parse(out); - return typeof value === "string" && value.length > 0 ? value : "/"; + return JSON.parse(out); } catch { - return "/"; + console.error( + `Could not reach component "${componentName}". Deploy the Convex backend first (npx convex deploy) and ensure --component matches the name in convex.config.ts.`, + ); + process.exit(1); } } @@ -128,7 +130,6 @@ function getConvexProdUrl(): string | null { // Fall back to env files } - // Try env files as fallback const envFiles = [".env.production", ".env.production.local", ".env.local"]; for (const envFile of envFiles) { if (existsSync(envFile)) { @@ -214,6 +215,8 @@ async function main(): Promise { } // Step 2: Build frontend + let siteUrl: string | null = null; + if (!args.skipBuild) { console.log(""); console.log("Step 2: Building frontend..."); @@ -252,7 +255,8 @@ async function main(): Promise { process.exit(1); } - const basePath = fetchBasePath(args.component); + siteUrl = fetchSiteUrl(args.component); + const basePath = new URL(siteUrl).pathname || "/"; console.log(` Build command: ${args.buildCommand}`); console.log(` VITE_CONVEX_URL=${convexUrl}`); @@ -334,10 +338,7 @@ async function main(): Promise { console.log(`✨ Deployment complete! (${duration}s)`); console.log(""); - const siteUrl = getConvexProdSiteUrl(); - if (siteUrl) { - console.log(`Frontend: ${siteUrl}`); - } + console.log(`Frontend: ${siteUrl ?? fetchSiteUrl(args.component)}`); console.log(""); } diff --git a/src/cli/upload.ts b/src/cli/upload.ts index eb73d13..2a12851 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -154,16 +154,19 @@ function getConvexEnv(name: string, prod: boolean): string | null { } /** - * Resolve the component's mount prefix from CONVEX_SITE_URL. Returns "/" if - * the component isn't deployed yet or the query is unavailable. + * Resolve the component's full site URL (CONVEX_SITE_URL with mount prefix). + * Bails the CLI if the component isn't deployed — uploading wouldn't work + * either, so a fallback would only hide the real problem. */ -async function fetchBasePath(componentName: string): Promise { +async function fetchSiteUrl(componentName: string): Promise { try { - const out = await convexRunComponentAsync(componentName, "lib:getBasePath"); - const value = JSON.parse(out); - return typeof value === "string" && value.length > 0 ? value : "/"; + const out = await convexRunComponentAsync(componentName, "lib:getSiteUrl"); + return JSON.parse(out); } catch { - return "/"; + console.error( + `Could not reach component "${componentName}". Deploy the Convex backend first and ensure --component matches the name in convex.config.ts.`, + ); + process.exit(1); } } @@ -188,7 +191,7 @@ async function uploadWithConcurrency( componentName: string, deploymentId: string, useCdn: boolean, - siteUrl: string | null, + cdnUploadBase: string | null, concurrency: number, ): Promise { const total = files.length; @@ -198,7 +201,7 @@ async function uploadWithConcurrency( const storageFiles: typeof files = []; for (const file of files) { const isHtml = file.contentType.startsWith("text/html"); - if (useCdn && !isHtml && siteUrl) { + if (useCdn && !isHtml && cdnUploadBase) { cdnFiles.push(file); } else { storageFiles.push(file); @@ -267,12 +270,12 @@ async function uploadWithConcurrency( } // Upload CDN files (still uses per-file calls since CDN has its own upload endpoint) - if (cdnFiles.length > 0 && siteUrl) { + if (cdnFiles.length > 0 && cdnUploadBase) { const pending = new Set>(); for (const file of cdnFiles) { const task = (async () => { const content = readFileSync(file.localPath); - const uploadResponse = await fetch(`${siteUrl}/fs/upload`, { + const uploadResponse = await fetch(`${cdnUploadBase}/fs/upload`, { method: "POST", headers: { "Content-Type": file.contentType }, body: content, @@ -368,6 +371,11 @@ async function main(): Promise { // Set global prod flag useProd = args.prod; + // Resolve where the app is served. The component knows its own mount via + // CONVEX_SITE_URL; on first deploy the query may not exist yet, in which + // case we fall back below to the deployment root only. + const componentSiteUrl = await fetchSiteUrl(args.component); + // Run build if requested if (args.build) { let convexUrl: string | null = null; @@ -391,7 +399,7 @@ async function main(): Promise { process.exit(1); } - const basePath = await fetchBasePath(args.component); + const basePath = new URL(componentSiteUrl).pathname || "/"; const envLabel = useProd ? "production" : "development"; console.log(`🔨 Building for ${envLabel}...`); @@ -428,18 +436,9 @@ async function main(): Promise { process.exit(1); } - // If CDN mode, we need the site URL for uploading to convex-fs - let siteUrl: string | null = null; - if (useCdn) { - siteUrl = getConvexSiteUrl(useProd); - if (!siteUrl) { - console.error( - "Error: Could not determine Convex site URL for CDN uploads.", - ); - console.error("Make sure your Convex deployment is running."); - process.exit(1); - } - } + // /fs/upload lives at the deployment root, not under the component's + // mount prefix. + const cdnUploadBase = useCdn ? new URL(componentSiteUrl).origin : null; const deploymentId = randomUUID(); const files = collectFiles(distDir, distDir); @@ -462,7 +461,7 @@ async function main(): Promise { componentName, deploymentId, useCdn, - siteUrl, + cdnUploadBase, args.concurrency, ); } catch { @@ -511,29 +510,8 @@ async function main(): Promise { console.log(""); console.log("✨ Upload complete!"); - // Show the deployment URL - const deployedSiteUrl = getConvexSiteUrl(useProd); - if (deployedSiteUrl) { - console.log(""); - console.log(`Your app is now available at: ${deployedSiteUrl}`); - } -} - -/** - * Get the Convex site URL (.convex.site) - */ -function getConvexSiteUrl(prod: boolean): string | null { - const siteUrl = getConvexEnv("CONVEX_SITE_URL", prod); - if (siteUrl) { - return siteUrl; - } - - const cloudUrl = getConvexEnv("CONVEX_CLOUD_URL", prod); - if (cloudUrl?.includes(".convex.cloud")) { - return cloudUrl.replace(".convex.cloud", ".convex.site"); - } - - return null; + console.log(""); + console.log(`Your app is now available at: ${componentSiteUrl}`); } main().catch((error) => { diff --git a/src/component/lib.ts b/src/component/lib.ts index ff5a96c..33cf2b0 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -30,18 +30,16 @@ export const getCurrentDeployment = query({ }, }); -export const getBasePath = internalQuery({ +// Returns the URL where the static site is served (CONVEX_SITE_URL, which +// includes the component's mount prefix). The CLI uses this to: +// - derive STATIC_HOSTING_BASE_PATH for the bundler +// - print where the deployed app lives +// - resolve the deployment root for platform endpoints like /fs/upload +export const getSiteUrl = internalQuery({ args: {}, returns: v.string(), handler: async () => { - const siteUrl = process.env.CONVEX_SITE_URL; - if (!siteUrl) return "/"; - try { - const pathname = new URL(siteUrl).pathname; - return pathname || "/"; - } catch { - return "/"; - } + return process.env.CONVEX_SITE_URL!; }, }); From dcf6154e0e0e35cb156a21c09f6ec96ebacf6a84 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Wed, 20 May 2026 19:13:53 -0700 Subject: [PATCH 10/29] get both urls at once authoriatively --- src/cli/deploy.ts | 109 +++++++++++++++++++------------------------ src/cli/upload.ts | 64 +++++++------------------ src/component/lib.ts | 25 ++++++---- 3 files changed, 80 insertions(+), 118 deletions(-) diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index b4d3a6f..4327c21 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -13,7 +13,7 @@ * The goal is to minimize the inconsistency window between backend and frontend. */ -import { existsSync, readFileSync } from "fs"; +import { existsSync } from "fs"; import { resolve } from "path"; import { runConvex, @@ -101,47 +101,43 @@ Examples: `); } +interface DeploymentUrls { + siteUrl: string; + cloudUrl: string; +} + /** - * Resolve the component's full site URL (CONVEX_SITE_URL with mount prefix). - * Bails the CLI if the component isn't deployed. + * Resolve the component's deployment URLs (siteUrl + cloudUrl). Returns null + * if the component isn't reachable yet — on first deploy the backend may not + * exist, in which case the caller should deploy it first and retry. */ -function fetchSiteUrl(componentName: string): string { +function tryFetchUrls(componentName: string): DeploymentUrls | null { try { - const out = execSync( - `npx convex run --component ${componentName} lib:getSiteUrl '{}' --prod --typecheck=disable --codegen=disable`, - { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }, - ).trim(); + const out = runConvex([ + "run", + "--component", + componentName, + "lib:getUrls", + "{}", + "--prod", + "--typecheck=disable", + "--codegen=disable", + ]); return JSON.parse(out); } catch { + return null; + } +} + +function fetchUrls(componentName: string): DeploymentUrls { + const urls = tryFetchUrls(componentName); + if (!urls) { console.error( `Could not reach component "${componentName}". Deploy the Convex backend first (npx convex deploy) and ensure --component matches the name in convex.config.ts.`, ); process.exit(1); } -} - -/** - * Get the production Convex URL - */ -function getConvexProdUrl(): string | null { - try { - return runConvex(["env", "get", "CONVEX_CLOUD_URL", "--prod"]) || null; - } catch { - // Fall back to env files - } - - const envFiles = [".env.production", ".env.production.local", ".env.local"]; - for (const envFile of envFiles) { - if (existsSync(envFile)) { - const content = readFileSync(envFile, "utf-8"); - const match = content.match(/(?:VITE_)?CONVEX_URL=(.+)/); - if (match) { - return match[1].trim(); - } - } - } - - return null; + return urls; } function getConvexProdSiteUrl(): string | null { @@ -200,30 +196,29 @@ async function main(): Promise { const startTime = Date.now(); - // Step 1: Get production Convex URL (needed for build) + // Step 1: Get deployment URLs (needed for build) console.log(""); - console.log("Step 1: Getting production Convex URL..."); + console.log("Step 1: Getting deployment URLs..."); - let convexUrl = getConvexProdUrl(); + let urls = tryFetchUrls(args.component); - if (!convexUrl && !args.skipConvex) { + if (!urls && !args.skipConvex) { console.log( - " No production deployment found. Will get URL after deploying backend.", + " Component not yet deployed. Will fetch URLs after deploying backend.", ); - } else if (convexUrl) { - console.log(` ✓ ${convexUrl}`); + } else if (urls) { + console.log(` ✓ ${urls.siteUrl}`); } // Step 2: Build frontend - let siteUrl: string | null = null; - if (!args.skipBuild) { console.log(""); console.log("Step 2: Building frontend..."); - // If we don't have a URL yet, we need to deploy Convex first to get it - if (!convexUrl && !args.skipConvex) { - console.log(" Deploying Convex backend first to get production URL..."); + // If the component isn't deployed yet, deploy the backend first so we + // can ask it for the URLs. + if (!urls && !args.skipConvex) { + console.log(" Deploying Convex backend first to get URLs..."); console.log(""); const convexResult = spawnConvex(["deploy"]); @@ -234,38 +229,29 @@ async function main(): Promise { process.exit(1); } - // Now get the URL - convexUrl = getConvexProdUrl(); - if (!convexUrl) { - console.error(""); - console.error( - "❌ Could not get production Convex URL after deployment", - ); - process.exit(1); - } + urls = fetchUrls(args.component); console.log(""); - console.log(` ✓ Production URL: ${convexUrl}`); + console.log(` ✓ Site URL: ${urls.siteUrl}`); args.skipConvex = true; // Already deployed } - if (!convexUrl) { + if (!urls) { console.error(""); - console.error("❌ Could not determine Convex URL for build"); + console.error("❌ Could not determine deployment URLs for build"); console.error(" Run 'npx convex deploy' first or remove --skip-convex"); process.exit(1); } - siteUrl = fetchSiteUrl(args.component); - const basePath = new URL(siteUrl).pathname || "/"; + const basePath = new URL(urls.siteUrl).pathname || "/"; console.log(` Build command: ${args.buildCommand}`); - console.log(` VITE_CONVEX_URL=${convexUrl}`); + console.log(` VITE_CONVEX_URL=${urls.cloudUrl}`); console.log(` STATIC_HOSTING_BASE_PATH=${basePath}`); console.log(""); const buildResult = spawnShell(args.buildCommand, { ...process.env, - VITE_CONVEX_URL: convexUrl, + VITE_CONVEX_URL: urls.cloudUrl, STATIC_HOSTING_BASE_PATH: basePath, }); @@ -338,7 +324,8 @@ async function main(): Promise { console.log(`✨ Deployment complete! (${duration}s)`); console.log(""); - console.log(`Frontend: ${siteUrl ?? fetchSiteUrl(args.component)}`); + const finalUrls = urls ?? fetchUrls(args.component); + console.log(`Frontend: ${finalUrls.siteUrl}`); console.log(""); } diff --git a/src/cli/upload.ts b/src/cli/upload.ts index 2a12851..546ea7d 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -15,7 +15,7 @@ import { readFileSync, readdirSync, existsSync } from "fs"; import { join, relative, extname, resolve } from "path"; import { randomUUID } from "crypto"; -import { runConvex, runConvexAsync, spawnShell } from "./commands.js"; +import { runConvexAsync, spawnShell } from "./commands.js"; // MIME type mapping const MIME_TYPES: Record = { @@ -135,32 +135,21 @@ Examples: // Global flag for production mode let useProd = true; -function getEnvFileConvexUrl(): string | null { - if (!existsSync(".env.local")) { - return null; - } - - const envContent = readFileSync(".env.local", "utf-8"); - const match = envContent.match(/(?:VITE_)?CONVEX_URL=(.+)/); - return match?.[1]?.trim() || null; -} - -function getConvexEnv(name: string, prod: boolean): string | null { - try { - return runConvex(["env", "get", name, ...(prod ? ["--prod"] : [])]) || null; - } catch { - return null; - } +interface DeploymentUrls { + /** CONVEX_SITE_URL — includes the component's mount prefix. */ + siteUrl: string; + /** CONVEX_CLOUD_URL — backend URL the frontend connects to. */ + cloudUrl: string; } /** - * Resolve the component's full site URL (CONVEX_SITE_URL with mount prefix). - * Bails the CLI if the component isn't deployed — uploading wouldn't work - * either, so a fallback would only hide the real problem. + * Resolve the component's deployment URLs. Bails the CLI if the component + * isn't deployed — uploading wouldn't work either, so a fallback would only + * hide the real problem. */ -async function fetchSiteUrl(componentName: string): Promise { +async function fetchUrls(componentName: string): Promise { try { - const out = await convexRunComponentAsync(componentName, "lib:getSiteUrl"); + const out = await convexRunAsync(componentName, "lib:getUrls"); return JSON.parse(out); } catch { console.error( @@ -371,34 +360,15 @@ async function main(): Promise { // Set global prod flag useProd = args.prod; - // Resolve where the app is served. The component knows its own mount via - // CONVEX_SITE_URL; on first deploy the query may not exist yet, in which - // case we fall back below to the deployment root only. - const componentSiteUrl = await fetchSiteUrl(args.component); + // The component knows both its CONVEX_SITE_URL (where the app is served, + // including the mount prefix) and CONVEX_CLOUD_URL (what the frontend + // connects to). We fetch both in one call and trust neither hostname. + const { siteUrl: componentSiteUrl, cloudUrl: convexUrl } = await fetchUrls( + args.component, + ); // Run build if requested if (args.build) { - let convexUrl: string | null = null; - - if (useProd) { - convexUrl = getConvexEnv("CONVEX_CLOUD_URL", true); - if (!convexUrl) { - console.error("Could not get production Convex URL."); - console.error( - "Make sure you have deployed to production: npx convex deploy", - ); - process.exit(1); - } - } else { - convexUrl = - getConvexEnv("CONVEX_CLOUD_URL", false) ?? getEnvFileConvexUrl(); - } - - if (!convexUrl) { - console.error("Could not determine Convex URL for build."); - process.exit(1); - } - const basePath = new URL(componentSiteUrl).pathname || "/"; const envLabel = useProd ? "production" : "development"; diff --git a/src/component/lib.ts b/src/component/lib.ts index 33cf2b0..99cc089 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -30,17 +30,22 @@ export const getCurrentDeployment = query({ }, }); -// Returns the URL where the static site is served (CONVEX_SITE_URL, which -// includes the component's mount prefix). The CLI uses this to: -// - derive STATIC_HOSTING_BASE_PATH for the bundler -// - print where the deployed app lives -// - resolve the deployment root for platform endpoints like /fs/upload -export const getSiteUrl = internalQuery({ +// Returns the deployment URLs visible to this component: +// siteUrl - CONVEX_SITE_URL (includes the component's mount prefix). Used +// by the CLI to derive STATIC_HOSTING_BASE_PATH and to show +// where the deployed app lives. +// cloudUrl - CONVEX_CLOUD_URL. Used by the CLI as VITE_CONVEX_URL when +// building the frontend. +export const getUrls = internalQuery({ args: {}, - returns: v.string(), - handler: async () => { - return process.env.CONVEX_SITE_URL!; - }, + returns: v.object({ + siteUrl: v.string(), + cloudUrl: v.string(), + }), + handler: async () => ({ + siteUrl: process.env.CONVEX_SITE_URL!, + cloudUrl: process.env.CONVEX_CLOUD_URL!, + }), }); export const getByPath = internalQuery({ From d8ea9d774c9e3488e9975b5eb2c93a2d98f5ffe7 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:45:56 -0700 Subject: [PATCH 11/29] update convex peer dep --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea94710..fb39503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "chokidar-cli": "3.0.0", - "convex": "1.38.0", + "convex": "1.39.1", "convex-test": "0.0.40", "cpy-cli": "^7.0.0", "eslint": "9.39.1", @@ -42,7 +42,7 @@ "vitest": "4.0.17" }, "peerDependencies": { - "convex": "^1.35.0", + "convex": "^1.39.1", "react": "^18.3.1 || ^19.0.0" } }, @@ -3297,9 +3297,9 @@ "dev": true }, "node_modules/convex": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.38.0.tgz", - "integrity": "sha512-122AC6y5lUS7mr39cluLw9+TOtRX5d/XxeivHhHObs/NTXoVvOnIgDzexVcxaz6Rk0oLFSoydSR1rDCltEz/0A==", + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.39.1.tgz", + "integrity": "sha512-W+gVXA7BpRF1xLlS1kGTtKVaqd5yonqbGESKiPtIUXjV744GdDz8IG7RVsSY5KzHbgxuJBHKaJYk+92OIHTskQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10660,9 +10660,9 @@ "dev": true }, "convex": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.38.0.tgz", - "integrity": "sha512-122AC6y5lUS7mr39cluLw9+TOtRX5d/XxeivHhHObs/NTXoVvOnIgDzexVcxaz6Rk0oLFSoydSR1rDCltEz/0A==", + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.39.1.tgz", + "integrity": "sha512-W+gVXA7BpRF1xLlS1kGTtKVaqd5yonqbGESKiPtIUXjV744GdDz8IG7RVsSY5KzHbgxuJBHKaJYk+92OIHTskQ==", "dev": true, "requires": { "esbuild": "0.27.0", diff --git a/package.json b/package.json index d93ea41..c9344fe 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ } }, "peerDependencies": { - "convex": "^1.35.0", + "convex": "^1.39.1", "react": "^18.3.1 || ^19.0.0" }, "devDependencies": { @@ -89,7 +89,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "chokidar-cli": "3.0.0", - "convex": "1.38.0", + "convex": "1.39.1", "convex-test": "0.0.40", "cpy-cli": "^7.0.0", "eslint": "9.39.1", From d29abd124ccfe1f0414d00535634582cfd116f0d Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:46:46 -0700 Subject: [PATCH 12/29] fix rename --- src/client/setup.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/setup.test.ts b/src/client/setup.test.ts index 45cdc64..ed0da48 100644 --- a/src/client/setup.test.ts +++ b/src/client/setup.test.ts @@ -20,7 +20,7 @@ export function initConvexTest< return t; } export const components = componentsGeneric() as unknown as { - selfHosting: ComponentApi; + staticHosting: ComponentApi; }; test("setup", () => {}); From baf6cfaec9ab0196388b57f8f4d852dad1e78d89 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:54:00 -0700 Subject: [PATCH 13/29] update readme --- CHANGELOG.md | 3 +++ INTEGRATION.md | 23 +++++++++++++++++------ README.md | 17 ++++++++++++----- example/convex/convex.config.ts | 2 +- src/cli/setup.ts | 11 ++++++++--- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c8127..e22c322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ upgrading.** - Assets uploaded under 0.1.x lived in the app's storage — those references won't resolve in 0.2.x. Run `npx @convex-dev/static-hosting deploy` to repopulate. +- Recommended setup now prefixes your own HTTP routes with + `defineApp({ httpPrefix: "/api" })` so the static site can own the root + without the catch-all route shadowing them. ## 0.1.4 diff --git a/INTEGRATION.md b/INTEGRATION.md index c38ce24..cc85ce4 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -43,15 +43,26 @@ This is the only required app-side file. import { defineApp } from "convex/server"; import staticHosting from "@convex-dev/static-hosting/convex.config"; -const app = defineApp(); -app.use(staticHosting, { httpPrefix: "/" }); +// Serve your own HTTP endpoints (convex/http.ts) under /api so the static +// site can own the root. +const app = defineApp({ httpPrefix: "/api" }); +// `env` is required because the component declares (optional) env vars; leave +// it `{}` for defaults. See "SPA routing" below to override. +app.use(staticHosting, { httpPrefix: "/", env: {} }); export default app; ``` -`httpPrefix: "/"` serves the static site at the deployment root. To host under -a sub-path instead (e.g. if you have your own HTTP routes at the root), use -`httpPrefix: "/app/"` and set your bundler's base path to match. +The static site is mounted at `/` with a catch-all route, so it would shadow +any HTTP endpoints you define at the root. Passing `httpPrefix: "/api"` to +`defineApp` relocates your own `convex/http.ts` routes to `/api/...`, leaving +the root for the static site; call those endpoints from the frontend at +`/api/...`. If you have no custom HTTP routes the prefix is harmless, and +keeping it avoids a collision the first time you add one. + +To host the static site itself under a sub-path instead, use +`app.use(staticHosting, { httpPrefix: "/app/" })` and set your bundler's base +path to match (see [Mounting under a sub-path](#mounting-under-a-sub-path)). > Run `npx convex dev` after editing `convex.config.ts` so codegen picks up the > component. @@ -161,7 +172,7 @@ import staticHosting from "@convex-dev/static-hosting/convex.config"; import fs from "convex-fs/convex.config"; const app = defineApp(); -app.use(staticHosting, { httpPrefix: "/" }); +app.use(staticHosting, { httpPrefix: "/", env: {} }); app.use(fs); export default app; diff --git a/README.md b/README.md index c343c68..7c32f95 100644 --- a/README.md +++ b/README.md @@ -55,15 +55,22 @@ npm install @convex-dev/static-hosting import { defineApp } from "convex/server"; import staticHosting from "@convex-dev/static-hosting/convex.config.js"; -const app = defineApp(); -app.use(staticHosting, { httpPrefix: "/" }); +// Your own HTTP endpoints (convex/http.ts) are served under /api so the +// static site can own the root. +const app = defineApp({ httpPrefix: "/api" }); +app.use(staticHosting, { httpPrefix: "/", env: {} }); export default app; ``` -`httpPrefix: "/"` serves your static site at the deployment root. If you have -other HTTP routes in your app, you can mount the static site under a sub-path -(see [Mounting under a sub-path](#mounting-under-a-sub-path) below). +The static site is mounted at `/` with a catch-all route, so it would shadow +any HTTP routes you define at the root. Passing `httpPrefix: "/api"` to +`defineApp` moves your own `convex/http.ts` routes under `/api/...`, leaving the +root for the static site (your frontend then calls those endpoints at +`/api/...`). + +To instead host the static site itself under a sub-path, see +[Mounting under a sub-path](#mounting-under-a-sub-path) below. ### 3. Add a deploy script diff --git a/example/convex/convex.config.ts b/example/convex/convex.config.ts index cb02407..06b14b7 100644 --- a/example/convex/convex.config.ts +++ b/example/convex/convex.config.ts @@ -3,6 +3,6 @@ import staticHosting from "@convex-dev/static-hosting/convex.config.js"; const app = defineApp({ httpPrefix: "/api" }); -app.use(staticHosting, { httpPrefix: "/" }); +app.use(staticHosting, { httpPrefix: "/", env: {} }); export default app; diff --git a/src/cli/setup.ts b/src/cli/setup.ts index f487b18..7cc0da9 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -30,7 +30,10 @@ function createConvexConfig(): void { console.log( ' import staticHosting from "@convex-dev/static-hosting/convex.config";', ); - console.log(' app.use(staticHosting, { httpPrefix: "/" });\n'); + console.log(' app.use(staticHosting, { httpPrefix: "/" });'); + console.log( + ' // and prefix your own routes: defineApp({ httpPrefix: "/api" })\n', + ); return; } @@ -39,8 +42,10 @@ function createConvexConfig(): void { `import { defineApp } from "convex/server"; import staticHosting from "@convex-dev/static-hosting/convex.config"; -const app = defineApp(); -app.use(staticHosting, { httpPrefix: "/" }); +// Your own HTTP endpoints (convex/http.ts) are served under /api so the +// static site can own the root. +const app = defineApp({ httpPrefix: "/api" }); +app.use(staticHosting, { httpPrefix: "/", env: {} }); export default app; `, From 24b300933e1ef40dc714d3540107b86d28b19d1b Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:04:52 -0700 Subject: [PATCH 14/29] add SPA fallback back --- CHANGELOG.md | 6 +++++ INTEGRATION.md | 18 +++++++++++++++ README.md | 18 +++++++++++++++ src/component/_generated/server.ts | 8 +++++++ src/component/convex.config.ts | 10 ++++++++- src/component/http.ts | 35 ++++++++++++++++++++---------- 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e22c322..a1884f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ upgrading.** - Recommended setup now prefixes your own HTTP routes with `defineApp({ httpPrefix: "/api" })` so the static site can own the root without the catch-all route shadowing them. +- SPA fallback is now configurable via a env var. The component declares a + `STATIC_HOSTING_SPA_FALLBACK` env var (`"enabled"` default / `"disabled"`); + bind it per-mount via `app.use(staticHosting, { env: { ... } })` to make + extension-less misses return 404 instead of `index.html`. Because the + component declares an env var, `app.use` now requires an `env` object (pass + `env: {}` for defaults). Requires `convex` ≥ 1.39. ## 0.1.4 diff --git a/INTEGRATION.md b/INTEGRATION.md index cc85ce4..eb37075 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -282,6 +282,24 @@ export default defineConfig({ Root-mounted apps don't need this — the default is `/`. Webpack/Next.js equivalents: `publicPath` and `assetPrefix`. +## SPA routing + +Requests for an extension-less path that doesn't match an uploaded file fall +back to `index.html`, so client-side routes survive a reload. Paths with an +extension (e.g. `/missing.js`) always 404 when not found. To turn the fallback +off for a multi-page app (unknown paths become real 404s), bind the component's +`STATIC_HOSTING_SPA_FALLBACK` env var where you mount it: + +```typescript +app.use(staticHosting, { + httpPrefix: "/", + env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" }, +}); +``` + +The env var accepts `"enabled"` (default) or `"disabled"`; unset keeps the +fallback on. (Component env vars require `convex` ≥ 1.39.) + ## Troubleshooting ### 404s on every path diff --git a/README.md b/README.md index 7c32f95..6bd7716 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,24 @@ export default defineConfig({ Root-mounted apps don't need this — the default is `/`. For webpack use `publicPath`, for Next.js `assetPrefix`. +## SPA routing + +By default, requests for a path with no file extension that doesn't match an +uploaded file fall back to `index.html`, so client-side routes like +`/dashboard/settings` work on reload. For a multi-page app where unknown paths +should be a real 404, disable the fallback by binding the component's +`STATIC_HOSTING_SPA_FALLBACK` env var when you mount it: + +```ts +app.use(staticHosting, { + httpPrefix: "/", + env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" }, +}); +``` + +Requests for paths with an extension (e.g. `/missing.js`) always 404 when not +found, regardless of this setting. + ## How it works 1. **Build** — your bundler emits `dist/`. diff --git a/src/component/_generated/server.ts b/src/component/_generated/server.ts index 739b02f..04a9e96 100644 --- a/src/component/_generated/server.ts +++ b/src/component/_generated/server.ts @@ -30,6 +30,13 @@ import { } from "convex/server"; import type { DataModel } from "./dataModel.js"; +/** + * Typesafe environment variables declared in `convex.config.ts`. + */ +type Env = { + readonly STATIC_HOSTING_SPA_FALLBACK: string | undefined; +}; + /** * Define a query in this Convex app's public API. * @@ -106,6 +113,7 @@ export const internalAction: ActionBuilder = * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export const httpAction: HttpActionBuilder = httpActionGeneric; +export const env: Env = process.env as unknown as Env; /** * A set of services for use within Convex query functions. diff --git a/src/component/convex.config.ts b/src/component/convex.config.ts index dc76c53..76944b1 100644 --- a/src/component/convex.config.ts +++ b/src/component/convex.config.ts @@ -1,3 +1,11 @@ import { defineComponent } from "convex/server"; +import { v } from "convex/values"; -export default defineComponent("staticHosting"); +export default defineComponent("staticHosting", { + env: { + // Control the SPA fallback behavior: "disabled" returns 404s instead of serving index.html. + STATIC_HOSTING_SPA_FALLBACK: v.optional( + v.union(v.literal("enabled"), v.literal("disabled")), + ), + }, +}); diff --git a/src/component/http.ts b/src/component/http.ts index 6da2c2a..9a5b788 100644 --- a/src/component/http.ts +++ b/src/component/http.ts @@ -1,5 +1,5 @@ import { httpRouter } from "convex/server"; -import { httpAction } from "./_generated/server.js"; +import { env, httpAction } from "./_generated/server.js"; import { internal } from "./_generated/api.js"; const MIME_TYPES: Record = { @@ -24,18 +24,18 @@ const MIME_TYPES: Record = { ".xml": "application/xml", }; -function getMimeType(path: string): string { +export function getMimeType(path: string): string { const ext = path.substring(path.lastIndexOf(".")).toLowerCase(); return MIME_TYPES[ext] || "application/octet-stream"; } -function hasFileExtension(path: string): boolean { +export function hasFileExtension(path: string): boolean { const lastSegment = path.split("/").pop() || ""; return lastSegment.includes(".") && !lastSegment.startsWith("."); } // Vite hashed asset suffix: e.g. `index-lj_vq_aF.js`, `style-B71cUw87.css` -function isHashedAsset(path: string): boolean { +export function isHashedAsset(path: string): boolean { return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); } @@ -43,6 +43,21 @@ function isHtmlContentType(contentType: string): boolean { return contentType.startsWith("text/html"); } +// SPA fallback (serving /index.html for extension-less paths that don't match +// a file) is on by default. Bind the STATIC_HOSTING_SPA_FALLBACK env var to +// "disabled" to turn it off and return 404s instead — useful for multi-page +// apps where unknown paths should be misses. +// app.use(staticHosting, { env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" } }) +export function isSpaFallbackEnabled(): boolean { + return env.STATIC_HOSTING_SPA_FALLBACK?.trim().toLowerCase() !== "disabled"; +} + +export function cacheControlFor(path: string): string { + return isHashedAsset(path) + ? "public, max-age=31536000, immutable" + : "public, max-age=0, must-revalidate"; +} + function getSetupHtml(): string { return ` @@ -122,7 +137,7 @@ function getSetupHtml(): string { // CONVEX_SITE_URL reflects the component's mount point (including any httpPrefix). // We use it to strip the prefix from incoming request paths so lookups in // the asset table remain relative to the component (e.g. `/index.html`). -function getMountPrefix(): string { +export function getMountPrefix(): string { const siteUrl = process.env.CONVEX_SITE_URL; if (!siteUrl) return ""; try { @@ -148,7 +163,7 @@ const serveStaticFile = httpAction(async (ctx, request) => { let asset = await ctx.runQuery(internal.lib.getByPath, { path }); - if (!asset && !hasFileExtension(path)) { + if (!asset && isSpaFallbackEnabled() && !hasFileExtension(path)) { asset = await ctx.runQuery(internal.lib.getByPath, { path: "/index.html" }); } @@ -171,9 +186,7 @@ const serveStaticFile = httpAction(async (ctx, request) => { // lives at the deployment root (not under the component's prefix). if (asset.blobId && !isHtmlContentType(contentType)) { const redirectUrl = `${url.origin}/fs/blobs/${asset.blobId}`; - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; + const cacheControl = cacheControlFor(path); return new Response(null, { status: 302, headers: { Location: redirectUrl, "Cache-Control": cacheControl }, @@ -189,9 +202,7 @@ const serveStaticFile = httpAction(async (ctx, request) => { const etag = `"${asset.storageId}"`; const ifNoneMatch = request.headers.get("If-None-Match"); - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; + const cacheControl = cacheControlFor(path); if (ifNoneMatch === etag) { return new Response(null, { From a258771039aa62c7701bbd60673cfe3523afb465 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:06:33 -0700 Subject: [PATCH 15/29] add http tests --- src/component/http.test.ts | 228 +++++++++++++++++++++++++++++++++++++ src/component/http.ts | 2 +- 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/component/http.test.ts diff --git a/src/component/http.test.ts b/src/component/http.test.ts new file mode 100644 index 0000000..33ad46f --- /dev/null +++ b/src/component/http.test.ts @@ -0,0 +1,228 @@ +/// + +import { afterEach, describe, expect, test, vi } from "vitest"; +import { internal } from "./_generated/api.js"; +import { initConvexTest } from "./setup.test.js"; +import { + cacheControlFor, + getMimeType, + getMountPrefix, + hasFileExtension, + isHashedAsset, + isSpaFallbackEnabled, +} from "./http.js"; + +async function storeAsset( + t: ReturnType, + path: string, + body: string, + contentType: string, + deploymentId = "deploy-1", +) { + const storageId = await t.run(async (ctx) => { + return await ctx.storage.store(new Blob([body], { type: contentType })); + }); + await t.mutation(internal.lib.recordAsset, { + path, + storageId, + contentType, + deploymentId, + }); + return storageId; +} + +describe("static file serving", () => { + test("serves index.html at the root", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/index.html", + "hi", + "text/html; charset=utf-8", + ); + + const res = await t.fetch("/", {}); + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toBe("text/html; charset=utf-8"); + expect(await res.text()).toContain("hi"); + }); + + test("serves an exact asset with immutable caching for hashed files", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/assets/index-B71cUw87.js", + "console.log(1)", + "application/javascript; charset=utf-8", + ); + + const res = await t.fetch("/assets/index-B71cUw87.js", {}); + expect(res.status).toBe(200); + expect(res.headers.get("Cache-Control")).toBe( + "public, max-age=31536000, immutable", + ); + expect(res.headers.get("ETag")).toBeTruthy(); + expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff"); + }); + + test("HTML is revalidated rather than cached immutably", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/index.html", + "", + "text/html; charset=utf-8", + ); + + const res = await t.fetch("/", {}); + expect(res.headers.get("Cache-Control")).toBe( + "public, max-age=0, must-revalidate", + ); + }); + + test("returns 304 when If-None-Match matches the ETag", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/assets/app-B71cUw87.js", + "x", + "application/javascript; charset=utf-8", + ); + + const first = await t.fetch("/assets/app-B71cUw87.js", {}); + const etag = first.headers.get("ETag")!; + expect(etag).toBeTruthy(); + + const second = await t.fetch("/assets/app-B71cUw87.js", { + headers: { "If-None-Match": etag }, + }); + expect(second.status).toBe(304); + }); + + test("SPA fallback serves index.html for extension-less misses", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/index.html", + "
", + "text/html; charset=utf-8", + ); + + const res = await t.fetch("/dashboard/settings", {}); + expect(res.status).toBe(200); + expect(await res.text()).toContain("id=root"); + }); + + test("missing files with an extension 404 (no SPA fallback)", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/index.html", + "", + "text/html; charset=utf-8", + ); + + const res = await t.fetch("/missing.js", {}); + expect(res.status).toBe(404); + }); + + test("shows the setup page when nothing is deployed", async () => { + const t = initConvexTest(); + const res = await t.fetch("/", {}); + expect(res.status).toBe(200); + expect(await res.text()).toContain("no static files have been deployed"); + }); + + describe("with SPA fallback disabled", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test("extension-less misses 404 instead of serving index.html", async () => { + vi.stubEnv("STATIC_HOSTING_SPA_FALLBACK", "disabled"); + const t = initConvexTest(); + await storeAsset( + t, + "/index.html", + "", + "text/html; charset=utf-8", + ); + + const res = await t.fetch("/dashboard", {}); + expect(res.status).toBe(404); + }); + }); +}); + +describe("serving helpers", () => { + test("getMimeType maps known extensions and defaults to octet-stream", () => { + expect(getMimeType("/index.html")).toBe("text/html; charset=utf-8"); + expect(getMimeType("/a/b/style.css")).toBe("text/css; charset=utf-8"); + expect(getMimeType("/favicon.ico")).toBe("image/x-icon"); + expect(getMimeType("/data.unknownext")).toBe("application/octet-stream"); + }); + + test("hasFileExtension distinguishes routes from files", () => { + expect(hasFileExtension("/index.html")).toBe(true); + expect(hasFileExtension("/assets/app.js")).toBe(true); + expect(hasFileExtension("/dashboard")).toBe(false); + expect(hasFileExtension("/dashboard/settings")).toBe(false); + // dotfiles aren't treated as having an extension + expect(hasFileExtension("/.well-known")).toBe(false); + }); + + test("isHashedAsset detects bundler content hashes", () => { + expect(isHashedAsset("/assets/index-lj_vq_aF.js")).toBe(true); + expect(isHashedAsset("/assets/style-B71cUw87.css")).toBe(true); + expect(isHashedAsset("/index.html")).toBe(false); + expect(isHashedAsset("/logo.svg")).toBe(false); + }); + + test("cacheControlFor is immutable only for hashed assets", () => { + expect(cacheControlFor("/assets/index-B71cUw87.js")).toBe( + "public, max-age=31536000, immutable", + ); + expect(cacheControlFor("/index.html")).toBe( + "public, max-age=0, must-revalidate", + ); + }); + + describe("getMountPrefix", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test("is empty when mounted at the root", () => { + vi.stubEnv("CONVEX_SITE_URL", "https://x.convex.site"); + expect(getMountPrefix()).toBe(""); + vi.stubEnv("CONVEX_SITE_URL", "https://x.convex.site/"); + expect(getMountPrefix()).toBe(""); + }); + + test("strips a trailing slash from a sub-path mount", () => { + vi.stubEnv("CONVEX_SITE_URL", "https://x.convex.site/app"); + expect(getMountPrefix()).toBe("/app"); + vi.stubEnv("CONVEX_SITE_URL", "https://x.convex.site/app/"); + expect(getMountPrefix()).toBe("/app"); + }); + }); + + describe("isSpaFallbackEnabled", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test("defaults to enabled when unset", () => { + expect(isSpaFallbackEnabled()).toBe(true); + }); + + test('is off only for "disabled"', () => { + vi.stubEnv("STATIC_HOSTING_SPA_FALLBACK", "disabled"); + expect(isSpaFallbackEnabled()).toBe(false); + for (const value of ["enabled", undefined]) { + vi.stubEnv("STATIC_HOSTING_SPA_FALLBACK", value); + expect(isSpaFallbackEnabled()).toBe(true); + } + }); + }); +}); diff --git a/src/component/http.ts b/src/component/http.ts index 9a5b788..2d00346 100644 --- a/src/component/http.ts +++ b/src/component/http.ts @@ -49,7 +49,7 @@ function isHtmlContentType(contentType: string): boolean { // apps where unknown paths should be misses. // app.use(staticHosting, { env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" } }) export function isSpaFallbackEnabled(): boolean { - return env.STATIC_HOSTING_SPA_FALLBACK?.trim().toLowerCase() !== "disabled"; + return env.STATIC_HOSTING_SPA_FALLBACK !== "disabled"; } export function cacheControlFor(path: string): string { From 49d516ce8331c0c165f5fcdf9047f0ffa9b8f61a Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:18:59 -0700 Subject: [PATCH 16/29] add migration instructions --- INTEGRATION.md | 41 +++++++++++++++++++++++++++++++++++++++++ README.md | 10 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/INTEGRATION.md b/INTEGRATION.md index eb37075..2e1c0b3 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -300,6 +300,47 @@ app.use(staticHosting, { The env var accepts `"enabled"` (default) or `"disabled"`; unset keeps the fallback on. (Component env vars require `convex` ≥ 1.39.) +## Upgrading from 0.1.x + +0.2.0 is a **breaking change**: HTTP serving and file storage moved into the +component itself, so the app no longer registers routes or owns the files. + +1. **Update `convex/convex.config.ts`** to the form shown in + [Manual Setup](#convexconfigts) — the component is now named `staticHosting` + (was `selfHosting`) and is mounted with `app.use(staticHosting, { + httpPrefix: "/" })`. +2. **Delete `convex/http.ts`** (or remove the `registerStaticRoutes` call) — the + component serves its own routes now. +3. **Trim `convex/staticHosting.ts`** down to just `exposeDeploymentQuery` if + you use ``; remove the `exposeUploadApi` re-exports. If you + don't surface deployment updates you can delete the file entirely. +4. **Redeploy your assets** with `npx @convex-dev/static-hosting deploy`. Assets + uploaded under 0.1.x lived in the *app's* storage and won't resolve against + the component's storage. + +Removed from the client API: `registerStaticRoutes` and `exposeUploadApi`. +`exposeDeploymentQuery` and `getConvexUrl` remain. + +### Side-by-side migration (no downtime) + +Because components are isolated by mount name, you can run the old and new +versions simultaneously and cut over when you're ready: + +1. Keep the existing 0.1.x install serving traffic at the root. +2. Install 0.2.x under a second mount, temporarily at a sub-path: + + ```ts + const app = defineApp({ httpPrefix: "/api" }); + app.use(staticHosting, { httpPrefix: "/next/", env: {} }); // new, staged here first + ``` + +3. Deploy assets to the new mount and verify it at `/next/`. +4. Flip the prefixes — move the new mount to `/` and retire the old one — then + remove the 0.1.x install and its `convex/http.ts`. + +(If you also need both package versions installed at once, alias one in +`package.json`, e.g. `"static-hosting-legacy": "npm:@convex-dev/static-hosting@0.1.3"`.) + ## Troubleshooting ### 404s on every path diff --git a/README.md b/README.md index 6bd7716..1b38c85 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,16 @@ app.use(staticHosting, { Requests for paths with an extension (e.g. `/missing.js`) always 404 when not found, regardless of this setting. +## Upgrading from 0.1.x + +0.2.0 moved HTTP serving and file storage into the component itself, which is a +**breaking change** — delete `convex/http.ts` and the upload-API re-exports, +re-register the component as shown above, and **redeploy your assets** (assets +uploaded under 0.1.x lived in your app's storage and won't resolve). See the +[CHANGELOG](./CHANGELOG.md) and the [upgrade guide in +INTEGRATION.md](./INTEGRATION.md#upgrading-from-01x) for the full steps, +including a side-by-side migration that avoids downtime. + ## How it works 1. **Build** — your bundler emits `dist/`. From fb7de46c760046e9a45cc2474c28f913d58858f6 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:26:54 -0700 Subject: [PATCH 17/29] push spa lookup into query --- CHANGELOG.md | 10 ++-- INTEGRATION.md | 25 +++++---- README.md | 20 +++---- example/convex/convex.config.ts | 2 +- src/cli/deploy.ts | 12 +++++ src/cli/setup.ts | 2 +- src/cli/upload.ts | 11 +++- src/component/_generated/api.ts | 2 + src/component/_generated/component.ts | 1 + src/component/_generated/server.ts | 8 --- src/component/convex.config.ts | 10 +--- src/component/http.test.ts | 54 ++++++------------- src/component/http.ts | 75 ++++----------------------- src/component/lib.ts | 38 +++++++++++++- src/component/schema.ts | 4 ++ 15 files changed, 122 insertions(+), 152 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1884f2..1c08ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,10 @@ upgrading.** - Recommended setup now prefixes your own HTTP routes with `defineApp({ httpPrefix: "/api" })` so the static site can own the root without the catch-all route shadowing them. -- SPA fallback is now configurable via a env var. The component declares a - `STATIC_HOSTING_SPA_FALLBACK` env var (`"enabled"` default / `"disabled"`); - bind it per-mount via `app.use(staticHosting, { env: { ... } })` to make - extension-less misses return 404 instead of `index.html`. Because the - component declares an env var, `app.use` now requires an `env` object (pass - `env: {}` for defaults). Requires `convex` ≥ 1.39. +- SPA fallback is now configurable per deployment. Deploy with `--no-spa` + (`deploy` or `upload`) to make extension-less misses return 404 instead of + `index.html`. The setting is stored on the deployment record, so it travels + with the code you ship. ## 0.1.4 diff --git a/INTEGRATION.md b/INTEGRATION.md index 2e1c0b3..28255f7 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -46,9 +46,7 @@ import staticHosting from "@convex-dev/static-hosting/convex.config"; // Serve your own HTTP endpoints (convex/http.ts) under /api so the static // site can own the root. const app = defineApp({ httpPrefix: "/api" }); -// `env` is required because the component declares (optional) env vars; leave -// it `{}` for defaults. See "SPA routing" below to override. -app.use(staticHosting, { httpPrefix: "/", env: {} }); +app.use(staticHosting, { httpPrefix: "/" }); export default app; ``` @@ -172,7 +170,7 @@ import staticHosting from "@convex-dev/static-hosting/convex.config"; import fs from "convex-fs/convex.config"; const app = defineApp(); -app.use(staticHosting, { httpPrefix: "/", env: {} }); +app.use(staticHosting, { httpPrefix: "/" }); app.use(fs); export default app; @@ -246,6 +244,7 @@ npx @convex-dev/static-hosting deploy [options] -c, --component Component instance name (default: staticHosting) --skip-build Skip the build step --skip-convex Skip Convex backend deployment + --no-spa Disable SPA fallback (extension-less misses 404) --cdn Upload non-HTML assets to convex-fs CDN npx @convex-dev/static-hosting upload [options] @@ -253,6 +252,7 @@ npx @convex-dev/static-hosting upload [options] -c, --component Component instance name (default: staticHosting) --prod Deploy to production deployment -b, --build Run 'npm run build' with VITE_CONVEX_URL set + --no-spa Disable SPA fallback (extension-less misses 404) --cdn Upload non-HTML assets to convex-fs CDN --cdn-delete-function App function path that deletes CDN blobs -j, --concurrency Parallel upload workers (default: 5) @@ -287,18 +287,17 @@ equivalents: `publicPath` and `assetPrefix`. Requests for an extension-less path that doesn't match an uploaded file fall back to `index.html`, so client-side routes survive a reload. Paths with an extension (e.g. `/missing.js`) always 404 when not found. To turn the fallback -off for a multi-page app (unknown paths become real 404s), bind the component's -`STATIC_HOSTING_SPA_FALLBACK` env var where you mount it: +off for a multi-page app (unknown paths become real 404s), deploy with +`--no-spa`: -```typescript -app.use(staticHosting, { - httpPrefix: "/", - env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" }, -}); +```bash +npx @convex-dev/static-hosting deploy --no-spa +# or: npx @convex-dev/static-hosting upload --no-spa ``` -The env var accepts `"enabled"` (default) or `"disabled"`; unset keeps the -fallback on. (Component env vars require `convex` ≥ 1.39.) +The flag is stored on the deployment record (the `deploymentInfo` table), so +the serving behavior travels with the code you ship rather than living in a +separate env var. Re-deploy without `--no-spa` or with `--spa` to turn it back on. ## Upgrading from 0.1.x diff --git a/README.md b/README.md index 1b38c85..e3a9778 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ import staticHosting from "@convex-dev/static-hosting/convex.config.js"; // Your own HTTP endpoints (convex/http.ts) are served under /api so the // static site can own the root. const app = defineApp({ httpPrefix: "/api" }); -app.use(staticHosting, { httpPrefix: "/", env: {} }); +app.use(staticHosting, { httpPrefix: "/" }); export default app; ``` @@ -161,6 +161,7 @@ npx @convex-dev/static-hosting deploy [options] -c, --component Component instance name (default: staticHosting) --skip-build Skip the build step (use existing dist) --skip-convex Skip Convex backend deployment + --no-spa Disable SPA fallback (extension-less misses 404) --cdn Upload non-HTML assets to convex-fs CDN npx @convex-dev/static-hosting upload [options] @@ -168,6 +169,7 @@ npx @convex-dev/static-hosting upload [options] -c, --component Component instance name (default: staticHosting) --prod Deploy to production deployment -b, --build Run 'npm run build' with VITE_CONVEX_URL set + --no-spa Disable SPA fallback (extension-less misses 404) --cdn Upload non-HTML assets to convex-fs CDN --cdn-delete-function App function path that deletes CDN blobs (opt-in) -j, --concurrency Parallel upload workers (default: 5) @@ -273,18 +275,16 @@ Root-mounted apps don't need this — the default is `/`. For webpack use By default, requests for a path with no file extension that doesn't match an uploaded file fall back to `index.html`, so client-side routes like `/dashboard/settings` work on reload. For a multi-page app where unknown paths -should be a real 404, disable the fallback by binding the component's -`STATIC_HOSTING_SPA_FALLBACK` env var when you mount it: +should be a real 404, deploy with `--no-spa`: -```ts -app.use(staticHosting, { - httpPrefix: "/", - env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" }, -}); +```bash +npx @convex-dev/static-hosting deploy --no-spa ``` -Requests for paths with an extension (e.g. `/missing.js`) always 404 when not -found, regardless of this setting. +The setting is stored with the deployment, so it travels with the code you +ship rather than living in a separate env var. Requests for paths with an +extension (e.g. `/missing.js`) always 404 when not found, regardless of this +setting. ## Upgrading from 0.1.x diff --git a/example/convex/convex.config.ts b/example/convex/convex.config.ts index 06b14b7..cb02407 100644 --- a/example/convex/convex.config.ts +++ b/example/convex/convex.config.ts @@ -3,6 +3,6 @@ import staticHosting from "@convex-dev/static-hosting/convex.config.js"; const app = defineApp({ httpPrefix: "/api" }); -app.use(staticHosting, { httpPrefix: "/", env: {} }); +app.use(staticHosting, { httpPrefix: "/" }); export default app; diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index 4327c21..8461b6d 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -29,6 +29,7 @@ interface ParsedArgs { skipBuild: boolean; skipConvex: boolean; cdn: boolean; + spaFallback: boolean; buildCommand: string; } @@ -40,6 +41,7 @@ function parseArgs(args: string[]): ParsedArgs { skipBuild: false, skipConvex: false, cdn: false, + spaFallback: true, buildCommand: "npm run build", }; @@ -57,6 +59,10 @@ function parseArgs(args: string[]): ParsedArgs { result.skipConvex = true; } else if (arg === "--cdn") { result.cdn = true; + } else if (arg === "--no-spa") { + result.spaFallback = false; + } else if (arg === "--spa") { + result.spaFallback = true; } else if (arg === "--build-command") { const cmd = args[++i]; if (cmd) result.buildCommand = cmd; @@ -81,6 +87,7 @@ Options: --skip-build Skip the build step (use existing dist) --skip-convex Skip Convex backend deployment --build-command Build command to run (default: 'npm run build') + --no-spa Disable SPA fallback (extension-less misses 404) --cdn Upload non-HTML assets to convex-fs CDN -h, --help Show this help message @@ -155,6 +162,7 @@ async function uploadToConvexStorage( distDir: string, componentName: string, useCdn: boolean, + spaFallback: boolean, ): Promise { console.log(""); console.log( @@ -176,6 +184,9 @@ async function uploadToConvexStorage( if (useCdn) { uploadArgs.push("--cdn"); } + if (!spaFallback) { + uploadArgs.push("--no-spa"); + } const result = spawnStaticHostingCli(uploadArgs); @@ -308,6 +319,7 @@ async function main(): Promise { distDir, args.component, args.cdn, + args.spaFallback, ); if (!staticDeploySuccess) { diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 7cc0da9..cf26452 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -45,7 +45,7 @@ import staticHosting from "@convex-dev/static-hosting/convex.config"; // Your own HTTP endpoints (convex/http.ts) are served under /api so the // static site can own the root. const app = defineApp({ httpPrefix: "/api" }); -app.use(staticHosting, { httpPrefix: "/", env: {} }); +app.use(staticHosting, { httpPrefix: "/" }); export default app; `, diff --git a/src/cli/upload.ts b/src/cli/upload.ts index 546ea7d..7ab7b4d 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -53,6 +53,7 @@ interface ParsedArgs { cdn: boolean; cdnDeleteFunction: string; concurrency: number; + spaFallback: boolean; help: boolean; } @@ -66,6 +67,7 @@ function parseArgs(args: string[]): ParsedArgs { cdn: false, cdnDeleteFunction: "", concurrency: 5, + spaFallback: true, help: false, }; @@ -89,6 +91,10 @@ function parseArgs(args: string[]): ParsedArgs { result.buildCommand = cmd; result.build = true; } + } else if (arg === "--no-spa") { + result.spaFallback = false; + } else if (arg === "--spa") { + result.spaFallback = true; } else if (arg === "--cdn") { result.cdn = true; } else if (arg === "--cdn-delete-function") { @@ -116,6 +122,8 @@ Options: STATIC_HOSTING_BASE_PATH set before uploading --build-command Build command to run (default: 'npm run build'). Implies --build. + --no-spa Disable SPA fallback for this deployment (extension-less + misses return 404 instead of index.html) --cdn Upload non-HTML assets to convex-fs CDN instead of Convex storage --cdn-delete-function App function to delete CDN blobs (e.g. staticHosting:deleteCdnBlobs) -j, --concurrency Number of parallel uploads (default: 5) @@ -441,9 +449,10 @@ async function main(): Promise { console.log(""); - // Garbage collect old files + // Garbage collect old files and record this deployment's SPA config. const gcOutput = await convexRunAsync(componentName, "lib:gcOldAssets", { currentDeploymentId: deploymentId, + spaFallback: args.spaFallback, }); const gcResult = JSON.parse(gcOutput); const deletedCount: number = gcResult.deleted; diff --git a/src/component/_generated/api.ts b/src/component/_generated/api.ts index d541c1b..dbdc532 100644 --- a/src/component/_generated/api.ts +++ b/src/component/_generated/api.ts @@ -10,6 +10,7 @@ import type * as http from "../http.js"; import type * as lib from "../lib.js"; +import type * as serving from "../serving.js"; import type { ApiFromModules, @@ -21,6 +22,7 @@ import { anyApi, componentsGeneric } from "convex/server"; const fullApi: ApiFromModules<{ http: typeof http; lib: typeof lib; + serving: typeof serving; }> = anyApi as any; /** diff --git a/src/component/_generated/component.ts b/src/component/_generated/component.ts index d8d2852..b7e9492 100644 --- a/src/component/_generated/component.ts +++ b/src/component/_generated/component.ts @@ -33,6 +33,7 @@ export type ComponentApi = _id: string; currentDeploymentId: string; deployedAt: number; + spaFallback?: boolean; } | null, Name >; diff --git a/src/component/_generated/server.ts b/src/component/_generated/server.ts index 04a9e96..739b02f 100644 --- a/src/component/_generated/server.ts +++ b/src/component/_generated/server.ts @@ -30,13 +30,6 @@ import { } from "convex/server"; import type { DataModel } from "./dataModel.js"; -/** - * Typesafe environment variables declared in `convex.config.ts`. - */ -type Env = { - readonly STATIC_HOSTING_SPA_FALLBACK: string | undefined; -}; - /** * Define a query in this Convex app's public API. * @@ -113,7 +106,6 @@ export const internalAction: ActionBuilder = * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export const httpAction: HttpActionBuilder = httpActionGeneric; -export const env: Env = process.env as unknown as Env; /** * A set of services for use within Convex query functions. diff --git a/src/component/convex.config.ts b/src/component/convex.config.ts index 76944b1..dc76c53 100644 --- a/src/component/convex.config.ts +++ b/src/component/convex.config.ts @@ -1,11 +1,3 @@ import { defineComponent } from "convex/server"; -import { v } from "convex/values"; -export default defineComponent("staticHosting", { - env: { - // Control the SPA fallback behavior: "disabled" returns 404s instead of serving index.html. - STATIC_HOSTING_SPA_FALLBACK: v.optional( - v.union(v.literal("enabled"), v.literal("disabled")), - ), - }, -}); +export default defineComponent("staticHosting"); diff --git a/src/component/http.test.ts b/src/component/http.test.ts index 33ad46f..e38ab0c 100644 --- a/src/component/http.test.ts +++ b/src/component/http.test.ts @@ -3,14 +3,13 @@ import { afterEach, describe, expect, test, vi } from "vitest"; import { internal } from "./_generated/api.js"; import { initConvexTest } from "./setup.test.js"; +import { getMountPrefix } from "./http.js"; import { cacheControlFor, getMimeType, - getMountPrefix, hasFileExtension, isHashedAsset, - isSpaFallbackEnabled, -} from "./http.js"; +} from "./serving.js"; async function storeAsset( t: ReturnType, @@ -133,24 +132,22 @@ describe("static file serving", () => { expect(await res.text()).toContain("no static files have been deployed"); }); - describe("with SPA fallback disabled", () => { - afterEach(() => { - vi.unstubAllEnvs(); + test("extension-less misses 404 when SPA fallback is disabled", async () => { + const t = initConvexTest(); + await storeAsset( + t, + "/index.html", + "", + "text/html; charset=utf-8", + ); + // Record the deployment with SPA fallback off (what `upload --no-spa` does). + await t.mutation(internal.lib.gcOldAssets, { + currentDeploymentId: "deploy-1", + spaFallback: false, }); - test("extension-less misses 404 instead of serving index.html", async () => { - vi.stubEnv("STATIC_HOSTING_SPA_FALLBACK", "disabled"); - const t = initConvexTest(); - await storeAsset( - t, - "/index.html", - "", - "text/html; charset=utf-8", - ); - - const res = await t.fetch("/dashboard", {}); - expect(res.status).toBe(404); - }); + const res = await t.fetch("/dashboard", {}); + expect(res.status).toBe(404); }); }); @@ -206,23 +203,4 @@ describe("serving helpers", () => { expect(getMountPrefix()).toBe("/app"); }); }); - - describe("isSpaFallbackEnabled", () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - - test("defaults to enabled when unset", () => { - expect(isSpaFallbackEnabled()).toBe(true); - }); - - test('is off only for "disabled"', () => { - vi.stubEnv("STATIC_HOSTING_SPA_FALLBACK", "disabled"); - expect(isSpaFallbackEnabled()).toBe(false); - for (const value of ["enabled", undefined]) { - vi.stubEnv("STATIC_HOSTING_SPA_FALLBACK", value); - expect(isSpaFallbackEnabled()).toBe(true); - } - }); - }); }); diff --git a/src/component/http.ts b/src/component/http.ts index 2d00346..4f5d72c 100644 --- a/src/component/http.ts +++ b/src/component/http.ts @@ -1,62 +1,7 @@ import { httpRouter } from "convex/server"; -import { env, httpAction } from "./_generated/server.js"; +import { httpAction } from "./_generated/server.js"; import { internal } from "./_generated/api.js"; - -const MIME_TYPES: Record = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", - ".webmanifest": "application/manifest+json", - ".xml": "application/xml", -}; - -export function getMimeType(path: string): string { - const ext = path.substring(path.lastIndexOf(".")).toLowerCase(); - return MIME_TYPES[ext] || "application/octet-stream"; -} - -export function hasFileExtension(path: string): boolean { - const lastSegment = path.split("/").pop() || ""; - return lastSegment.includes(".") && !lastSegment.startsWith("."); -} - -// Vite hashed asset suffix: e.g. `index-lj_vq_aF.js`, `style-B71cUw87.css` -export function isHashedAsset(path: string): boolean { - return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); -} - -function isHtmlContentType(contentType: string): boolean { - return contentType.startsWith("text/html"); -} - -// SPA fallback (serving /index.html for extension-less paths that don't match -// a file) is on by default. Bind the STATIC_HOSTING_SPA_FALLBACK env var to -// "disabled" to turn it off and return 404s instead — useful for multi-page -// apps where unknown paths should be misses. -// app.use(staticHosting, { env: { STATIC_HOSTING_SPA_FALLBACK: "disabled" } }) -export function isSpaFallbackEnabled(): boolean { - return env.STATIC_HOSTING_SPA_FALLBACK !== "disabled"; -} - -export function cacheControlFor(path: string): string { - return isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; -} +import { cacheControlFor, getMimeType, isHtmlContentType } from "./serving.js"; function getSetupHtml(): string { return ` @@ -161,11 +106,11 @@ const serveStaticFile = httpAction(async (ctx, request) => { path = "/index.html"; } - let asset = await ctx.runQuery(internal.lib.getByPath, { path }); - - if (!asset && isSpaFallbackEnabled() && !hasFileExtension(path)) { - asset = await ctx.runQuery(internal.lib.getByPath, { path: "/index.html" }); - } + // One query resolves the asset: an exact match, or — when SPA fallback is + // enabled for the current deployment — the index.html asset for an + // extension-less miss. The fallback lookup happens inside the query so the + // HTTP action never makes a second round-trip. + const asset = await ctx.runQuery(internal.lib.resolveAsset, { path }); if (!asset) { if (path === "/index.html") { @@ -186,10 +131,12 @@ const serveStaticFile = httpAction(async (ctx, request) => { // lives at the deployment root (not under the component's prefix). if (asset.blobId && !isHtmlContentType(contentType)) { const redirectUrl = `${url.origin}/fs/blobs/${asset.blobId}`; - const cacheControl = cacheControlFor(path); return new Response(null, { status: 302, - headers: { Location: redirectUrl, "Cache-Control": cacheControl }, + headers: { + Location: redirectUrl, + "Cache-Control": cacheControlFor(path), + }, }); } diff --git a/src/component/lib.ts b/src/component/lib.ts index 99cc089..2bc5fd7 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -4,6 +4,7 @@ import { internalQuery, query, } from "./_generated/server.js"; +import { hasFileExtension } from "./serving.js"; const staticAssetValidator = v.object({ _id: v.id("staticAssets"), @@ -20,6 +21,7 @@ const deploymentInfoValidator = v.object({ _creationTime: v.number(), currentDeploymentId: v.string(), deployedAt: v.number(), + spaFallback: v.optional(v.boolean()), }); export const getCurrentDeployment = query({ @@ -59,6 +61,33 @@ export const getByPath = internalQuery({ }, }); +// Resolves the asset the HTTP handler should serve for a request path: the +// exact match, or — when SPA fallback is enabled for the current deployment +// and the path looks like a client-side route (no file extension) — the +// index.html asset. Doing the fallback here keeps it to a single query. +export const resolveAsset = internalQuery({ + args: { path: v.string() }, + returns: v.union(staticAssetValidator, v.null()), + handler: async (ctx, { path }) => { + const exact = await ctx.db + .query("staticAssets") + .withIndex("by_path", (q) => q.eq("path", path)) + .unique(); + if (exact) return exact; + + if (hasFileExtension(path)) return null; + + const info = await ctx.db.query("deploymentInfo").first(); + const spaFallback = info?.spaFallback ?? true; + if (!spaFallback) return null; + + return await ctx.db + .query("staticAssets") + .withIndex("by_path", (q) => q.eq("path", "/index.html")) + .unique(); + }, +}); + export const listAssets = internalQuery({ args: { limit: v.optional(v.number()) }, returns: v.array(staticAssetValidator), @@ -151,7 +180,11 @@ export const recordAssets = internalMutation({ }); export const gcOldAssets = internalMutation({ - args: { currentDeploymentId: v.string() }, + args: { + currentDeploymentId: v.string(), + // Whether to serve SPA fallback for this deployment (default true). + spaFallback: v.optional(v.boolean()), + }, returns: v.object({ deleted: v.number(), blobIds: v.array(v.string()), @@ -172,16 +205,19 @@ export const gcOldAssets = internalMutation({ await ctx.db.delete("staticAssets", asset._id); } + const spaFallback = args.spaFallback ?? true; const existing = await ctx.db.query("deploymentInfo").first(); if (existing) { await ctx.db.patch("deploymentInfo", existing._id, { currentDeploymentId: args.currentDeploymentId, deployedAt: Date.now(), + spaFallback, }); } else { await ctx.db.insert("deploymentInfo", { currentDeploymentId: args.currentDeploymentId, deployedAt: Date.now(), + spaFallback, }); } return { deleted, blobIds }; diff --git a/src/component/schema.ts b/src/component/schema.ts index 7a5175b..ef78e52 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -17,5 +17,9 @@ export default defineSchema({ deploymentInfo: defineTable({ currentDeploymentId: v.string(), deployedAt: v.number(), // timestamp + // SPA fallback config for the current deployment. Set at upload time + // (`--no-spa` turns it off). Absent means enabled. Travels with the + // deploy so the serving behavior matches the code that was shipped. + spaFallback: v.optional(v.boolean()), }), }); From 48cc76ccc37c70638a30117a0a6fea78922dccc6 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:39:23 -0700 Subject: [PATCH 18/29] rename to commit deployment --- src/cli/upload.ts | 12 ++++--- src/component/http.test.ts | 2 +- src/component/lib.test.ts | 68 +++++++++++++++++++++++++++++++++++--- src/component/lib.ts | 6 +++- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/cli/upload.ts b/src/cli/upload.ts index 7ab7b4d..3215eb4 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -449,11 +449,13 @@ async function main(): Promise { console.log(""); - // Garbage collect old files and record this deployment's SPA config. - const gcOutput = await convexRunAsync(componentName, "lib:gcOldAssets", { - currentDeploymentId: deploymentId, - spaFallback: args.spaFallback, - }); + // Commit the deployment: record it as current (with SPA config) and GC + // assets from previous deployments. + const gcOutput = await convexRunAsync( + componentName, + "lib:commitDeployment", + { currentDeploymentId: deploymentId, spaFallback: args.spaFallback }, + ); const gcResult = JSON.parse(gcOutput); const deletedCount: number = gcResult.deleted; const oldBlobIds: string[] = gcResult.blobIds ?? []; diff --git a/src/component/http.test.ts b/src/component/http.test.ts index e38ab0c..96341e7 100644 --- a/src/component/http.test.ts +++ b/src/component/http.test.ts @@ -141,7 +141,7 @@ describe("static file serving", () => { "text/html; charset=utf-8", ); // Record the deployment with SPA fallback off (what `upload --no-spa` does). - await t.mutation(internal.lib.gcOldAssets, { + await t.mutation(internal.lib.commitDeployment, { currentDeploymentId: "deploy-1", spaFallback: false, }); diff --git a/src/component/lib.test.ts b/src/component/lib.test.ts index bf86731..bf2a7e4 100644 --- a/src/component/lib.test.ts +++ b/src/component/lib.test.ts @@ -37,9 +37,9 @@ describe("component lib", () => { expect(assets).toHaveLength(0); }); - test("gcOldAssets on empty db returns zero", async () => { + test("commitDeployment on empty db returns zero", async () => { const t = initConvexTest(); - const result = await t.mutation(internal.lib.gcOldAssets, { + const result = await t.mutation(internal.lib.commitDeployment, { currentDeploymentId: "deploy-1", }); expect(result.deleted).toBe(0); @@ -70,7 +70,7 @@ describe("component lib", () => { expect(second?.blobId).toBe("blob-456"); }); - test("gcOldAssets returns blobIds for old CDN assets and bumps deployment", async () => { + test("commitDeployment returns blobIds for old CDN assets and bumps deployment", async () => { const t = initConvexTest(); await t.mutation(internal.lib.recordAsset, { @@ -80,7 +80,7 @@ describe("component lib", () => { deploymentId: "deploy-old", }); - const result = await t.mutation(internal.lib.gcOldAssets, { + const result = await t.mutation(internal.lib.commitDeployment, { currentDeploymentId: "deploy-new", }); expect(result.deleted).toBe(0); @@ -113,4 +113,64 @@ describe("component lib", () => { const all = await t.query(internal.lib.listAssets, {}); expect(all).toHaveLength(2); }); + + describe("resolveAsset", () => { + async function seedIndex(t: ReturnType) { + await t.mutation(internal.lib.recordAsset, { + path: "/index.html", + blobId: "blob-index", + contentType: "text/html; charset=utf-8", + deploymentId: "deploy-1", + }); + } + + test("returns the exact match when present", async () => { + const t = initConvexTest(); + await t.mutation(internal.lib.recordAsset, { + path: "/assets/app-B71cUw87.js", + blobId: "blob-app", + contentType: "application/javascript; charset=utf-8", + deploymentId: "deploy-1", + }); + + const asset = await t.query(internal.lib.resolveAsset, { + path: "/assets/app-B71cUw87.js", + }); + expect(asset?.path).toBe("/assets/app-B71cUw87.js"); + }); + + test("falls back to index.html for extension-less misses by default", async () => { + const t = initConvexTest(); + await seedIndex(t); + + const asset = await t.query(internal.lib.resolveAsset, { + path: "/dashboard/settings", + }); + expect(asset?.path).toBe("/index.html"); + }); + + test("does not fall back for paths with an extension", async () => { + const t = initConvexTest(); + await seedIndex(t); + + const asset = await t.query(internal.lib.resolveAsset, { + path: "/missing.js", + }); + expect(asset).toBeNull(); + }); + + test("does not fall back when the deployment disables it", async () => { + const t = initConvexTest(); + await seedIndex(t); + await t.mutation(internal.lib.commitDeployment, { + currentDeploymentId: "deploy-1", + spaFallback: false, + }); + + const asset = await t.query(internal.lib.resolveAsset, { + path: "/dashboard", + }); + expect(asset).toBeNull(); + }); + }); }); diff --git a/src/component/lib.ts b/src/component/lib.ts index 2bc5fd7..f8caaa6 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -179,7 +179,11 @@ export const recordAssets = internalMutation({ }, }); -export const gcOldAssets = internalMutation({ +// Commits a finished upload as the current deployment: records the deployment +// id + SPA config, then garbage-collects assets left over from previous +// deployments. Returns the storage cleanup tally plus any CDN blobIds the +// caller should delete (component actions can't reach the /fs blobs endpoint). +export const commitDeployment = internalMutation({ args: { currentDeploymentId: v.string(), // Whether to serve SPA fallback for this deployment (default true). From 3294409c8735b42bb69ac4181d230757766e3747 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:43:19 -0700 Subject: [PATCH 19/29] improve migration instructions --- INTEGRATION.md | 53 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/INTEGRATION.md b/INTEGRATION.md index 28255f7..01d5565 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -322,23 +322,54 @@ Removed from the client API: `registerStaticRoutes` and `exposeUploadApi`. ### Side-by-side migration (no downtime) -Because components are isolated by mount name, you can run the old and new -versions simultaneously and cut over when you're ready: +You can run 0.1.x and 0.2.x at the same time and cut over when the new mount is +verified. The two don't collide: the 0.1.x component is named `selfHosting` and +serves via your app's `convex/http.ts`, while 0.2.x is named `staticHosting` and +serves itself. The catch is that both ship from the same package name, so +install the old one under an **npm alias**: -1. Keep the existing 0.1.x install serving traffic at the root. -2. Install 0.2.x under a second mount, temporarily at a sub-path: +1. Install the new version, and the old one aliased to a distinct slug: + + ```bash + npm install @convex-dev/static-hosting@^0.2.0 + npm install static-hosting-legacy@npm:@convex-dev/static-hosting@0.1.3 + ``` + +2. Mount both in `convex/convex.config.ts`, importing the legacy config from the + alias and staging the new mount at a sub-path: + + ```ts + import { defineApp } from "convex/server"; + import staticHostingLegacy from "static-hosting-legacy/convex.config.js"; + import staticHosting from "@convex-dev/static-hosting/convex.config.js"; + + const app = defineApp(); + app.use(staticHostingLegacy); // 0.1.x — keeps serving the live site + app.use(staticHosting, { httpPrefix: "/next/" }); // 0.2.x — staged here first + + export default app; + ``` + +3. Keep your existing `convex/http.ts` registering the **legacy** routes at the + root, importing `registerStaticRoutes` from the alias: ```ts - const app = defineApp({ httpPrefix: "/api" }); - app.use(staticHosting, { httpPrefix: "/next/", env: {} }); // new, staged here first + import { registerStaticRoutes } from "static-hosting-legacy"; + import { components } from "./_generated/api"; + + registerStaticRoutes(http, components.selfHosting); ``` -3. Deploy assets to the new mount and verify it at `/next/`. -4. Flip the prefixes — move the new mount to `/` and retire the old one — then - remove the 0.1.x install and its `convex/http.ts`. +4. Deploy assets to the new mount and verify them at `/next/`: + + ```bash + npx @convex-dev/static-hosting deploy # targets the staticHosting component + ``` -(If you also need both package versions installed at once, alias one in -`package.json`, e.g. `"static-hosting-legacy": "npm:@convex-dev/static-hosting@0.1.3"`.) +5. **Cut over:** delete the legacy `registerStaticRoutes` call (and + `convex/http.ts` if it's now empty), drop `app.use(staticHostingLegacy)`, + change the new mount to `httpPrefix: "/"`, and redeploy. Once traffic is + served by 0.2.x, remove the `static-hosting-legacy` dependency. ## Troubleshooting From 80377307254d4c706d27f4600fc334bc4cb604ee Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:43:45 -0700 Subject: [PATCH 20/29] fixup! a0206ad --- src/component/serving.ts | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/component/serving.ts diff --git a/src/component/serving.ts b/src/component/serving.ts new file mode 100644 index 0000000..15d905c --- /dev/null +++ b/src/component/serving.ts @@ -0,0 +1,50 @@ +// Pure, dependency-free helpers shared by the HTTP handler (http.ts) and the +// asset-resolution query (lib.ts). Kept separate so lib.ts can reuse them +// without importing the HTTP router. + +const MIME_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".txt": "text/plain; charset=utf-8", + ".map": "application/json", + ".webmanifest": "application/manifest+json", + ".xml": "application/xml", +}; + +export function getMimeType(path: string): string { + const ext = path.substring(path.lastIndexOf(".")).toLowerCase(); + return MIME_TYPES[ext] || "application/octet-stream"; +} + +export function hasFileExtension(path: string): boolean { + const lastSegment = path.split("/").pop() || ""; + return lastSegment.includes(".") && !lastSegment.startsWith("."); +} + +// Vite hashed asset suffix: e.g. `index-lj_vq_aF.js`, `style-B71cUw87.css` +export function isHashedAsset(path: string): boolean { + return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); +} + +export function isHtmlContentType(contentType: string): boolean { + return contentType.startsWith("text/html"); +} + +export function cacheControlFor(path: string): string { + return isHashedAsset(path) + ? "public, max-age=31536000, immutable" + : "public, max-age=0, must-revalidate"; +} From cc356f58436029e80451a21c17922be458ebe045 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:44:05 -0700 Subject: [PATCH 21/29] update peer dep back --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb39503..b2fbab8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "vitest": "4.0.17" }, "peerDependencies": { - "convex": "^1.39.1", + "convex": "^1.35.0", "react": "^18.3.1 || ^19.0.0" } }, diff --git a/package.json b/package.json index c9344fe..44fdb60 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ } }, "peerDependencies": { - "convex": "^1.39.1", + "convex": "^1.35.0", "react": "^18.3.1 || ^19.0.0" }, "devDependencies": { From 653444ca93215faecce57a8232816e9f9a1721b8 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:45:25 -0700 Subject: [PATCH 22/29] improve --no-spa help text --- INTEGRATION.md | 4 ++-- README.md | 4 ++-- src/cli/deploy.ts | 2 +- src/cli/upload.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/INTEGRATION.md b/INTEGRATION.md index 01d5565..b821b16 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -244,7 +244,7 @@ npx @convex-dev/static-hosting deploy [options] -c, --component Component instance name (default: staticHosting) --skip-build Skip the build step --skip-convex Skip Convex backend deployment - --no-spa Disable SPA fallback (extension-less misses 404) + --no-spa Disable SPA fallback (404 instead of /index.html) --cdn Upload non-HTML assets to convex-fs CDN npx @convex-dev/static-hosting upload [options] @@ -252,7 +252,7 @@ npx @convex-dev/static-hosting upload [options] -c, --component Component instance name (default: staticHosting) --prod Deploy to production deployment -b, --build Run 'npm run build' with VITE_CONVEX_URL set - --no-spa Disable SPA fallback (extension-less misses 404) + --no-spa Disable SPA fallback (404 instead of /index.html) --cdn Upload non-HTML assets to convex-fs CDN --cdn-delete-function App function path that deletes CDN blobs -j, --concurrency Parallel upload workers (default: 5) diff --git a/README.md b/README.md index e3a9778..aa23a7e 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ npx @convex-dev/static-hosting deploy [options] -c, --component Component instance name (default: staticHosting) --skip-build Skip the build step (use existing dist) --skip-convex Skip Convex backend deployment - --no-spa Disable SPA fallback (extension-less misses 404) + --no-spa Disable SPA fallback (404 instead of /index.html) --cdn Upload non-HTML assets to convex-fs CDN npx @convex-dev/static-hosting upload [options] @@ -169,7 +169,7 @@ npx @convex-dev/static-hosting upload [options] -c, --component Component instance name (default: staticHosting) --prod Deploy to production deployment -b, --build Run 'npm run build' with VITE_CONVEX_URL set - --no-spa Disable SPA fallback (extension-less misses 404) + --no-spa Disable SPA fallback (404 instead of /index.html) --cdn Upload non-HTML assets to convex-fs CDN --cdn-delete-function App function path that deletes CDN blobs (opt-in) -j, --concurrency Parallel upload workers (default: 5) diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index 8461b6d..bf0833a 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -87,7 +87,7 @@ Options: --skip-build Skip the build step (use existing dist) --skip-convex Skip Convex backend deployment --build-command Build command to run (default: 'npm run build') - --no-spa Disable SPA fallback (extension-less misses 404) + --no-spa Disable SPA fallback (404 instead of /index.html) --cdn Upload non-HTML assets to convex-fs CDN -h, --help Show this help message diff --git a/src/cli/upload.ts b/src/cli/upload.ts index 3215eb4..1b3e49f 100644 --- a/src/cli/upload.ts +++ b/src/cli/upload.ts @@ -122,8 +122,8 @@ Options: STATIC_HOSTING_BASE_PATH set before uploading --build-command Build command to run (default: 'npm run build'). Implies --build. - --no-spa Disable SPA fallback for this deployment (extension-less - misses return 404 instead of index.html) + --no-spa Disable SPA fallback for this deployment (return a + 404 instead of falling back to /index.html) --cdn Upload non-HTML assets to convex-fs CDN instead of Convex storage --cdn-delete-function App function to delete CDN blobs (e.g. staticHosting:deleteCdnBlobs) -j, --concurrency Number of parallel uploads (default: 5) From 897628fb413da5c0eaac2538edc19748530997ed Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:50:16 -0700 Subject: [PATCH 23/29] fix outdated docs --- CONVEX_STATIC_HOSTING.md | 480 ------------------------------- example/README.md | 17 +- example/scripts/upload-static.ts | 179 ------------ 3 files changed, 8 insertions(+), 668 deletions(-) delete mode 100644 CONVEX_STATIC_HOSTING.md delete mode 100644 example/scripts/upload-static.ts diff --git a/CONVEX_STATIC_HOSTING.md b/CONVEX_STATIC_HOSTING.md deleted file mode 100644 index 5181364..0000000 --- a/CONVEX_STATIC_HOSTING.md +++ /dev/null @@ -1,480 +0,0 @@ -# Convex Static Site Hosting - Complete Implementation Guide - -## Overview - -This implementation allows hosting static React/Vite apps using Convex HTTP actions and file storage, eliminating the need for Vercel or other hosting providers. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ BUILD PHASE │ -│ npm run build → Vite creates dist/ with index.html, JS, CSS, assets │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ UPLOAD PHASE │ -│ 1. Generate unique deploymentId (UUID) │ -│ 2. For each file in dist/: │ -│ a. Call generateUploadUrl mutation → get signed URL │ -│ b. POST file content to signed URL → get storageId │ -│ c. Call recordAsset mutation → store path→storageId mapping │ -│ 3. Call gcOldAssets mutation → delete files with old deploymentIds │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ SERVE PHASE │ -│ Browser requests https://deployment.convex.site/some/path │ -│ → HTTP action receives request │ -│ → Query staticAssets table for path │ -│ → If not found & SPA mode & no file extension → try /index.html │ -│ → Fetch blob from ctx.storage.get(storageId) │ -│ → Return Response with correct Content-Type and Cache-Control │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -## Database Schema - -### Table: `staticAssets` - -```typescript -import { defineTable } from "convex/server"; -import { v } from "convex/values"; - -defineTable({ - path: v.string(), // URL path, e.g., "/index.html", "/assets/main-abc123.js" - storageId: v.id("_storage"), // Reference to Convex file storage - contentType: v.string(), // MIME type, e.g., "text/html; charset=utf-8" - deploymentId: v.string(), // UUID for garbage collection -}) - .index("by_path", ["path"]) - .index("by_deploymentId", ["deploymentId"]) -``` - -## Convex Functions - -### 1. `getByPath` - Query to look up assets - -```typescript -import { query } from "./_generated/server"; -import { v } from "convex/values"; - -export const getByPath = query({ - args: { path: v.string() }, - returns: v.union( - v.object({ - _id: v.id("staticAssets"), - _creationTime: v.number(), // IMPORTANT: Include this or validation fails - path: v.string(), - storageId: v.id("_storage"), - contentType: v.string(), - deploymentId: v.string(), - }), - v.null() - ), - handler: async (ctx, args) => { - return await ctx.db - .query("staticAssets") - .withIndex("by_path", (q) => q.eq("path", args.path)) - .unique(); - }, -}); -``` - -### 2. `generateUploadUrl` - Get signed URL for upload - -```typescript -export const generateUploadUrl = mutation({ - args: {}, - returns: v.string(), - handler: async (ctx) => { - return await ctx.storage.generateUploadUrl(); - }, -}); -``` - -### 3. `recordAsset` - Store asset metadata (upsert) - -```typescript -export const recordAsset = mutation({ - args: { - path: v.string(), - storageId: v.id("_storage"), - contentType: v.string(), - deploymentId: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - // Check if asset already exists at this path - const existing = await ctx.db - .query("staticAssets") - .withIndex("by_path", (q) => q.eq("path", args.path)) - .unique(); - - if (existing) { - // Delete old storage file to avoid orphans - await ctx.storage.delete(existing.storageId); - // Delete old record - await ctx.db.delete(existing._id); - } - - // Insert new asset - await ctx.db.insert("staticAssets", { - path: args.path, - storageId: args.storageId, - contentType: args.contentType, - deploymentId: args.deploymentId, - }); - - return null; - }, -}); -``` - -### 4. `gcOldAssets` - Garbage collect previous deployments - -```typescript -export const gcOldAssets = mutation({ - args: { - currentDeploymentId: v.string(), - }, - returns: v.number(), - handler: async (ctx, args) => { - const oldAssets = await ctx.db.query("staticAssets").collect(); - let deletedCount = 0; - - for (const asset of oldAssets) { - if (asset.deploymentId !== args.currentDeploymentId) { - // Delete from file storage - await ctx.storage.delete(asset.storageId); - // Delete database record - await ctx.db.delete(asset._id); - deletedCount++; - } - } - - return deletedCount; - }, -}); -``` - -## HTTP Handler (convex/http.ts) - -```typescript -import { httpRouter } from "convex/server"; -import { httpAction } from "./_generated/server"; -import { api } from "./_generated/api"; - -const http = httpRouter(); - -// Helper: Check if path has a file extension -function hasFileExtension(path: string): boolean { - const lastSegment = path.split("/").pop() || ""; - return lastSegment.includes(".") && !lastSegment.startsWith("."); -} - -// Helper: Check if asset is hashed (for cache control) -// Vite produces: index-lj_vq_aF.js, style-B71cUw87.css -function isHashedAsset(path: string): boolean { - return /[-.][\dA-Za-z_]{6,12}\.[a-z]+$/.test(path); -} - -// Static file server with SPA support -const serveStaticFile = httpAction(async (ctx, request) => { - const url = new URL(request.url); - let path = url.pathname; - - // Normalize: serve index.html for root - if (path === "" || path === "/") { - path = "/index.html"; - } - - // Look up the asset - type AssetDoc = { - _id: string; - path: string; - storageId: string; - contentType: string; - deploymentId: string; - } | null; - - let asset: AssetDoc = await ctx.runQuery(api.staticAssets.getByPath, { path }); - - // SPA fallback: if not found and no file extension, serve index.html - if (!asset && !hasFileExtension(path)) { - asset = await ctx.runQuery(api.staticAssets.getByPath, { path: "/index.html" }); - } - - // 404 if still not found - if (!asset) { - return new Response("Not Found", { - status: 404, - headers: { "Content-Type": "text/plain" }, - }); - } - - // Get file from storage - const blob = await ctx.storage.get(asset.storageId); - if (!blob) { - return new Response("Storage error", { - status: 500, - headers: { "Content-Type": "text/plain" }, - }); - } - - // Cache control: hashed assets can be cached forever - const cacheControl = isHashedAsset(path) - ? "public, max-age=31536000, immutable" - : "public, max-age=0, must-revalidate"; - - return new Response(blob, { - status: 200, - headers: { - "Content-Type": asset.contentType, - "Cache-Control": cacheControl, - "X-Content-Type-Options": "nosniff", - }, - }); -}); - -// Catch-all route with pathPrefix -http.route({ - pathPrefix: "/", - method: "GET", - handler: serveStaticFile, -}); - -export default http; -``` - -## Upload Script (scripts/upload-static.ts) - -```typescript -import { readFileSync, readdirSync, existsSync } from "fs"; -import { join, relative, dirname, extname } from "path"; -import { randomUUID } from "crypto"; -import { fileURLToPath } from "url"; -import { ConvexHttpClient } from "convex/browser"; -import { api } from "../convex/_generated/api"; -import type { Id } from "../convex/_generated/dataModel"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const distDir = join(__dirname, "../dist"); - -// MIME type mapping -const MIME_TYPES: Record = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", -}; - -function getMimeType(path: string): string { - return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream"; -} - -// Recursively collect files -function collectFiles(dir: string, baseDir: string) { - const files: Array<{ path: string; localPath: string; contentType: string }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...collectFiles(fullPath, baseDir)); - } else if (entry.isFile()) { - files.push({ - path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"), - localPath: fullPath, - contentType: getMimeType(fullPath), - }); - } - } - return files; -} - -async function main() { - const convexUrl = process.env.CONVEX_URL; - if (!convexUrl) { - console.error("Error: CONVEX_URL environment variable required"); - process.exit(1); - } - - if (!existsSync(distDir)) { - console.error("Error: dist directory not found. Run 'npm run build' first."); - process.exit(1); - } - - const client = new ConvexHttpClient(convexUrl); - const deploymentId = randomUUID(); - const files = collectFiles(distDir, distDir); - - console.log(`Uploading ${files.length} files...`); - - for (const file of files) { - const content = readFileSync(file.localPath); - - // Get upload URL - const uploadUrl = await client.mutation(api.staticAssets.generateUploadUrl); - - // Upload to storage - const response = await fetch(uploadUrl, { - method: "POST", - headers: { "Content-Type": file.contentType }, - body: content, - }); - - const { storageId } = await response.json() as { storageId: Id<"_storage"> }; - - // Record in database - await client.mutation(api.staticAssets.recordAsset, { - path: file.path, - storageId, - contentType: file.contentType, - deploymentId, - }); - - console.log(` ${file.path}`); - } - - // Garbage collect old files - const deleted = await client.mutation(api.staticAssets.gcOldAssets, { - currentDeploymentId: deploymentId, - }); - - console.log(`\nDeleted ${deleted} old files`); - console.log(`App available at: ${convexUrl.replace(".convex.cloud", ".convex.site")}`); -} - -main().catch(console.error); -``` - -## Client-Side: Auto-detect Convex URL - -When self-hosted, the app needs to derive the Convex URL from the current location: - -```typescript -// src/main.tsx -function getConvexUrl(): string { - // Prefer environment variable (works in dev with Vite) - if (import.meta.env.VITE_CONVEX_URL) { - return import.meta.env.VITE_CONVEX_URL as string; - } - - // If hosted on Convex (.convex.site), derive API URL (.convex.cloud) - if (window.location.hostname.endsWith(".convex.site")) { - return `https://${window.location.hostname.replace(".convex.site", ".convex.cloud")}`; - } - - throw new Error("VITE_CONVEX_URL not set and not hosted on Convex."); -} - -const convex = new ConvexReactClient(getConvexUrl()); -``` - -## Vite Configuration: Module Deduplication - -**CRITICAL**: When importing from shared modules outside the project, React and Convex can be duplicated, causing runtime errors. Fix with aliases: - -```typescript -// vite.config.ts -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import path from "path"; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - // IMPORTANT: Deduplicate React and Convex to avoid multiple instances - react: path.resolve(__dirname, "./node_modules/react"), - "react-dom": path.resolve(__dirname, "./node_modules/react-dom"), - "convex/react": path.resolve(__dirname, "./node_modules/convex/react"), - }, - }, -}); -``` - -Without this, you'll see errors like: -- `Cannot read properties of null (reading 'useRef')` - duplicate React -- `Could not find Convex client! useMutation must be used under ConvexProvider` - duplicate convex/react - -## Package.json Scripts - -```json -{ - "scripts": { - "build": "vite build", - "upload:static": "npx tsx scripts/upload-static.ts", - "deploy:static": "npm run build && npm run upload:static" - } -} -``` - -## Usage - -```bash -# Deploy -CONVEX_URL=https://your-deployment.convex.cloud npm run deploy:static - -# App is now live at: -# https://your-deployment.convex.site/ -``` - -## CDN Mode (Optional) - -For better performance, non-HTML static assets can be served from a CDN via [convex-fs](https://convexfs.dev) instead of Convex storage. HTML files continue to be served from Convex (needed for SPA routing). - -**How it works**: When CDN mode is enabled, the HTTP action returns a 302 redirect to the convex-fs blob endpoint for non-HTML assets. convex-fs then redirects to a signed CDN URL (Bunny.net). For hashed assets, the browser caches after the first load. - -**Schema change**: The `staticAssets` table now supports both `storageId` (optional) and `blobId` (optional). An asset has either a `storageId` (Convex storage) or a `blobId` (CDN). - -**Deploy command**: `npx @convex-dev/static-hosting deploy --cdn` - -See `INTEGRATION.md` for full CDN setup instructions. - -## Key Learnings / Gotchas - -1. **Return validator must include `_creationTime`** - Convex adds this automatically to all documents, and the return validator must include it. - -2. **Vite hash pattern**: Vite uses base64-like hashes (e.g., `lj_vq_aF`), not hex. Regex: `/[-.][\dA-Za-z_]{6,12}\.[a-z]+$/` - -3. **Module deduplication**: Essential when importing from outside node_modules to avoid duplicate React/Convex instances. - -4. **SPA fallback logic**: Only fallback to index.html for paths WITHOUT file extensions. Paths like `/assets/missing.js` should 404. - -5. **Garbage collection**: Use a unique deploymentId per upload, then delete everything with a different deploymentId. This cleans up old hashed assets. - -6. **URL derivation**: `.convex.site` → `.convex.cloud` transformation for self-hosted apps. - -## File Structure - -After implementation, your project should have: - -``` -your-app/ -├── convex/ -│ ├── schema.ts # Add staticAssets table -│ ├── staticAssets.ts # getByPath, generateUploadUrl, recordAsset, gcOldAssets -│ └── http.ts # Static file serving with SPA support -├── scripts/ -│ └── upload-static.ts # Upload script with GC -├── src/ -│ └── main.tsx # Auto-detect Convex URL -├── vite.config.ts # Module deduplication -└── package.json # deploy:static script -``` diff --git a/example/README.md b/example/README.md index 9576ae0..a7da2aa 100644 --- a/example/README.md +++ b/example/README.md @@ -19,12 +19,13 @@ This starts: ## Deploying Static Files -Once you have a Convex deployment, you can deploy your static files: +Once you have a Convex deployment, build and upload the static files with the +CLI (it builds with the right `VITE_CONVEX_URL`, deploys the backend, and +uploads `dist/`): ```bash -# Build the example app and upload to Convex -cd example -CONVEX_URL=https://your-deployment.convex.cloud npx tsx scripts/upload-static.ts +# From the repo root +npm run deploy:static ``` Your app will then be available at `https://your-deployment.convex.site`. @@ -32,14 +33,12 @@ Your app will then be available at `https://your-deployment.convex.site`. ## Files - `convex/convex.config.ts` - Imports and uses the static hosting component -- `convex/staticHosting.ts` - Exposes upload API functions -- `convex/http.ts` - Registers static file serving routes -- `scripts/upload-static.ts` - Script to upload built files to Convex +- `convex/staticHosting.ts` - Exposes the deployment query for `` - `src/` - Example React application ## How It Works -1. The component stores static files in Convex file storage -2. HTTP routes serve files with proper Content-Type and caching headers +1. The component stores static files in its own Convex file storage +2. The component's HTTP routes serve files with proper Content-Type and caching 3. SPA fallback ensures client-side routing works correctly 4. Each deployment gets a unique ID for atomic updates and garbage collection diff --git a/example/scripts/upload-static.ts b/example/scripts/upload-static.ts deleted file mode 100644 index e4a3a85..0000000 --- a/example/scripts/upload-static.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Upload static files from the dist/ directory to Convex storage. - * - * Usage: - * npx tsx scripts/upload-static.ts - * - * This script uses `npx convex run` to call INTERNAL functions, - * which means it requires Convex CLI authentication (not just a URL). - * This is more secure than exposing public mutations. - */ - -import { readFileSync, readdirSync, existsSync } from "fs"; -import { join, relative, dirname, extname } from "path"; -import { randomUUID } from "crypto"; -import { fileURLToPath } from "url"; -import { execSync } from "child_process"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const exampleDir = join(__dirname, ".."); -const repoRoot = join(exampleDir, ".."); -const distDir = join(exampleDir, "dist"); - -// MIME type mapping -const MIME_TYPES: Record = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", - ".webmanifest": "application/manifest+json", - ".xml": "application/xml", -}; - -function getMimeType(path: string): string { - return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream"; -} - -/** - * Run a Convex function using the CLI - */ -function convexRun( - functionPath: string, - args: Record = {}, -): string { - const argsJson = JSON.stringify(args); - const cmd = `npx convex run "${functionPath}" '${argsJson}' --typecheck=disable --codegen=disable`; - try { - const result = execSync(cmd, { - cwd: repoRoot, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }); - // Parse the output - convex run outputs the result as JSON - return result.trim(); - } catch (error) { - const execError = error as { stderr?: string; stdout?: string }; - console.error("Convex run failed:", execError.stderr || execError.stdout); - throw error; - } -} - -// Recursively collect files -function collectFiles( - dir: string, - baseDir: string, -): Array<{ path: string; localPath: string; contentType: string }> { - const files: Array<{ - path: string; - localPath: string; - contentType: string; - }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...collectFiles(fullPath, baseDir)); - } else if (entry.isFile()) { - files.push({ - path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"), - localPath: fullPath, - contentType: getMimeType(fullPath), - }); - } - } - return files; -} - -async function main() { - if (!existsSync(distDir)) { - console.error( - "Error: dist directory not found. Run 'npm run build' first.", - ); - process.exit(1); - } - - const deploymentId = randomUUID(); - const files = collectFiles(distDir, distDir); - - console.log("🔒 Using secure internal functions (requires Convex CLI auth)"); - console.log( - `Uploading ${files.length} files with deployment ID: ${deploymentId}`, - ); - console.log(""); - - for (const file of files) { - const content = readFileSync(file.localPath); - - // Get upload URL via internal function - const uploadUrlOutput = convexRun("example:generateUploadUrl"); - // Parse the JSON string output (convex run returns JSON) - const uploadUrl = JSON.parse(uploadUrlOutput); - - // Upload to storage (this still uses fetch - it's a signed URL) - const response = await fetch(uploadUrl, { - method: "POST", - headers: { "Content-Type": file.contentType }, - body: content, - }); - - const { storageId } = (await response.json()) as { storageId: string }; - - // Record in database via internal function - convexRun("example:recordAsset", { - path: file.path, - storageId, - contentType: file.contentType, - deploymentId, - }); - - console.log(` ✓ ${file.path} (${file.contentType})`); - } - - console.log(""); - - // Garbage collect old files via internal function - const deletedOutput = convexRun("example:gcOldAssets", { - currentDeploymentId: deploymentId, - }); - const deleted = JSON.parse(deletedOutput); - - if (deleted > 0) { - console.log(`Cleaned up ${deleted} old file(s) from previous deployments`); - } - - console.log(""); - - console.log("✨ Upload complete!"); - console.log(""); - - // Read the deployment URL from .env.local - const envPath = join(repoRoot, ".env.local"); - if (existsSync(envPath)) { - const envContent = readFileSync(envPath, "utf-8"); - const match = envContent.match(/VITE_CONVEX_URL=(.+)/); - if (match) { - const convexUrl = match[1].trim(); - console.log( - `Your app is now available at: ${convexUrl.replace(".convex.cloud", ".convex.site")}`, - ); - } - } -} - -main().catch((error) => { - console.error("Upload failed:", error); - process.exit(1); -}); From 50087d990e334f76bb2a096025e33cc123a7390b Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:59:12 -0700 Subject: [PATCH 24/29] improve migration docs --- INTEGRATION.md | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/INTEGRATION.md b/INTEGRATION.md index b821b16..436964e 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -320,13 +320,18 @@ component itself, so the app no longer registers routes or owns the files. Removed from the client API: `registerStaticRoutes` and `exposeUploadApi`. `exposeDeploymentQuery` and `getConvexUrl` remain. -### Side-by-side migration (no downtime) +### Side-by-side migration (seamless cutover) -You can run 0.1.x and 0.2.x at the same time and cut over when the new mount is -verified. The two don't collide: the 0.1.x component is named `selfHosting` and -serves via your app's `convex/http.ts`, while 0.2.x is named `staticHosting` and -serves itself. The catch is that both ship from the same package name, so -install the old one under an **npm alias**: +You can run 0.1.x and 0.2.x at the same time and make the final switch a single +mount change with **no re-upload** — so `/` is correct the instant the new mount +goes live and the old version keeps serving until then. The trick is to upload +assets built for the *final* base path (`/`) ahead of the flip, so the cutover +just exposes assets that are already correct. + +The two versions don't collide: 0.1.x is named `selfHosting` and serves via your +app's `convex/http.ts`, while 0.2.x is named `staticHosting` and serves itself. +Both ship from the same package name, so install the old one under an **npm +alias**: 1. Install the new version, and the old one aliased to a distinct slug: @@ -344,7 +349,7 @@ install the old one under an **npm alias**: import staticHosting from "@convex-dev/static-hosting/convex.config.js"; const app = defineApp(); - app.use(staticHostingLegacy); // 0.1.x — keeps serving the live site + app.use(staticHostingLegacy); // 0.1.x — keeps serving the live site at / app.use(staticHosting, { httpPrefix: "/next/" }); // 0.2.x — staged here first export default app; @@ -360,16 +365,32 @@ install the old one under an **npm alias**: registerStaticRoutes(http, components.selfHosting); ``` -4. Deploy assets to the new mount and verify them at `/next/`: +4. Build for the **final** base path (`/`, the default) and upload that to the + new component **without** `--build` — so the CLI uploads your `/`-based dist + as-is instead of rebuilding it for `/next/`: ```bash - npx @convex-dev/static-hosting deploy # targets the staticHosting component + npm run build # base "/", the eventual mount + npx @convex-dev/static-hosting upload --prod # uploads dist as-is, no rebuild ``` -5. **Cut over:** delete the legacy `registerStaticRoutes` call (and - `convex/http.ts` if it's now empty), drop `app.use(staticHostingLegacy)`, - change the new mount to `httpPrefix: "/"`, and redeploy. Once traffic is - served by 0.2.x, remove the `static-hosting-legacy` dependency. + > Don't use `deploy` or `upload --build` here: those set + > `STATIC_HOSTING_BASE_PATH` from the *current* mount (`/next/`), which is the + > opposite of what you want. You're deliberately uploading `/`-based assets. + + Because the assets reference the absolute root (`/assets/…`), the app won't + be fully clickable at `/next/` — that root is still owned by the old version. + `/next/` is a smoke test (the component is mounted, serves `index.html`, and + the files exist at `/next/assets/…`), not a full preview. That's the price of + a re-upload-free cutover. + +5. **Cut over** with a single deploy: delete the legacy `registerStaticRoutes` + call (and `convex/http.ts` if it's now empty), drop + `app.use(staticHostingLegacy)`, change the new mount to `httpPrefix: "/"`, and + `npx convex deploy`. The new component already holds the `/`-based assets, so + `/` is correct immediately — no rebuild, no re-upload, no asset-less window. + Once traffic is served by 0.2.x, remove the `static-hosting-legacy` + dependency. ## Troubleshooting From e193a63f94765d6fd82647302538d53dd53afead Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 03:26:45 -0700 Subject: [PATCH 25/29] 0.2.0-alpha.0 --- CHANGELOG.md | 8 ++++---- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c08ea9..1f30d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.2.0 (Unreleased) +## 0.2.0 Alpha Component-owned HTTP and storage. **Breaking — redeploy your static assets after upgrading.** @@ -11,9 +11,9 @@ upgrading.** `convex/staticHosting.ts`. - Removed `registerStaticRoutes` and `exposeUploadApi` from the client API. `exposeDeploymentQuery` and `getConvexUrl` remain if you use the UpdateBanner. -- The component is now named `staticHosting` (previously `selfHosting`). The - CLI invokes it directly via `npx convex run --component staticHosting - lib:...`. If you mount the component under a different name, pass +- The component is now named `staticHosting` (previously `selfHosting`). The CLI + invokes it directly via `npx convex run --component staticHosting lib:...`. If + you mount the component under a different name, pass `--component `. - `useDeploymentUpdates` / `UpdateBanner` use `useQuery_experimental` and default to `api.staticHosting.getCurrentDeployment`. If you don't surface diff --git a/package-lock.json b/package-lock.json index b2fbab8..e033efd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/static-hosting", - "version": "0.1.4", + "version": "0.2.0-alpha.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@convex-dev/static-hosting", - "version": "0.1.4", + "version": "0.2.0-alpha.0", "hasInstallScript": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 44fdb60..9b7fb4e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "bugs": { "url": "https://github.com/get-convex/static-hosting/issues" }, - "version": "0.1.4", + "version": "0.2.0-alpha.0", "license": "Apache-2.0", "keywords": [ "convex", From a3b264db136bd7e2b58359e9aa390f859ff691a0 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:36:42 -0700 Subject: [PATCH 26/29] don't track dist --- dist/cli/commands.d.ts | 6 - dist/cli/commands.d.ts.map | 1 - dist/cli/commands.js | 51 ----- dist/cli/commands.js.map | 1 - dist/cli/deploy.js | 256 ---------------------- dist/cli/deploy.js.map | 1 - dist/cli/init.d.ts | 9 - dist/cli/init.d.ts.map | 1 - dist/cli/init.js | 181 ---------------- dist/cli/upload.d.ts | 15 -- dist/cli/upload.js | 430 ------------------------------------- dist/cli/upload.js.map | 1 - dist/component/lib.d.ts | 88 -------- 13 files changed, 1041 deletions(-) delete mode 100644 dist/cli/commands.d.ts delete mode 100644 dist/cli/commands.d.ts.map delete mode 100644 dist/cli/commands.js delete mode 100644 dist/cli/commands.js.map delete mode 100644 dist/cli/deploy.js delete mode 100644 dist/cli/deploy.js.map delete mode 100644 dist/cli/init.d.ts delete mode 100644 dist/cli/init.d.ts.map delete mode 100644 dist/cli/init.js delete mode 100644 dist/cli/upload.d.ts delete mode 100644 dist/cli/upload.js delete mode 100644 dist/cli/upload.js.map delete mode 100644 dist/component/lib.d.ts diff --git a/dist/cli/commands.d.ts b/dist/cli/commands.d.ts deleted file mode 100644 index adc7aba..0000000 --- a/dist/cli/commands.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export declare function runConvex(args: string[]): string; -export declare function runConvexAsync(args: string[]): Promise; -export declare function spawnConvex(args: string[]): number | null; -export declare function spawnNpmRun(script: string, env?: NodeJS.ProcessEnv): number | null; -export declare function spawnStaticHostingCli(args: string[]): number | null; -//# sourceMappingURL=commands.d.ts.map \ No newline at end of file diff --git a/dist/cli/commands.d.ts.map b/dist/cli/commands.d.ts.map deleted file mode 100644 index fcf774f..0000000 --- a/dist/cli/commands.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAkBA,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAKhD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB9D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAIzD;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,IAAI,CAMf;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAKnE"} \ No newline at end of file diff --git a/dist/cli/commands.js b/dist/cli/commands.js deleted file mode 100644 index 94fa59b..0000000 --- a/dist/cli/commands.js +++ /dev/null @@ -1,51 +0,0 @@ -import { execFile, execFileSync, spawnSync } from "child_process"; -import { createRequire } from "module"; -import { dirname, join, resolve } from "path"; -import { fileURLToPath } from "url"; -const requireFromCwd = createRequire(join(process.cwd(), "package.json")); -function convexBinPath() { - const packageJsonPath = requireFromCwd.resolve("convex/package.json"); - return join(dirname(packageJsonPath), "bin", "main.js"); -} -function shellCommand(command) { - return process.platform === "win32" - ? { command: "cmd.exe", args: ["/d", "/s", "/c", command] } - : { command: "sh", args: ["-lc", command] }; -} -export function runConvex(args) { - return execFileSync(process.execPath, [convexBinPath(), ...args], { - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }).trim(); -} -export function runConvexAsync(args) { - return new Promise((resolve, reject) => { - execFile(process.execPath, [convexBinPath(), ...args], { encoding: "utf-8" }, (error, stdout, stderr) => { - if (error) { - console.error("Convex run failed:", stderr || stdout); - reject(error); - return; - } - resolve(stdout.trim()); - }); - }); -} -export function spawnConvex(args) { - return spawnSync(process.execPath, [convexBinPath(), ...args], { - stdio: "inherit", - }).status; -} -export function spawnNpmRun(script, env = process.env) { - const command = shellCommand(`npm run ${script}`); - return spawnSync(command.command, command.args, { - env, - stdio: "inherit", - }).status; -} -export function spawnStaticHostingCli(args) { - const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), "index.js"); - return spawnSync(process.execPath, [cliPath, ...args], { - stdio: "inherit", - }).status; -} -//# sourceMappingURL=commands.js.map \ No newline at end of file diff --git a/dist/cli/commands.js.map b/dist/cli/commands.js.map deleted file mode 100644 index 9ccc9d5..0000000 --- a/dist/cli/commands.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;AAE1E,SAAS,aAAa;IACpB,MAAM,eAAe,GAAG,cAAc,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO;QACjC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;QAC3D,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,OAAO,YAAY,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,aAAa,EAAE,EAAE,GAAG,IAAI,CAAC,EAAE;QAChE,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;KAChC,CAAC,CAAC,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,OAAO,CAAC,QAAQ,EAChB,CAAC,aAAa,EAAE,EAAE,GAAG,IAAI,CAAC,EAC1B,EAAE,QAAQ,EAAE,OAAO,EAAE,EACrB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,MAAM,IAAI,MAAM,CAAC,CAAC;gBACtD,MAAM,CAAC,KAAK,CAAC,CAAC;gBACd,OAAO;YACT,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACzB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAc;IACxC,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,aAAa,EAAE,EAAE,GAAG,IAAI,CAAC,EAAE;QAC7D,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC,MAAM,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,MAAc,EACd,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,MAAM,EAAE,CAAC,CAAC;IAClD,OAAO,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE;QAC9C,GAAG;QACH,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC,MAAM,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAc;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC7E,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,EAAE;QACrD,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC,MAAM,CAAC;AACZ,CAAC"} \ No newline at end of file diff --git a/dist/cli/deploy.js b/dist/cli/deploy.js deleted file mode 100644 index b2557fc..0000000 --- a/dist/cli/deploy.js +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env node -/** - * One-shot deployment command that deploys both Convex backend and static files. - * - * Usage: - * npx @convex-dev/static-hosting deploy [options] - * - * This command: - * 1. Builds the frontend with the correct VITE_CONVEX_URL - * 2. Deploys the Convex backend (npx convex deploy) - * 3. Deploys static files to Convex storage - * - * The goal is to minimize the inconsistency window between backend and frontend. - */ -import { existsSync, readFileSync } from "fs"; -import { resolve } from "path"; -import { runConvex, spawnConvex, spawnNpmRun, spawnStaticHostingCli, } from "./commands.js"; -function parseArgs(args) { - const result = { - dist: "./dist", - component: "staticHosting", - help: false, - skipBuild: false, - skipConvex: false, - cdn: false, - }; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === "--help" || arg === "-h") { - result.help = true; - } - else if (arg === "--dist" || arg === "-d") { - result.dist = args[++i] || result.dist; - } - else if (arg === "--component" || arg === "-c") { - result.component = args[++i] || result.component; - } - else if (arg === "--skip-build") { - result.skipBuild = true; - } - else if (arg === "--skip-convex") { - result.skipConvex = true; - } - else if (arg === "--cdn") { - result.cdn = true; - } - } - return result; -} -function showHelp() { - console.log(` -Usage: npx @convex-dev/static-hosting deploy [options] - -One-shot deployment: builds frontend, deploys Convex backend, then deploys static files. -Minimizes the inconsistency window between backend and frontend updates. - -Options: - -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. - convex/.ts (default: staticHosting). Not - the registered component name from convex.config.ts. - --skip-build Skip the build step (use existing dist) - --skip-convex Skip Convex backend deployment - --cdn Upload non-HTML assets to convex-fs CDN - -h, --help Show this help message - -Deployment Flow: - 1. Build frontend with production VITE_CONVEX_URL - 2. Deploy Convex backend (npx convex deploy) - 3. Deploy static files to Convex storage - -Examples: - # Full deployment - npx @convex-dev/static-hosting deploy - - # Skip build (if already built) - npx @convex-dev/static-hosting deploy --skip-build - - # Only deploy static files (skip Convex backend) - npx @convex-dev/static-hosting deploy --skip-convex -`); -} -/** - * Get the production Convex URL - */ -function getConvexProdUrl() { - try { - return runConvex(["env", "get", "CONVEX_CLOUD_URL", "--prod"]) || null; - } - catch { - // Fall back to env files - } - // Try env files as fallback - const envFiles = [".env.production", ".env.production.local", ".env.local"]; - for (const envFile of envFiles) { - if (existsSync(envFile)) { - const content = readFileSync(envFile, "utf-8"); - const match = content.match(/(?:VITE_)?CONVEX_URL=(.+)/); - if (match) { - return match[1].trim(); - } - } - } - return null; -} -function getConvexProdSiteUrl() { - try { - return runConvex(["env", "get", "CONVEX_SITE_URL", "--prod"]) || null; - } - catch { - return null; - } -} -/** - * Run the Convex storage upload flow - */ -async function uploadToConvexStorage(distDir, componentName, useCdn) { - console.log(""); - console.log(useCdn - ? "📦 Uploading static files (HTML to Convex, assets to CDN)..." - : "📦 Uploading static files to Convex storage..."); - console.log(""); - const uploadArgs = [ - "upload", - "--dist", - distDir, - "--component", - componentName, - "--prod", - ]; - if (useCdn) { - uploadArgs.push("--cdn"); - } - const result = spawnStaticHostingCli(uploadArgs); - return result === 0; -} -async function main() { - const args = parseArgs(process.argv.slice(2)); - if (args.help) { - showHelp(); - process.exit(0); - } - console.log(""); - console.log("🚀 Convex + Static Files Deployment"); - console.log("═══════════════════════════════════════════════════════════"); - const startTime = Date.now(); - // Step 1: Get production Convex URL (needed for build) - console.log(""); - console.log("Step 1: Getting production Convex URL..."); - let convexUrl = getConvexProdUrl(); - if (!convexUrl && !args.skipConvex) { - console.log(" No production deployment found. Will get URL after deploying backend."); - } - else if (convexUrl) { - console.log(` ✓ ${convexUrl}`); - } - // Step 2: Build frontend - if (!args.skipBuild) { - console.log(""); - console.log("Step 2: Building frontend..."); - // If we don't have a URL yet, we need to deploy Convex first to get it - if (!convexUrl && !args.skipConvex) { - console.log(" Deploying Convex backend first to get production URL..."); - console.log(""); - const convexResult = spawnConvex(["deploy"]); - if (convexResult !== 0) { - console.error(""); - console.error("❌ Convex deployment failed"); - process.exit(1); - } - // Now get the URL - convexUrl = getConvexProdUrl(); - if (!convexUrl) { - console.error(""); - console.error("❌ Could not get production Convex URL after deployment"); - process.exit(1); - } - console.log(""); - console.log(` ✓ Production URL: ${convexUrl}`); - args.skipConvex = true; // Already deployed - } - if (!convexUrl) { - console.error(""); - console.error("❌ Could not determine Convex URL for build"); - console.error(" Run 'npx convex deploy' first or remove --skip-convex"); - process.exit(1); - } - console.log(` Building with VITE_CONVEX_URL=${convexUrl}`); - console.log(""); - const buildResult = spawnNpmRun("build", { - ...process.env, - VITE_CONVEX_URL: convexUrl, - }); - if (buildResult !== 0) { - console.error(""); - console.error("❌ Build failed"); - process.exit(1); - } - console.log(""); - console.log(" ✓ Build complete"); - } - else { - console.log(""); - console.log("Step 2: Skipping build (--skip-build)"); - } - // Step 3: Deploy Convex backend - if (!args.skipConvex) { - console.log(""); - console.log("Step 3: Deploying Convex backend..."); - console.log(""); - const convexResult = spawnConvex(["deploy"]); - if (convexResult !== 0) { - console.error(""); - console.error("❌ Convex deployment failed"); - process.exit(1); - } - console.log(""); - console.log(" ✓ Convex backend deployed"); - } - else { - console.log(""); - console.log("Step 3: Skipping Convex deployment (--skip-convex or already deployed)"); - } - // Step 4: Deploy static files - console.log(""); - console.log("Step 4: Deploying static files to Convex storage..."); - const distDir = resolve(args.dist); - if (!existsSync(distDir)) { - console.error(""); - console.error(`❌ Dist directory not found: ${distDir}`); - console.error(" Run build first or check --dist path"); - process.exit(1); - } - const staticDeploySuccess = await uploadToConvexStorage(distDir, args.component, args.cdn); - if (!staticDeploySuccess) { - console.error(""); - console.error("❌ Static file upload failed"); - process.exit(1); - } - // Done! - const duration = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(""); - console.log("═══════════════════════════════════════════════════════════"); - console.log(`✨ Deployment complete! (${duration}s)`); - console.log(""); - const siteUrl = getConvexProdSiteUrl(); - if (siteUrl) { - console.log(`Frontend: ${siteUrl}`); - } - console.log(""); -} -main().catch((error) => { - console.error("Deployment failed:", error); - process.exit(1); -}); -//# sourceMappingURL=deploy.js.map \ No newline at end of file diff --git a/dist/cli/deploy.js.map b/dist/cli/deploy.js.map deleted file mode 100644 index 8bab917..0000000 --- a/dist/cli/deploy.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"deploy.js","sourceRoot":"","sources":["../../src/cli/deploy.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli/init.d.ts b/dist/cli/init.d.ts deleted file mode 100644 index 06ae4fd..0000000 --- a/dist/cli/init.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node -/** - * Output integration instructions for LLMs. - * - * Usage: - * npx @convex-dev/static-hosting init - */ -declare const instructions = "\n# Convex Static Hosting - Integration Instructions\n\nYou are integrating the @convex-dev/static-hosting component into a Convex app.\nThis component enables hosting static files (React/Vite apps) directly on Convex.\n\n## What This Component Does\n\n- Stores static files in Convex storage\n- Serves files via HTTP actions with proper MIME types\n- Supports SPA routing (fallback to index.html)\n- Smart caching: hashed assets cached forever, HTML revalidates\n- ETag support for efficient cache revalidation\n- Live reload notifications when new deployments happen\n\n## Files to Create/Modify\n\n### 1. convex/convex.config.ts (create or modify)\n\n```typescript\nimport { defineApp } from \"convex/server\";\nimport selfHosting from \"@convex-dev/static-hosting/convex.config\";\n\nconst app = defineApp();\napp.use(selfHosting);\n\nexport default app;\n```\n\n### 2. convex/staticHosting.ts (create)\n\n```typescript\nimport { components } from \"./_generated/api\";\nimport {\n exposeUploadApi,\n exposeDeploymentQuery,\n} from \"@convex-dev/static-hosting\";\n\n// Internal functions for secure uploads (only callable via CLI)\nexport const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } =\n exposeUploadApi(components.selfHosting);\n\n// Public query for live reload notifications\nexport const { getCurrentDeployment } =\n exposeDeploymentQuery(components.selfHosting);\n```\n\n### 3. convex/http.ts (create or modify)\n\n```typescript\nimport { httpRouter } from \"convex/server\";\nimport { registerStaticRoutes } from \"@convex-dev/static-hosting\";\nimport { components } from \"./_generated/api\";\n\nconst http = httpRouter();\n\n// Option A: Serve at root (if no other HTTP routes)\nregisterStaticRoutes(http, components.selfHosting);\n\n// Option B: Serve at /app/ prefix (recommended if you have API routes)\n// registerStaticRoutes(http, components.selfHosting, {\n// pathPrefix: \"/app\",\n// });\n\n// Add other HTTP routes here if needed\n// http.route({ path: \"/api/webhook\", method: \"POST\", handler: ... });\n\nexport default http;\n```\n\n### 4. package.json scripts (add)\n\n```json\n{\n \"scripts\": {\n \"build\": \"vite build\",\n \"deploy:static\": \"npx @convex-dev/static-hosting upload --build --prod\"\n }\n}\n```\n\nIMPORTANT: Use `--build` flag instead of running `npm run build` separately.\nThe `--build` flag ensures `VITE_CONVEX_URL` is set correctly for the target\nenvironment (production or dev). Running build separately uses .env.local which\nhas the dev URL.\n\n### 5. src/App.tsx (optional: add live reload banner)\n\n```typescript\nimport { UpdateBanner } from \"@convex-dev/static-hosting/react\";\nimport { api } from \"../convex/_generated/api\";\n\nfunction App() {\n return (\n
\n {/* Shows banner when new deployment is available */}\n \n \n {/* Rest of your app */}\n
\n );\n}\n```\n\nOr use the hook for custom UI:\n```typescript\nimport { useDeploymentUpdates } from \"@convex-dev/static-hosting/react\";\nimport { api } from \"../convex/_generated/api\";\n\nfunction App() {\n const { updateAvailable, reload, dismiss } = useDeploymentUpdates(\n api.staticHosting.getCurrentDeployment\n );\n \n // Custom update notification UI\n}\n```\n\n## Deployment\n\n```bash\n# Login to Convex (first time)\nnpx convex login\n\n# Deploy Convex backend to production FIRST\nnpx convex deploy\n\n# Deploy static files to production\nnpm run deploy:static\n\n# Your app is now live at:\n# https://your-deployment.convex.site\n# (or https://your-deployment.convex.site/app/ if using path prefix)\n```\n\nFor development/testing:\n```bash\n# Push to dev environment\nnpx convex dev --once\n\n# Deploy static files to dev (omit --prod)\nnpx @convex-dev/static-hosting upload --build\n```\n\n## CLI Reference\n\n```bash\nnpx @convex-dev/static-hosting upload [options]\n\nOptions:\n -d, --dist Path to dist directory (default: ./dist)\n -c, --component Module name where upload API is exposed \u2014 i.e. convex/.ts (default: staticHosting)\n --prod Deploy to production Convex deployment\n --dev Deploy to dev deployment (default)\n -b, --build Run 'npm run build' with correct VITE_CONVEX_URL\n -h, --help Show help\n```\n\n## Important Notes\n\n1. The upload functions are INTERNAL - they can only be called via `npx convex run`, not from the public internet\n2. Static files are stored in the app's storage (not the component's) for proper isolation\n3. Hashed assets (e.g., main-abc123.js) get immutable caching; HTML files always revalidate\n4. The component supports SPA routing - routes without file extensions serve index.html\n5. Always use `--build` flag to ensure VITE_CONVEX_URL is set correctly for the target environment\n6. Deploy Convex backend (`npx convex deploy`) BEFORE deploying static files to production\n"; -//# sourceMappingURL=init.d.ts.map \ No newline at end of file diff --git a/dist/cli/init.d.ts.map b/dist/cli/init.d.ts.map deleted file mode 100644 index 0104f94..0000000 --- a/dist/cli/init.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,YAAY,siKA0KjB,CAAC"} \ No newline at end of file diff --git a/dist/cli/init.js b/dist/cli/init.js deleted file mode 100644 index b4a837e..0000000 --- a/dist/cli/init.js +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env node -"use strict"; -/** - * Output integration instructions for LLMs. - * - * Usage: - * npx @convex-dev/static-hosting init - */ -const instructions = ` -# Convex Static Hosting - Integration Instructions - -You are integrating the @convex-dev/static-hosting component into a Convex app. -This component enables hosting static files (React/Vite apps) directly on Convex. - -## What This Component Does - -- Stores static files in Convex storage -- Serves files via HTTP actions with proper MIME types -- Supports SPA routing (fallback to index.html) -- Smart caching: hashed assets cached forever, HTML revalidates -- ETag support for efficient cache revalidation -- Live reload notifications when new deployments happen - -## Files to Create/Modify - -### 1. convex/convex.config.ts (create or modify) - -\`\`\`typescript -import { defineApp } from "convex/server"; -import selfHosting from "@convex-dev/static-hosting/convex.config"; - -const app = defineApp(); -app.use(selfHosting); - -export default app; -\`\`\` - -### 2. convex/staticHosting.ts (create) - -\`\`\`typescript -import { components } from "./_generated/api"; -import { - exposeUploadApi, - exposeDeploymentQuery, -} from "@convex-dev/static-hosting"; - -// Internal functions for secure uploads (only callable via CLI) -export const { generateUploadUrl, generateUploadUrls, recordAsset, recordAssets, gcOldAssets, listAssets } = - exposeUploadApi(components.selfHosting); - -// Public query for live reload notifications -export const { getCurrentDeployment } = - exposeDeploymentQuery(components.selfHosting); -\`\`\` - -### 3. convex/http.ts (create or modify) - -\`\`\`typescript -import { httpRouter } from "convex/server"; -import { registerStaticRoutes } from "@convex-dev/static-hosting"; -import { components } from "./_generated/api"; - -const http = httpRouter(); - -// Option A: Serve at root (if no other HTTP routes) -registerStaticRoutes(http, components.selfHosting); - -// Option B: Serve at /app/ prefix (recommended if you have API routes) -// registerStaticRoutes(http, components.selfHosting, { -// pathPrefix: "/app", -// }); - -// Add other HTTP routes here if needed -// http.route({ path: "/api/webhook", method: "POST", handler: ... }); - -export default http; -\`\`\` - -### 4. package.json scripts (add) - -\`\`\`json -{ - "scripts": { - "build": "vite build", - "deploy:static": "npx @convex-dev/static-hosting upload --build --prod" - } -} -\`\`\` - -IMPORTANT: Use \`--build\` flag instead of running \`npm run build\` separately. -The \`--build\` flag ensures \`VITE_CONVEX_URL\` is set correctly for the target -environment (production or dev). Running build separately uses .env.local which -has the dev URL. - -### 5. src/App.tsx (optional: add live reload banner) - -\`\`\`typescript -import { UpdateBanner } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; - -function App() { - return ( -
- {/* Shows banner when new deployment is available */} - - - {/* Rest of your app */} -
- ); -} -\`\`\` - -Or use the hook for custom UI: -\`\`\`typescript -import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; -import { api } from "../convex/_generated/api"; - -function App() { - const { updateAvailable, reload, dismiss } = useDeploymentUpdates( - api.staticHosting.getCurrentDeployment - ); - - // Custom update notification UI -} -\`\`\` - -## Deployment - -\`\`\`bash -# Login to Convex (first time) -npx convex login - -# Deploy Convex backend to production FIRST -npx convex deploy - -# Deploy static files to production -npm run deploy:static - -# Your app is now live at: -# https://your-deployment.convex.site -# (or https://your-deployment.convex.site/app/ if using path prefix) -\`\`\` - -For development/testing: -\`\`\`bash -# Push to dev environment -npx convex dev --once - -# Deploy static files to dev (omit --prod) -npx @convex-dev/static-hosting upload --build -\`\`\` - -## CLI Reference - -\`\`\`bash -npx @convex-dev/static-hosting upload [options] - -Options: - -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. convex/.ts (default: staticHosting) - --prod Deploy to production Convex deployment - --dev Deploy to dev deployment (default) - -b, --build Run 'npm run build' with correct VITE_CONVEX_URL - -h, --help Show help -\`\`\` - -## Important Notes - -1. The upload functions are INTERNAL - they can only be called via \`npx convex run\`, not from the public internet -2. Static files are stored in the app's storage (not the component's) for proper isolation -3. Hashed assets (e.g., main-abc123.js) get immutable caching; HTML files always revalidate -4. The component supports SPA routing - routes without file extensions serve index.html -5. Always use \`--build\` flag to ensure VITE_CONVEX_URL is set correctly for the target environment -6. Deploy Convex backend (\`npx convex deploy\`) BEFORE deploying static files to production -`; -console.log(instructions); -//# sourceMappingURL=init.js.map \ No newline at end of file diff --git a/dist/cli/upload.d.ts b/dist/cli/upload.d.ts deleted file mode 100644 index 501e628..0000000 --- a/dist/cli/upload.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node -/** - * CLI tool to upload static files to Convex storage. - * - * Usage: - * npx @convex-dev/static-hosting upload [options] - * - * Options: - * --dist Path to dist directory (default: ./dist) - * --component Module name where upload API is exposed — i.e. convex/.ts (default: staticHosting) - * --prod Deploy to production deployment - * --help Show help - */ -export {}; -//# sourceMappingURL=upload.d.ts.map \ No newline at end of file diff --git a/dist/cli/upload.js b/dist/cli/upload.js deleted file mode 100644 index 2401c32..0000000 --- a/dist/cli/upload.js +++ /dev/null @@ -1,430 +0,0 @@ -#!/usr/bin/env node -/** - * CLI tool to upload static files to Convex storage. - * - * Usage: - * npx @convex-dev/static-hosting upload [options] - * - * Options: - * --dist Path to dist directory (default: ./dist) - * --component Module name where upload API is exposed — i.e. convex/.ts (default: staticHosting) - * --prod Deploy to production deployment - * --help Show help - */ -import { readFileSync, readdirSync, existsSync } from "fs"; -import { join, relative, extname, resolve } from "path"; -import { randomUUID } from "crypto"; -import { runConvex, runConvexAsync, spawnShell } from "./commands.js"; -// MIME type mapping -const MIME_TYPES = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".txt": "text/plain; charset=utf-8", - ".map": "application/json", - ".webmanifest": "application/manifest+json", - ".xml": "application/xml", -}; -function getMimeType(path) { - return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream"; -} -function parseArgs(args) { - const result = { - dist: "./dist", - component: "staticHosting", - prod: false, // Default to dev, use --prod for production - build: false, - cdn: false, - cdnDeleteFunction: "", - concurrency: 5, - help: false, - }; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === "--help" || arg === "-h") { - result.help = true; - } else if (arg === "--dist" || arg === "-d") { - result.dist = args[++i] || result.dist; - } else if (arg === "--component" || arg === "-c") { - result.component = args[++i] || result.component; - } else if (arg === "--prod") { - result.prod = true; - } else if (arg === "--no-prod" || arg === "--dev") { - result.prod = false; - } else if (arg === "--build" || arg === "-b") { - result.build = true; - } else if (arg === "--cdn") { - result.cdn = true; - } else if (arg === "--cdn-delete-function") { - result.cdnDeleteFunction = args[++i] || result.cdnDeleteFunction; - } else if (arg === "--concurrency" || arg === "-j") { - const val = parseInt(args[++i], 10); - if (val > 0) result.concurrency = val; - } - } - return result; -} -function showHelp() { - console.log(` -Usage: npx @convex-dev/static-hosting upload [options] - -Upload static files from a dist directory to Convex storage. - -Options: - -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. - convex/.ts (default: staticHosting). Not - the registered component name from convex.config.ts. - --prod Deploy to production deployment - -b, --build Run 'npm run build' with correct VITE_CONVEX_URL before uploading - --cdn Upload non-HTML assets to convex-fs CDN instead of Convex storage - --cdn-delete-function Convex function to delete CDN blobs (default: :deleteCdnBlobs) - -j, --concurrency Number of parallel uploads (default: 5) - -h, --help Show this help message - -Examples: - # Upload to Convex storage - npx @convex-dev/static-hosting upload - npx @convex-dev/static-hosting upload --dist ./build --prod - npx @convex-dev/static-hosting upload --build --prod - - # Upload with CDN (non-HTML files served from CDN) - npx @convex-dev/static-hosting upload --cdn --prod -`); -} -// Global flag for production mode -let useProd = true; -function getEnvFileConvexUrl() { - if (!existsSync(".env.local")) { - return null; - } - const envContent = readFileSync(".env.local", "utf-8"); - const match = envContent.match(/(?:VITE_)?CONVEX_URL=(.+)/); - return match?.[1]?.trim() || null; -} -function getConvexEnv(name, prod) { - try { - return runConvex(["env", "get", name, ...(prod ? ["--prod"] : [])]) || null; - } catch { - return null; - } -} -function convexRunAsync(functionPath, args = {}) { - return runConvexAsync([ - "run", - functionPath, - JSON.stringify(args), - "--typecheck=disable", - "--codegen=disable", - ...(useProd ? ["--prod"] : []), - ]); -} -async function uploadWithConcurrency( - files, - componentName, - deploymentId, - useCdn, - siteUrl, - concurrency, -) { - const total = files.length; - // Separate CDN and storage files - const cdnFiles = []; - const storageFiles = []; - for (const file of files) { - const isHtml = file.contentType.startsWith("text/html"); - if (useCdn && !isHtml && siteUrl) { - cdnFiles.push(file); - } else { - storageFiles.push(file); - } - } - // Upload storage files using batch operations - let completed = 0; - const allAssets = []; - if (storageFiles.length > 0) { - // Step 1: Generate all upload URLs in one batch call - console.log(` Generating ${storageFiles.length} upload URLs...`); - const urlsOutput = await convexRunAsync( - `${componentName}:generateUploadUrls`, - { count: storageFiles.length }, - ); - const uploadUrls = JSON.parse(urlsOutput); - // Step 2: Upload all files in parallel via fetch - const storageIds = new Array(storageFiles.length); - const pending = new Set(); - for (let i = 0; i < storageFiles.length; i++) { - const idx = i; - const file = storageFiles[idx]; - const task = (async () => { - const content = readFileSync(file.localPath); - const response = await fetch(uploadUrls[idx], { - method: "POST", - headers: { "Content-Type": file.contentType }, - body: content, - }); - const { storageId } = await response.json(); - storageIds[idx] = storageId; - completed++; - const isHtml = file.contentType.startsWith("text/html"); - console.log( - ` [${completed}/${total}] ${file.path} (${isHtml ? "storage/html" : "storage"})`, - ); - })().then(() => { - pending.delete(task); - }); - pending.add(task); - if (pending.size >= concurrency) { - await Promise.race(pending); - } - } - await Promise.all(pending); - for (let i = 0; i < storageFiles.length; i++) { - allAssets.push({ - path: storageFiles[i].path, - storageId: storageIds[i], - contentType: storageFiles[i].contentType, - deploymentId, - }); - } - } - // Upload CDN files (still uses per-file calls since CDN has its own upload endpoint) - if (cdnFiles.length > 0 && siteUrl) { - const pending = new Set(); - for (const file of cdnFiles) { - const task = (async () => { - const content = readFileSync(file.localPath); - const uploadResponse = await fetch(`${siteUrl}/fs/upload`, { - method: "POST", - headers: { "Content-Type": file.contentType }, - body: content, - }); - if (!uploadResponse.ok) { - throw new Error( - `CDN upload failed for ${file.path}: ${uploadResponse.status}`, - ); - } - const { blobId } = await uploadResponse.json(); - allAssets.push({ - path: file.path, - blobId, - contentType: file.contentType, - deploymentId, - }); - completed++; - console.log(` [${completed}/${total}] ${file.path} (cdn)`); - })().then(() => { - pending.delete(task); - }); - pending.add(task); - if (pending.size >= concurrency) { - await Promise.race(pending); - } - } - await Promise.all(pending); - } - // Step 3: Record all assets in one batch call - if (allAssets.length > 0) { - console.log(" Recording assets..."); - // recordAssets only handles storageId assets; CDN assets need individual recording - const storageAssets = allAssets.filter((a) => a.storageId); - const cdnAssets = allAssets.filter((a) => a.blobId); - if (storageAssets.length > 0) { - await convexRunAsync(`${componentName}:recordAssets`, { - assets: storageAssets.map((a) => ({ - path: a.path, - storageId: a.storageId, - contentType: a.contentType, - deploymentId: a.deploymentId, - })), - }); - } - // CDN assets still need individual recording (they use blobId not storageId) - for (const asset of cdnAssets) { - await convexRunAsync(`${componentName}:recordAsset`, { - path: asset.path, - blobId: asset.blobId, - contentType: asset.contentType, - deploymentId: asset.deploymentId, - }); - } - } -} -function collectFiles(dir, baseDir) { - const files = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...collectFiles(fullPath, baseDir)); - } else if (entry.isFile()) { - files.push({ - path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"), - localPath: fullPath, - contentType: getMimeType(fullPath), - }); - } - } - return files; -} -async function main() { - const args = parseArgs(process.argv.slice(2)); - if (args.help) { - showHelp(); - process.exit(0); - } - // Set global prod flag - useProd = args.prod; - // Run build if requested - if (args.build) { - let convexUrl = null; - if (useProd) { - convexUrl = getConvexEnv("CONVEX_CLOUD_URL", true); - if (!convexUrl) { - console.error("Could not get production Convex URL."); - console.error( - "Make sure you have deployed to production: npx convex deploy", - ); - process.exit(1); - } - } else { - convexUrl = - getConvexEnv("CONVEX_CLOUD_URL", false) ?? getEnvFileConvexUrl(); - } - if (!convexUrl) { - console.error("Could not determine Convex URL for build."); - process.exit(1); - } - const envLabel = useProd ? "production" : "development"; - console.log(`🔨 Building for ${envLabel}...`); - console.log(` VITE_CONVEX_URL=${convexUrl}`); - console.log(""); - const buildResult = spawnShell("build", { - ...process.env, - VITE_CONVEX_URL: convexUrl, - }); - if (buildResult !== 0) { - console.error("Build failed."); - process.exit(1); - } - console.log(""); - } - const distDir = resolve(args.dist); - const componentName = args.component; - const useCdn = args.cdn; - // Convex storage deployment - if (!existsSync(distDir)) { - console.error(`Error: dist directory not found: ${distDir}`); - console.error( - "Run your build command first (e.g., 'npm run build' or add --build flag)", - ); - process.exit(1); - } - // If CDN mode, we need the site URL for uploading to convex-fs - let siteUrl = null; - if (useCdn) { - siteUrl = getConvexSiteUrl(useProd); - if (!siteUrl) { - console.error( - "Error: Could not determine Convex site URL for CDN uploads.", - ); - console.error("Make sure your Convex deployment is running."); - process.exit(1); - } - } - const deploymentId = randomUUID(); - const files = collectFiles(distDir, distDir); - const envLabel = useProd ? "production" : "development"; - console.log(`🚀 Deploying to ${envLabel} environment`); - if (useCdn) { - console.log("☁️ CDN mode: non-HTML assets will be uploaded to convex-fs"); - } - console.log("🔒 Using secure internal functions (requires Convex CLI auth)"); - console.log( - `Uploading ${files.length} files with deployment ID: ${deploymentId}`, - ); - console.log(`Component: ${componentName}`); - console.log(""); - try { - await uploadWithConcurrency( - files, - componentName, - deploymentId, - useCdn, - siteUrl, - args.concurrency, - ); - } catch { - console.error("Upload failed."); - process.exit(1); - } - console.log(""); - // Garbage collect old files - const gcOutput = await convexRunAsync(`${componentName}:gcOldAssets`, { - currentDeploymentId: deploymentId, - }); - const gcResult = JSON.parse(gcOutput); - // Handle both old format (number) and new format ({ deleted, blobIds }) - const deletedCount = - typeof gcResult === "number" ? gcResult : gcResult.deleted; - const oldBlobIds = - typeof gcResult === "object" && gcResult.blobIds ? gcResult.blobIds : []; - if (deletedCount > 0) { - console.log( - `Cleaned up ${deletedCount} old storage file(s) from previous deployments`, - ); - } - // Clean up old CDN blobs if any - if (oldBlobIds.length > 0) { - const cdnDeleteFn = - args.cdnDeleteFunction || `${componentName}:deleteCdnBlobs`; - try { - await convexRunAsync(cdnDeleteFn, { blobIds: oldBlobIds }); - console.log( - `Cleaned up ${oldBlobIds.length} old CDN blob(s) from previous deployments`, - ); - } catch { - console.warn( - `Warning: Could not delete old CDN blobs. Make sure ${cdnDeleteFn} is defined.`, - ); - } - } - console.log(""); - console.log("✨ Upload complete!"); - // Show the deployment URL - const deployedSiteUrl = getConvexSiteUrl(useProd); - if (deployedSiteUrl) { - console.log(""); - console.log(`Your app is now available at: ${deployedSiteUrl}`); - } -} -/** - * Get the Convex site URL (.convex.site) - */ -function getConvexSiteUrl(prod) { - const siteUrl = getConvexEnv("CONVEX_SITE_URL", prod); - if (siteUrl) { - return siteUrl; - } - const cloudUrl = getConvexEnv("CONVEX_CLOUD_URL", prod); - if (cloudUrl?.includes(".convex.cloud")) { - return cloudUrl.replace(".convex.cloud", ".convex.site"); - } - return null; -} -main().catch((error) => { - console.error("Upload failed:", error); - process.exit(1); -}); -//# sourceMappingURL=upload.js.map diff --git a/dist/cli/upload.js.map b/dist/cli/upload.js.map deleted file mode 100644 index ad0d82e..0000000 --- a/dist/cli/upload.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/cli/upload.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/component/lib.d.ts b/dist/component/lib.d.ts deleted file mode 100644 index 42c7fa5..0000000 --- a/dist/component/lib.d.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { v } from "convex/values"; -/** - * Look up an asset by its URL path. - */ -export declare const getByPath: import("convex/server").RegisteredQuery<"public", { - path: string; -}, Promise<{ - _id: import("convex/values").GenericId<"staticAssets">; - _creationTime: number; - blobId?: string | undefined; - storageId?: import("convex/values").GenericId<"_storage"> | undefined; - path: string; - contentType: string; - deploymentId: string; -} | null>>; -/** - * Generate a signed URL for uploading a file to Convex storage. - * Note: This is kept for backwards compatibility but the recommended approach - * is to use the app's storage directly via exposeUploadApi(). - */ -export declare const generateUploadUrl: import("convex/server").RegisteredMutation<"public", {}, Promise>; -/** - * Record an asset in the database after uploading to storage. - * If an asset already exists at this path, returns the old storageId for cleanup. - * - * Note: Storage files are stored in the app's storage, not the component's storage. - * The caller is responsible for deleting the returned storageId from app storage. - */ -export declare const recordAsset: import("convex/server").RegisteredMutation<"public", { - blobId?: string | undefined; - storageId?: import("convex/values").GenericId<"_storage"> | undefined; - path: string; - contentType: string; - deploymentId: string; -}, Promise<{ - oldStorageId: import("convex/values").GenericId<"_storage"> | null; - oldBlobId: string | null; -}>>; -/** - * Garbage collect assets from old deployments. - * Returns the storageIds that need to be deleted from app storage. - */ -export declare const gcOldAssets: import("convex/server").RegisteredMutation<"public", { - currentDeploymentId: string; -}, Promise<{ - storageIds: Array>["type"]>; - blobIds: string[]; -}>>; -/** - * List all assets (useful for debugging). - */ -export declare const listAssets: import("convex/server").RegisteredQuery<"public", { - limit?: number | undefined; -}, Promise<{ - _id: import("convex/values").GenericId<"staticAssets">; - _creationTime: number; - blobId?: string | undefined; - storageId?: import("convex/values").GenericId<"_storage"> | undefined; - path: string; - contentType: string; - deploymentId: string; -}[]>>; -/** - * Delete all assets records (useful for cleanup). - * Returns storageIds that need to be deleted from app storage. - */ -export declare const deleteAllAssets: import("convex/server").RegisteredMutation<"internal", {}, Promise<{ - storageIds: Array>["type"]>; - blobIds: string[]; -}>>; -/** - * Get the current deployment info. - * Clients subscribe to this to detect when a new deployment happens. - */ -export declare const getCurrentDeployment: import("convex/server").RegisteredQuery<"public", {}, Promise<{ - _id: import("convex/values").GenericId<"deploymentInfo">; - _creationTime: number; - currentDeploymentId: string; - deployedAt: number; -} | null>>; -/** - * Update the current deployment ID. - * Called after a successful deployment to notify all connected clients. - */ -export declare const setCurrentDeployment: import("convex/server").RegisteredMutation<"public", { - deploymentId: string; -}, Promise>; -//# sourceMappingURL=lib.d.ts.map \ No newline at end of file From 9b8d5ee7697c69423fccc1607fbfec5d4d135aa8 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:37:10 -0700 Subject: [PATCH 27/29] recover things from rebase --- INTEGRATION.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/INTEGRATION.md b/INTEGRATION.md index 436964e..3af708c 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -409,6 +409,12 @@ npx @convex-dev/static-hosting deploy npm run build && npx @convex-dev/static-hosting upload --prod ``` +### "Cannot find module convex.config" +Make sure you've installed the package and it's listed in `package.json`: +```bash +npm install @convex-dev/static-hosting +``` + ### Component name mismatch If you've renamed the component instance with `app.use(staticHosting, { name: From a92979f18784f073be6ca21f60ae10b74ee9f0c8 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:37:21 -0700 Subject: [PATCH 28/29] call out cdnBaseUrl feature loss --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f30d9a..21f700f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ upgrading.** (`deploy` or `upload`) to make extension-less misses return 404 instead of `index.html`. The setting is stored on the deployment record, so it travels with the code you ship. +- The `cdnBaseUrl` override (from the removed `registerStaticRoutes`) is not + carried forward. In CDN mode, blob redirects always point at the deployment's + own `{origin}/fs/blobs`; pointing them at a separate CDN host is no longer + configurable. File an issue if you need it back. ## 0.1.4 From 3b261a9dcab37082bc8ce6a738d4da592e9acec9 Mon Sep 17 00:00:00 2001 From: Ian Macartney <366683+ianmacartney@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:41:31 -0700 Subject: [PATCH 29/29] recover things from rebase --- README.md | 16 +--------------- src/cli/deploy.ts | 12 +----------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index aa23a7e..9c58231 100644 --- a/README.md +++ b/README.md @@ -135,24 +135,10 @@ For more control, you can run the two halves separately: ```bash npx convex deploy npx @convex-dev/static-hosting upload --build --prod - +``` Your app is live at `https://.convex.site`. -### Non-Vite bundlers - -The `--build` flag sets `VITE_CONVEX_URL` for your build. To use a different -env var (Expo, Next.js, etc.), wrap your build script so the value passes -through: - -```json -// Expo -"build": "EXPO_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$EXPO_PUBLIC_CONVEX_URL} npx expo export --platform web" - -// Next.js -"build": "NEXT_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$NEXT_PUBLIC_CONVEX_URL} next build" -``` - ### CLI options ```bash diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index bf0833a..61b2cf1 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -81,9 +81,7 @@ Minimizes the inconsistency window between backend and frontend updates. Options: -d, --dist Path to dist directory (default: ./dist) - -c, --component Module name where upload API is exposed — i.e. - convex/.ts (default: staticHosting). Not - the registered component name from convex.config.ts. + -c, --component Convex component name (default: staticHosting) --skip-build Skip the build step (use existing dist) --skip-convex Skip Convex backend deployment --build-command Build command to run (default: 'npm run build') @@ -147,14 +145,6 @@ function fetchUrls(componentName: string): DeploymentUrls { return urls; } -function getConvexProdSiteUrl(): string | null { - try { - return runConvex(["env", "get", "CONVEX_SITE_URL", "--prod"]) || null; - } catch { - return null; - } -} - /** * Run the Convex storage upload flow */