ugly-app 0.1.951 → 0.1.953
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -210
- package/dist/character/client/CharacterCanvas.d.ts.map +1 -1
- package/dist/character/client/CharacterCanvas.js +38 -3
- package/dist/character/client/CharacterCanvas.js.map +1 -1
- package/dist/character/client/CharacterCreator.d.ts +6 -10
- package/dist/character/client/CharacterCreator.d.ts.map +1 -1
- package/dist/character/client/CharacterCreator.js +60 -28
- package/dist/character/client/CharacterCreator.js.map +1 -1
- package/dist/character/client/index.d.ts +3 -1
- package/dist/character/client/index.d.ts.map +1 -1
- package/dist/character/client/index.js +1 -0
- package/dist/character/client/index.js.map +1 -1
- package/dist/character/client/lib/characterScene.d.ts +16 -1
- package/dist/character/client/lib/characterScene.d.ts.map +1 -1
- package/dist/character/client/lib/characterScene.js +52 -5
- package/dist/character/client/lib/characterScene.js.map +1 -1
- package/dist/character/client/lib/voiceCatalog.d.ts +54 -0
- package/dist/character/client/lib/voiceCatalog.d.ts.map +1 -0
- package/dist/character/client/lib/voiceCatalog.js +130 -0
- package/dist/character/client/lib/voiceCatalog.js.map +1 -0
- package/dist/character/shared/index.d.ts +1 -1
- package/dist/character/shared/index.d.ts.map +1 -1
- package/dist/character/shared/index.js +1 -1
- package/dist/character/shared/index.js.map +1 -1
- package/dist/character/shared/spec.d.ts.map +1 -1
- package/dist/character/shared/spec.js +3 -2
- package/dist/character/shared/spec.js.map +1 -1
- package/dist/character/shared/specAdapter.d.ts +23 -0
- package/dist/character/shared/specAdapter.d.ts.map +1 -1
- package/dist/character/shared/specAdapter.js +85 -2
- package/dist/character/shared/specAdapter.js.map +1 -1
- package/dist/cli/auth/cloudflareOauth.js +1 -1
- package/dist/cli/auth/cloudflareOauth.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +11 -1
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/audio/AudioPlayer.d.ts +0 -1
- package/dist/client/audio/AudioPlayer.d.ts.map +1 -1
- package/dist/client/audio/AudioPlayer.js +5 -13
- package/dist/client/audio/AudioPlayer.js.map +1 -1
- package/dist/client/audio/AudioRecorder.d.ts +0 -1
- package/dist/client/audio/AudioRecorder.d.ts.map +1 -1
- package/dist/client/audio/AudioRecorder.js +2 -8
- package/dist/client/audio/AudioRecorder.js.map +1 -1
- package/dist/client/audio/audioPlayer.worklet.js +3 -30
- package/dist/client/audio/audioPlayer.worklet.js.map +1 -1
- package/dist/client/audio/audioRecorder.worklet.js +3 -7
- package/dist/client/audio/audioRecorder.worklet.js.map +1 -1
- package/dist/client/callAI.d.ts +8 -0
- package/dist/client/callAI.d.ts.map +1 -1
- package/dist/client/callAI.js +8 -1
- package/dist/client/callAI.js.map +1 -1
- package/dist/server/App.d.ts.map +1 -1
- package/dist/server/App.js +1 -7
- package/dist/server/App.js.map +1 -1
- package/dist/shared/FrameworkRequests.d.ts +2 -2
- package/dist/vite/index.d.ts +0 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +0 -1
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/character/client/CharacterCanvas.tsx +39 -3
- package/src/character/client/CharacterCreator.tsx +154 -110
- package/src/character/client/index.ts +11 -1
- package/src/character/client/lib/characterScene.ts +54 -5
- package/src/character/client/lib/voiceCatalog.ts +171 -0
- package/src/character/shared/index.ts +1 -0
- package/src/character/shared/spec.ts +3 -2
- package/src/character/shared/specAdapter.ts +88 -2
- package/src/cli/auth/cloudflareOauth.ts +1 -1
- package/src/cli/build.ts +11 -1
- package/src/cli/version.ts +1 -1
- package/src/client/audio/AudioPlayer.ts +5 -25
- package/src/client/audio/AudioRecorder.ts +2 -20
- package/src/client/audio/audioPlayer.worklet.ts +6 -33
- package/src/client/audio/audioRecorder.worklet.ts +7 -11
- package/src/client/callAI.ts +11 -1
- package/src/server/App.ts +1 -7
- package/src/vite/index.ts +0 -6
- package/templates/vite.config.ts +2 -11
- package/dist/client/audio/workletLoader.d.ts +0 -19
- package/dist/client/audio/workletLoader.d.ts.map +0 -1
- package/dist/client/audio/workletLoader.js +0 -57
- package/dist/client/audio/workletLoader.js.map +0 -1
- package/src/client/audio/workletLoader.test.ts +0 -122
- package/src/client/audio/workletLoader.ts +0 -66
- package/src/client/audio/workletSync.test.ts +0 -43
- package/src/vite/audioWorkletsPlugin.test.ts +0 -45
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# ugly-app
|
|
2
2
|
|
|
3
|
-
A full-stack TypeScript framework for shipping production web apps.
|
|
4
|
-
with `npx ugly-app init my-app` and get an opinionated Node +
|
|
5
|
-
stack with type-safe RPC over WebSocket and HTTP,
|
|
6
|
-
built-in auth, AI generation, storage,
|
|
7
|
-
workflow.
|
|
3
|
+
A full-stack TypeScript framework for shipping production web apps.
|
|
4
|
+
Scaffold with `npx ugly-app init my-app` and get an opinionated Node +
|
|
5
|
+
React + Postgres stack with type-safe RPC over WebSocket and HTTP,
|
|
6
|
+
real-time doc subscriptions, built-in auth, AI generation, storage,
|
|
7
|
+
workers, cron, and a CLI for every workflow.
|
|
8
8
|
|
|
9
9
|
ugly-app is designed to run against [ugly.bot](https://ugly.bot), which
|
|
10
|
-
provides auth, infra (Postgres, Qdrant, NATS, S3-compatible object
|
|
11
|
-
AI provider keys, push, email, and deployment. Your app talks
|
|
12
|
-
through the project's dev tunnel and its `UGLY_BOT_TOKEN`.
|
|
10
|
+
provides auth, infra (Postgres, Qdrant, NATS, S3-compatible object
|
|
11
|
+
storage), AI provider keys, push, email, and deployment. Your app talks
|
|
12
|
+
to all of it through the project's dev tunnel and its `UGLY_BOT_TOKEN`.
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
npx ugly-app init my-app
|
|
@@ -27,7 +27,6 @@ npm run dev
|
|
|
27
27
|
| `ugly-app/server/adapter/workers` | Cloudflare Workers entry (Hono router + Durable Objects + Neon HTTP driver + R2). |
|
|
28
28
|
| `ugly-app/server/ssr` | SSR renderer used by the Workers adapter. |
|
|
29
29
|
| `ugly-app/conversation/{shared,server,client,engine}` | AI chat sessions with persisted history. |
|
|
30
|
-
| `ugly-app/character/{shared,server,client}` | `CharacterSpec` store + WebGPU `CharacterCanvas` / `CharacterCreator`. |
|
|
31
30
|
| `ugly-app/collab/{server,client}` | Yjs-based collaborative editing. |
|
|
32
31
|
| `ugly-app/markdown/{shared,client}` | Markdown rendering + editor. |
|
|
33
32
|
| `ugly-app/search/{shared,server}` | Search primitives. |
|
|
@@ -45,8 +44,8 @@ npm run dev
|
|
|
45
44
|
|
|
46
45
|
## Server — `createApp()`
|
|
47
46
|
|
|
48
|
-
Single server entry point. Returns an `App` that owns Express, the
|
|
49
|
-
server, the typed DB, and the RPC dispatcher.
|
|
47
|
+
Single server entry point. Returns an `App` that owns Express, the
|
|
48
|
+
WebSocket server, the typed DB, and the RPC dispatcher.
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
51
|
// server/index.ts
|
|
@@ -104,31 +103,29 @@ function createApp<
|
|
|
104
103
|
): App<CollectionMap<typeof BUILTIN_DEFS & Defs>, RegistryPages<R>>;
|
|
105
104
|
```
|
|
106
105
|
|
|
107
|
-
Passing `pages` in the registry (`{ requests, messages, pages }`)
|
|
108
|
-
`app.pushSend()` to a per-route typed API. `pages` is optional;
|
|
109
|
-
call `configurator.setPages()` still work — they just get
|
|
110
|
-
`PageRegistry` typing on `pushSend`.
|
|
106
|
+
Passing `pages` in the registry (`{ requests, messages, pages }`)
|
|
107
|
+
upgrades `app.pushSend()` to a per-route typed API. `pages` is optional;
|
|
108
|
+
apps that only call `configurator.setPages()` still work — they just get
|
|
109
|
+
the loose `PageRegistry` typing on `pushSend`.
|
|
111
110
|
|
|
112
111
|
### The returned `App`
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
| Member | Description |
|
|
118
|
-
|--------|-------------|
|
|
113
|
+
| Field | Description |
|
|
114
|
+
|-------|-------------|
|
|
119
115
|
| `start(port?)` | Start the server (default 3000; templates use 4321). |
|
|
120
|
-
| `db` | `TypedDB<Map>`
|
|
116
|
+
| `db` | The `TypedDB<Map>` (map inferred from your collections + framework built-ins). |
|
|
121
117
|
| `httpServer` | The underlying Node `http.Server`. |
|
|
122
118
|
| `wss` | The main `WebSocketServer` (path set via `setWsPath`, default `/rpc`). |
|
|
123
|
-
| `dispatch(name, input, userId)` |
|
|
119
|
+
| `dispatch(name, input, userId)` | Invoke a registered RPC handler programmatically (inherited from `AppRouter`). |
|
|
124
120
|
| `registerRoutes(fn)` | Mount additional Express routes after creation. |
|
|
125
|
-
| `pushSend(input)` | Typed push whose click-through target is a route from this app's `pages`.
|
|
121
|
+
| `pushSend(input)` | Typed push whose click-through target is a route from this app's `pages`. The framework mints a `https://ugly.bot/l/<code>` short link so the click-through is always absolute and dock-app-routable. |
|
|
126
122
|
|
|
127
|
-
Framework services start inside `app.start()`: schema
|
|
128
|
-
connection + KV buckets, data-proxy connection,
|
|
129
|
-
cleanup for log tables, console/error capture,
|
|
130
|
-
Postgres, NATS, storage, and AI clients
|
|
131
|
-
`DATABASE_URL` / `NATS_URL` /
|
|
123
|
+
Framework services start automatically inside `app.start()`: schema
|
|
124
|
+
drift check, NATS connection + KV buckets, data-proxy connection,
|
|
125
|
+
event-counter flush, TTL cleanup for log tables, console/error capture,
|
|
126
|
+
and ugly.bot log forwarding. Postgres, NATS, storage, and AI clients
|
|
127
|
+
are loaded **lazily** — a host without `DATABASE_URL` / `NATS_URL` /
|
|
128
|
+
`R2_BUCKET` doesn't pay their startup cost.
|
|
132
129
|
|
|
133
130
|
### `AppConfigurator`
|
|
134
131
|
|
|
@@ -159,8 +156,8 @@ Every method is optional; `setPages` is what mounts the SPA.
|
|
|
159
156
|
|
|
160
157
|
### Handler signatures
|
|
161
158
|
|
|
162
|
-
Handlers are plain async functions — no context object. Access state
|
|
163
|
-
captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, …).
|
|
159
|
+
Handlers are plain async functions — no context object. Access state
|
|
160
|
+
via captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, …).
|
|
164
161
|
|
|
165
162
|
```ts
|
|
166
163
|
// req() — public, userId may be null
|
|
@@ -172,37 +169,30 @@ getMe: async (userId: string, input) => { /* … */ }
|
|
|
172
169
|
|
|
173
170
|
### Built-in framework requests
|
|
174
171
|
|
|
175
|
-
`createApp` registers several framework handlers reachable from any
|
|
176
|
-
the normal RPC pipeline. App-provided handlers with the same
|
|
177
|
-
framework's defaults.
|
|
172
|
+
`createApp` registers several framework handlers reachable from any
|
|
173
|
+
client via the normal RPC pipeline. App-provided handlers with the same
|
|
174
|
+
name override the framework's defaults.
|
|
178
175
|
|
|
179
|
-
| Name |
|
|
180
|
-
|
|
176
|
+
| Name | Purpose |
|
|
177
|
+
|------|---------|
|
|
181
178
|
| `userGet` | Returns `{ userId, name, avatarUri }` for the given user (or caller). |
|
|
182
179
|
| `initSession` / `captureEvent` | Session + event logging tagged with experiment branches (public — no auth). |
|
|
183
|
-
| `textGen` | AI
|
|
184
|
-
| `
|
|
185
|
-
| `kagiSearch` / `kagiEnrichWeb` / `kagiEnrichNews` | Web search via ugly.bot. Rate-limited 20/60s. |
|
|
186
|
-
| `kagiSummarize` | URL / text summary. Rate-limited 10/60s. |
|
|
180
|
+
| `textGen` / `imageGen` | AI proxies — server-validated, billed through ugly.bot. |
|
|
181
|
+
| `kagiSearch` / `kagiSummarize` / `kagiEnrichWeb` / `kagiEnrichNews` | Web search via ugly.bot. |
|
|
187
182
|
| `uploadUrl` | Issues a presigned PUT for the `temp` bucket. |
|
|
188
|
-
| `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata.
|
|
189
|
-
| `feedbackReportCreateNoAuth` |
|
|
190
|
-
| `
|
|
191
|
-
| `perfSnapshotCaptureNoAuth` | Public perf snapshot capture. Rate-limited 60/60s. |
|
|
192
|
-
| `submitFeedbackBot` | Bot-persona feedback submission. |
|
|
193
|
-
| `feedbackReportResolve` | Admin resolve/decline. Rate-limited 120/60s. |
|
|
183
|
+
| `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata. |
|
|
184
|
+
| `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` / `perfSnapshotCaptureNoAuth` | Same-origin, public endpoints used by browser telemetry. |
|
|
185
|
+
| `submitFeedbackBot` / `feedbackReportResolve` | Bot-persona feedback submission; admin resolve/decline. |
|
|
194
186
|
| `adminGetPerfLogs` | Admin-only perf telemetry read. |
|
|
195
187
|
| `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()`. |
|
|
196
188
|
| `projectPlanList` / `projectPlanCreate` / `projectPlanUpdate` / `projectPlanDelete` | Project plan CRUD used by Studio. |
|
|
197
189
|
|
|
198
|
-
Add per-endpoint rate limits on your own handlers with `rateLimit: { max, window }` on the request definition (see below).
|
|
199
|
-
|
|
200
190
|
---
|
|
201
191
|
|
|
202
192
|
## Shared API definitions
|
|
203
193
|
|
|
204
|
-
`shared/` is consumed by both server and client. Keep all Zod schemas,
|
|
205
|
-
collections, and route declarations here.
|
|
194
|
+
`shared/` is consumed by both server and client. Keep all Zod schemas,
|
|
195
|
+
types, collections, and route declarations here.
|
|
206
196
|
|
|
207
197
|
### Requests (`shared/api.ts`)
|
|
208
198
|
|
|
@@ -232,8 +222,8 @@ export const requests = defineRequests({
|
|
|
232
222
|
```
|
|
233
223
|
|
|
234
224
|
Every request is reachable as **both** `socket.request(name, input)`
|
|
235
|
-
(WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported
|
|
236
|
-
Zod for convenience.
|
|
225
|
+
(WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported
|
|
226
|
+
from Zod for convenience.
|
|
237
227
|
|
|
238
228
|
### Collections (`shared/collections.ts`)
|
|
239
229
|
|
|
@@ -269,19 +259,20 @@ export const collections = defineCollections({
|
|
|
269
259
|
- `public` — allow unauthenticated client reads.
|
|
270
260
|
- `cascadeFrom` — parent collection: when the parent doc is deleted, docs in this collection are cascade-deleted.
|
|
271
261
|
- `trackKeys?` — fields usable as NATS routing keys for scoped `trackDocs` subscriptions.
|
|
272
|
-
- `getter?(ids)` — batch resolver for collections with no local table (e.g. `userPublic` resolves via ugly.bot).
|
|
262
|
+
- `getter?(ids)` — batch resolver for collections with no local table (framework pattern; e.g. `userPublic` resolves via ugly.bot).
|
|
273
263
|
- `skipReadValidation?` — opt out of per-row zod validation on reads for very hot collections. Writes are always validated. Global kill-switch: `UGLY_DB_VALIDATE_READS=0`.
|
|
274
264
|
- `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
|
|
275
265
|
- `vector?: { dimensions, metric?, filterable? }` — ANN index. Vector supplied out-of-band at write time (`setDoc(c, doc, { vec })`) — never stored in the doc JSON. Query with `getDocs(c, filter, { near })`.
|
|
276
266
|
|
|
277
267
|
All documents extend `DBObject`: `{ _id, version, created, updated }`.
|
|
278
|
-
Use `dbDefaults()` to stamp `version` / `created` / `updated` on
|
|
279
|
-
**Always generate `_id` with `nanoid()`** — never
|
|
280
|
-
`Date.now()`, or `Math.random()` (see
|
|
268
|
+
Use `dbDefaults()` to stamp `version` / `created` / `updated` on
|
|
269
|
+
inserts. **Always generate `_id` with `nanoid()`** — never
|
|
270
|
+
`crypto.randomUUID()`, `Date.now()`, or `Math.random()` (see
|
|
271
|
+
`CLAUDE.md`).
|
|
281
272
|
|
|
282
|
-
After schema changes, run `npm run db:schema-gen` then `npm run
|
|
283
|
-
The app refuses to start when drift is detected (set
|
|
284
|
-
only as a last resort).
|
|
273
|
+
After schema changes, run `npm run db:schema-gen` then `npm run
|
|
274
|
+
db:migrate`. The app refuses to start when drift is detected (set
|
|
275
|
+
`SCHEMA_CHECK_SKIP=true` only as a last resort).
|
|
285
276
|
|
|
286
277
|
---
|
|
287
278
|
|
|
@@ -304,20 +295,23 @@ export const pages = definePages({
|
|
|
304
295
|
export type AppPages = typeof pages;
|
|
305
296
|
```
|
|
306
297
|
|
|
307
|
-
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime
|
|
308
|
-
carrying `PageMeta` plus a phantom `_params` used only for
|
|
309
|
-
inference.
|
|
298
|
+
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime
|
|
299
|
+
object carrying `PageMeta` plus a phantom `_params` used only for
|
|
300
|
+
TypeScript inference.
|
|
301
|
+
|
|
302
|
+
**Options** (all optional):
|
|
310
303
|
|
|
311
|
-
- `auth` (default `true`) — protected route. When `isAuthenticated()` returns false on this route, the router synchronously renders the framework's `<AuthRoot>` in place of the page (Mode A → `<LoginPopup>`, Mode B → `<MagicLinkForm>` plus optional Google button). Apps cannot override this fallback.
|
|
312
|
-
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is silently
|
|
304
|
+
- `auth` (default `true`) — protected route. When `isAuthenticated()` returns false on this route, the router synchronously renders the framework's `<AuthRoot>` in place of the page (dispatches on `window.__UGLY_APP_AUTH_MODE__`: Mode A → `<LoginPopup>`, Mode B → `<MagicLinkForm>` plus optional Google button). Apps cannot override this fallback — the framework owns the login UX so no route can accidentally ship as auth-required with no way to log in.
|
|
305
|
+
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is silently dropped to `ssr: false` with a warning: the SSR document is served from a shared edge cache and must not depend on the viewer.
|
|
313
306
|
- `cacheQuery?: string[]` — query params that participate in the SSR edge-cache key. Anything NOT listed bypasses the cache instead of poisoning it (e.g. `cacheQuery: ['q']` on a search page).
|
|
314
|
-
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is
|
|
307
|
+
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is effectively 1 year (`DEFAULT_SSR_CACHE_TIMEOUT`) because the `buildId` is part of the cache key. Override only for pages that drift between deploys.
|
|
315
308
|
|
|
316
309
|
Path syntax: `:param` matches a single path segment; `*param` is greedy
|
|
317
|
-
(captures slashes). Query-string params are declared in `Params` but
|
|
318
|
-
appear in the path template.
|
|
310
|
+
(captures slashes). Query-string params are declared in `Params` but
|
|
311
|
+
never appear in the path template.
|
|
319
312
|
|
|
320
|
-
`definePages<T>(p)` is a pass-through identity — use it to
|
|
313
|
+
`definePages<T>(p)` is a pass-through identity — use it to give the
|
|
314
|
+
registry a name.
|
|
321
315
|
|
|
322
316
|
### `createRouter()` (`ugly-app/client`)
|
|
323
317
|
|
|
@@ -338,23 +332,23 @@ export const {
|
|
|
338
332
|
**Config:**
|
|
339
333
|
- `pages` (required) — the `PageRegistry` from `shared/pages.ts`.
|
|
340
334
|
- `allPages?` — the lazy route → element map (`PageMap<Pages>`). Prefer registering via `setAllPages()` from the browser entry when the app also server-renders — the Worker bundle has no code splitting, so a static import inlines every page (and its deps) into `worker.js`.
|
|
341
|
-
- `ssrPages
|
|
335
|
+
- `ssrPages?` — `Partial<{ [K]: ComponentType<Params> }>` of statically-imported components for `ssr: true` routes. Used by both the Worker's `renderToString` and the client's first hydration render so the initial paint resolves synchronously and never mismatches the server output.
|
|
342
336
|
|
|
343
337
|
**Returns:**
|
|
344
338
|
|
|
345
|
-
- **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated
|
|
346
|
-
- **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing
|
|
339
|
+
- **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated?`, `initialUrl?` (`{ pathname, search }`, required for SSR + hydration). Manages route state, browser history, and the popup layer. Runs the per-route auth check synchronously — apps cannot override the login fallback.
|
|
340
|
+
- **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing?`, `transitionComponent?` (a `React.ComponentType<RouterTransitionProps>` that replaces the default `ViewFlipper`), `renderPage?(state) => ReactElement` (sync alternative to `allPages` loaders, called with `RouterStateRaw`).
|
|
347
341
|
- **`useRouter()`** — returns `RouterContextValue<Pages>` (typed navigation + popup API). Throws if used outside `<RouterProvider>`.
|
|
348
342
|
- **`Link`** — typed SPA link bound to this router. Renders a real `<a>` (right-click / cmd-click / SEO keep working) but intercepts a plain left-click and routes through `push` / `replace`. Props: `to`, `params` (both type-checked against `pages`), `replace?`, `children`, plus common anchor attrs (`className`, `style`, `target`, `aria-label`, `data-id`, `onClick`).
|
|
349
343
|
- **`setAllPages(map)`** — register the lazy `PageMap<Pages>` after construction (typically from the browser entry, so the Worker's import graph stays free of every lazy page).
|
|
350
344
|
|
|
351
|
-
A standalone `Link` is also exported from `ugly-app/client` for code
|
|
352
|
-
can't easily reach the typed one; it accepts an optional `router`
|
|
353
|
-
falls back to `<RouterProvider>` context.
|
|
345
|
+
A standalone `Link` is also exported from `ugly-app/client` for code
|
|
346
|
+
that can't easily reach the typed one; it accepts an optional `router`
|
|
347
|
+
prop and falls back to `<RouterProvider>` context.
|
|
354
348
|
|
|
355
|
-
**Never use a bare `<a href="/route">`** for internal navigation — it
|
|
356
|
-
a full document reload (white flash + repaint). See the "client
|
|
357
|
-
rule in `CLAUDE.md`.
|
|
349
|
+
**Never use a bare `<a href="/route">`** for internal navigation — it
|
|
350
|
+
triggers a full document reload (white flash + repaint). See the "client
|
|
351
|
+
navigation" rule in `CLAUDE.md`.
|
|
358
352
|
|
|
359
353
|
### Page map — `lazyPage` / `lazyPageLoader`
|
|
360
354
|
|
|
@@ -375,9 +369,10 @@ export const allPages = {
|
|
|
375
369
|
- **`lazyPageLoader(factory)`** — lazy-imports an async loader `(params) => Promise<ReactElement>`. Use when a route needs data fetching before render. The loader file is the chunk boundary, so it can statically import its page component.
|
|
376
370
|
|
|
377
371
|
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch
|
|
378
|
-
dynamically imported module"): on the first failure they trigger a
|
|
379
|
-
`window.location.reload()` (guarded by `sessionStorage` so a
|
|
380
|
-
chunk can't loop) to fetch fresh
|
|
372
|
+
dynamically imported module"): on the first failure they trigger a
|
|
373
|
+
single `window.location.reload()` (guarded by `sessionStorage` so a
|
|
374
|
+
genuinely broken chunk can't loop) to fetch the fresh `index.html` with
|
|
375
|
+
new chunk hashes.
|
|
381
376
|
|
|
382
377
|
```ts
|
|
383
378
|
// pages/SlowPageLoader.tsx
|
|
@@ -407,15 +402,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
|
|
|
407
402
|
back(); // browser history back
|
|
408
403
|
```
|
|
409
404
|
|
|
410
|
-
Route names and params are fully typed against `pages`. `push` /
|
|
411
|
-
no-op with a `console.error` when `buildUrl()` produces a URL
|
|
412
|
-
match a registered route.
|
|
405
|
+
Route names and params are fully typed against `pages`. `push` /
|
|
406
|
+
`replace` no-op with a `console.error` when `buildUrl()` produces a URL
|
|
407
|
+
that doesn't match a registered route.
|
|
413
408
|
|
|
414
409
|
### Popups — `openPopup()`
|
|
415
410
|
|
|
416
|
-
`useRouter().openPopup()` is the canonical modal / sheet / menu API.
|
|
417
|
-
router owns the popup layer, drives a spring animation, and stacks
|
|
418
|
-
z-index-correctly above every page.
|
|
411
|
+
`useRouter().openPopup()` is the canonical modal / sheet / menu API.
|
|
412
|
+
The router owns the popup layer, drives a spring animation, and stacks
|
|
413
|
+
popups z-index-correctly above every page.
|
|
419
414
|
|
|
420
415
|
```tsx
|
|
421
416
|
const { openPopup } = useRouter();
|
|
@@ -440,20 +435,20 @@ handle.hide(); // dismiss programmatically — same as router.closePopup(handle.
|
|
|
440
435
|
- **`contextMenu`** — same as transient, intended for menus and pickers.
|
|
441
436
|
|
|
442
437
|
`renderLayer` receives `{ content, spring, hide }`: `spring` is a
|
|
443
|
-
`createAnimatedValue()` result driving 0 → 1; `hide` closes the popup.
|
|
444
|
-
default layer animates open with `easeOut` (250 ms) and closed with
|
|
445
|
-
(200 ms). Popups render as siblings of the router's children
|
|
446
|
-
`RouterProvider`), so they stack above every page.
|
|
438
|
+
`createAnimatedValue()` result driving 0 → 1; `hide` closes the popup.
|
|
439
|
+
The default layer animates open with `easeOut` (250 ms) and closed with
|
|
440
|
+
`easeIn` (200 ms). Popups render as siblings of the router's children
|
|
441
|
+
(managed inside `RouterProvider`), so they stack above every page.
|
|
447
442
|
|
|
448
443
|
### Scroll containers
|
|
449
444
|
|
|
450
445
|
`html, body, #root` are `overflow: hidden` — the document itself never
|
|
451
|
-
scrolls, so a page that just renders tall content is clipped. For pages
|
|
452
|
-
own their scrolling (or render outside the normal authed chrome —
|
|
453
|
-
public page special-cased before the auth gate), wrap the
|
|
454
|
-
`SimpleScrollView` (exported from `ugly-app/client`). For
|
|
455
|
-
lists with scroll-position persistence, use the
|
|
456
|
-
"page scrolling" rule in `CLAUDE.md`.
|
|
446
|
+
scrolls, so a page that just renders tall content is clipped. For pages
|
|
447
|
+
that own their scrolling (or render outside the normal authed chrome —
|
|
448
|
+
e.g. a public page special-cased before the auth gate), wrap the
|
|
449
|
+
content in `SimpleScrollView` (exported from `ugly-app/client`). For
|
|
450
|
+
long / virtualized lists with scroll-position persistence, use the
|
|
451
|
+
richer `ScrollView`. See the "page scrolling" rule in `CLAUDE.md`.
|
|
457
452
|
|
|
458
453
|
---
|
|
459
454
|
|
|
@@ -476,9 +471,6 @@ bootstrapApp({
|
|
|
476
471
|
});
|
|
477
472
|
```
|
|
478
473
|
|
|
479
|
-
`bootstrapApp(options): void` — returns nothing; it renders directly to the
|
|
480
|
-
DOM.
|
|
481
|
-
|
|
482
474
|
**`BootstrapAppOptions`:**
|
|
483
475
|
|
|
484
476
|
| Field | Description |
|
|
@@ -496,28 +488,28 @@ DOM.
|
|
|
496
488
|
| `silentSso?` | Apex-domain apps that use ugly.bot as their auth authority but don't share its cookie. When `true`, a logged-out boot attempts a top-level SSO redirect (once per tab) to adopt an existing ugly.bot session. Leave unset for `*.ugly.bot` subdomains and Mode B apps. |
|
|
497
489
|
| `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
|
|
498
490
|
|
|
499
|
-
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server
|
|
500
|
-
cookie verification). If absent, it renders unauthenticated
|
|
501
|
-
router's per-route auth guard surfaces `<AuthRoot>`
|
|
502
|
-
protected route. If the token is present, it
|
|
503
|
-
`<AppProvider>`, and renders.
|
|
491
|
+
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server
|
|
492
|
+
after cookie verification). If absent, it renders unauthenticated
|
|
493
|
+
immediately — the router's per-route auth guard surfaces `<AuthRoot>`
|
|
494
|
+
only when the user opens a protected route. If the token is present, it
|
|
495
|
+
connects the socket, mounts `<AppProvider>`, and renders.
|
|
504
496
|
|
|
505
497
|
After render, a hidden iframe silently calls
|
|
506
|
-
`${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie
|
|
507
|
-
refreshed via `POST /auth/verify`; if the returned account differs
|
|
508
|
-
current session, the page reloads onto the live account
|
|
509
|
-
from→to pair to prevent loops). Any
|
|
510
|
-
(redirect-fallback OAuth code from
|
|
511
|
-
bootstrap before anything renders.
|
|
498
|
+
`${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie
|
|
499
|
+
is refreshed via `POST /auth/verify`; if the returned account differs
|
|
500
|
+
from the current session, the page reloads onto the live account
|
|
501
|
+
(guarded once per from→to pair to prevent loops). Any
|
|
502
|
+
`?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from
|
|
503
|
+
ugly.bot) is redeemed at the top of bootstrap before anything renders.
|
|
512
504
|
|
|
513
|
-
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it
|
|
514
|
-
`<MagicLinkCallback>` instead of the regular app shell.
|
|
505
|
+
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it
|
|
506
|
+
renders `<MagicLinkCallback>` instead of the regular app shell.
|
|
515
507
|
|
|
516
508
|
### `AppProvider` & `useApp()`
|
|
517
509
|
|
|
518
|
-
`bootstrapApp` mounts `<AppProvider>` automatically after socket
|
|
519
|
-
`useApp()` inside any page to access the active user,
|
|
520
|
-
services.
|
|
510
|
+
`bootstrapApp` mounts `<AppProvider>` automatically after socket
|
|
511
|
+
connect. Use `useApp()` inside any page to access the active user,
|
|
512
|
+
socket, and app-scoped services.
|
|
521
513
|
|
|
522
514
|
```ts
|
|
523
515
|
const {
|
|
@@ -525,26 +517,28 @@ const {
|
|
|
525
517
|
user, // UserBase doc
|
|
526
518
|
socket, // AppSocket — typed RPC client
|
|
527
519
|
uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS
|
|
528
|
-
showPopup, //
|
|
529
|
-
hidePopup,
|
|
530
|
-
hideAllPopups,
|
|
520
|
+
showPopup, // flat overlay layer (see note) — returns popup id
|
|
521
|
+
hidePopup,
|
|
522
|
+
hideAllPopups,
|
|
531
523
|
runAsync, // (label, async () => {…}, options?) — shows loading overlay
|
|
532
|
-
splashDone, // (step: string)
|
|
524
|
+
splashDone, // (step: string) — mark a splash-screen step complete
|
|
533
525
|
localizer, // (key, params?) => string — alias for useLocalizer()
|
|
534
526
|
} = useApp();
|
|
535
527
|
```
|
|
536
528
|
|
|
537
|
-
`useApp<TAsyncOptions>()` is generic in the async-options type so apps
|
|
538
|
-
pass a custom option through to a custom `loadingOverlay` element.
|
|
539
|
-
`
|
|
540
|
-
`
|
|
541
|
-
provider; `useLocalizer()`
|
|
542
|
-
the `AppProvider` `localizer`
|
|
529
|
+
`useApp<TAsyncOptions>()` is generic in the async-options type so apps
|
|
530
|
+
can pass a custom option through to a custom `loadingOverlay` element.
|
|
531
|
+
`AppProvider` `cloneElement`s the overlay with `{ label, asyncOptions
|
|
532
|
+
}` while `runAsync` is pending. `useAppOptional()` returns `null`
|
|
533
|
+
outside the provider; `useLocalizer()` returns a localizer that prefers
|
|
534
|
+
`<StringsProvider>` data, falls back to the `AppProvider` `localizer`
|
|
535
|
+
prop, and finally to identity.
|
|
543
536
|
|
|
544
537
|
Two popup APIs coexist: `useRouter().openPopup()` (spring-animated,
|
|
545
|
-
dismissible, mode-aware — **the default choice**) and
|
|
546
|
-
(a flat overlay layer returning a string id, for
|
|
547
|
-
*outside* the router transition). Prefer the
|
|
538
|
+
dismissible, mode-aware — **the default choice**) and
|
|
539
|
+
`useApp().showPopup()` (a flat overlay layer returning a string id, for
|
|
540
|
+
content that must sit *outside* the router transition). Prefer the
|
|
541
|
+
router's.
|
|
548
542
|
|
|
549
543
|
### Direct socket access
|
|
550
544
|
|
|
@@ -571,14 +565,14 @@ from `ugly-app/client`.
|
|
|
571
565
|
ugly-app uses HttpOnly cookies and server-side JWT injection — no
|
|
572
566
|
`localStorage`, no client-side token handling.
|
|
573
567
|
|
|
574
|
-
Two modes, picked from the `auth` block in the project's `.uglyapp`
|
|
568
|
+
Two modes, picked from the `auth` block in the project's `.uglyapp`
|
|
569
|
+
config:
|
|
575
570
|
|
|
576
571
|
- **Mode A — `mode: 'uglybot'`** (default when no `auth` block is present). Auth is delegated to ugly.bot OAuth. AI / email / push proxies bill the end user's ugly.bot credits.
|
|
577
572
|
- **Mode B — `mode: 'self'`**. The app issues its own sessions via magic-link email and/or Google OAuth. AI / email / push proxies bill the developer's ugly.bot account using the project's `AI_PROXY_TOKEN`.
|
|
578
573
|
|
|
579
|
-
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client
|
|
580
|
-
(incl. `<AuthRoot>`) can branch correctly.
|
|
581
|
-
`window.__UGLY_APP_GOOGLE_CLIENT_ID__` is also injected.
|
|
574
|
+
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client
|
|
575
|
+
code (incl. `<AuthRoot>`) can branch correctly.
|
|
582
576
|
|
|
583
577
|
### Mode A — ugly.bot OAuth (default)
|
|
584
578
|
|
|
@@ -590,9 +584,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
|
590
584
|
|
|
591
585
|
### Mode B — self-issued sessions
|
|
592
586
|
|
|
593
|
-
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the
|
|
594
|
-
provider as primary; if `providers.google.clientId` is
|
|
595
|
-
Google as an extra provider on the same router.
|
|
587
|
+
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the
|
|
588
|
+
magic-link provider as primary; if `providers.google.clientId` is
|
|
589
|
+
configured it adds Google as an extra provider on the same router.
|
|
596
590
|
|
|
597
591
|
- `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
|
|
598
592
|
- Background ugly.bot silent SSO is a no-op.
|
|
@@ -602,9 +596,9 @@ Google as an extra provider on the same router.
|
|
|
602
596
|
|
|
603
597
|
### Token-in-URL embed
|
|
604
598
|
|
|
605
|
-
Any GET request with `?token=<JWT>` will, if the token verifies, set
|
|
606
|
-
cookie and 302-redirect to the same URL without the token
|
|
607
|
-
for embedding any page in an iframe.
|
|
599
|
+
Any GET request with `?token=<JWT>` will, if the token verifies, set
|
|
600
|
+
the cookie and 302-redirect to the same URL without the token
|
|
601
|
+
parameter — useful for embedding any page in an iframe.
|
|
608
602
|
|
|
609
603
|
### Built-in auth routes
|
|
610
604
|
|
|
@@ -638,8 +632,8 @@ configurator.setAuth({
|
|
|
638
632
|
## Database — `TypedDB`
|
|
639
633
|
|
|
640
634
|
Access via `app.db`. All methods accept a `CollectionDef` (from
|
|
641
|
-
`defineCollections()`) or, on the `raw*` / `getQuery*` variants, a
|
|
642
|
-
collection name string.
|
|
635
|
+
`defineCollections()`) or, on the `raw*` / `getQuery*` variants, a
|
|
636
|
+
plain collection name string.
|
|
643
637
|
|
|
644
638
|
### Writing
|
|
645
639
|
|
|
@@ -664,10 +658,10 @@ await db.batch(async () => {
|
|
|
664
658
|
});
|
|
665
659
|
```
|
|
666
660
|
|
|
667
|
-
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`,
|
|
668
|
-
All keys are dot-notation, fully typed against the collection's
|
|
669
|
-
Partial updates go through optimistic concurrency to prevent
|
|
670
|
-
on concurrent writers.
|
|
661
|
+
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`,
|
|
662
|
+
`$set`. All keys are dot-notation, fully typed against the collection's
|
|
663
|
+
schema. Partial updates go through optimistic concurrency to prevent
|
|
664
|
+
lost updates on concurrent writers.
|
|
671
665
|
|
|
672
666
|
### Reading
|
|
673
667
|
|
|
@@ -701,8 +695,8 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
|
|
|
701
695
|
await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
|
|
702
696
|
```
|
|
703
697
|
|
|
704
|
-
Pass `deleteHandlers` as the 5th argument to `createApp` to run
|
|
705
|
-
`onDelete` callbacks.
|
|
698
|
+
Pass `deleteHandlers` as the 5th argument to `createApp` to run
|
|
699
|
+
per-collection `onDelete` callbacks.
|
|
706
700
|
|
|
707
701
|
### Search
|
|
708
702
|
|
|
@@ -751,9 +745,10 @@ Imports available from `ugly-app`:
|
|
|
751
745
|
|
|
752
746
|
## AI providers
|
|
753
747
|
|
|
754
|
-
AI calls are proxied through ugly.bot — your app never holds a
|
|
755
|
-
Pass `UGLY_BOT_TOKEN` in the environment and the
|
|
756
|
-
balance tracking, retries, and per-user
|
|
748
|
+
AI calls are proxied through ugly.bot — your app never holds a
|
|
749
|
+
provider key. Pass `UGLY_BOT_TOKEN` in the environment and the
|
|
750
|
+
framework handles routing, balance tracking, retries, and per-user
|
|
751
|
+
billing.
|
|
757
752
|
|
|
758
753
|
### Server-side text generation
|
|
759
754
|
|
|
@@ -775,9 +770,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
|
|
|
775
770
|
});
|
|
776
771
|
```
|
|
777
772
|
|
|
778
|
-
Available models are exposed via `textGenModels` / `textGenModelData`
|
|
779
|
-
`ugly-app` — the platform supports Claude, GPT, Gemini, Together,
|
|
780
|
-
Fireworks, Kimi, and Kie families.
|
|
773
|
+
Available models are exposed via `textGenModels` / `textGenModelData`
|
|
774
|
+
from `ugly-app` — the platform supports Claude, GPT, Gemini, Together,
|
|
775
|
+
Groq, Fireworks, Kimi, and Kie families.
|
|
781
776
|
|
|
782
777
|
### Server-side image generation
|
|
783
778
|
|
|
@@ -788,8 +783,8 @@ const imageGen = createImageGen(userId);
|
|
|
788
783
|
const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
|
|
789
784
|
```
|
|
790
785
|
|
|
791
|
-
`imageGenModels` / `imageGenModelData` enumerate available models
|
|
792
|
-
FLUX, FAL, Google Imagen, Wavespeed, Kie Kolors).
|
|
786
|
+
`imageGenModels` / `imageGenModelData` enumerate available models
|
|
787
|
+
(Together FLUX, FAL, Google Imagen, Wavespeed, Kie Kolors).
|
|
793
788
|
|
|
794
789
|
### Embeddings
|
|
795
790
|
|
|
@@ -814,8 +809,8 @@ await search.enrichNews({ query: 'topic' });
|
|
|
814
809
|
|
|
815
810
|
### Client-side AI calls
|
|
816
811
|
|
|
817
|
-
Calls from React components go through the framework RPC pipeline — no
|
|
818
|
-
plumbing in the browser:
|
|
812
|
+
Calls from React components go through the framework RPC pipeline — no
|
|
813
|
+
token plumbing in the browser:
|
|
819
814
|
|
|
820
815
|
```ts
|
|
821
816
|
import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
|
|
@@ -827,8 +822,9 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
|
|
|
827
822
|
|
|
828
823
|
### STT / TTS
|
|
829
824
|
|
|
830
|
-
Speech goes **directly** from the browser to ugly.bot — never proxied
|
|
831
|
-
your app server (see the "STT/TTS routes to ugly.bot" rule in
|
|
825
|
+
Speech goes **directly** from the browser to ugly.bot — never proxied
|
|
826
|
+
through your app server (see the "STT/TTS routes to ugly.bot" rule in
|
|
827
|
+
`CLAUDE.md`).
|
|
832
828
|
|
|
833
829
|
```ts
|
|
834
830
|
import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
|
|
@@ -862,23 +858,24 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
|
862
858
|
await storage.delete('public', destKey);
|
|
863
859
|
```
|
|
864
860
|
|
|
865
|
-
Client-side, use `socket.uploadFile(file, key)` — it requests a
|
|
866
|
-
via the built-in `uploadUrl` framework request and
|
|
867
|
-
dev, uploads go through a same-origin `/_s3`
|
|
868
|
-
MinIO.
|
|
861
|
+
Client-side, use `socket.uploadFile(file, key)` — it requests a
|
|
862
|
+
presigned URL via the built-in `uploadUrl` framework request and
|
|
863
|
+
streams the upload. In dev, uploads go through a same-origin `/_s3`
|
|
864
|
+
proxy to avoid CORS with local MinIO.
|
|
869
865
|
|
|
870
|
-
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for
|
|
871
|
-
isolation.
|
|
866
|
+
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for
|
|
867
|
+
per-environment isolation.
|
|
872
868
|
|
|
873
|
-
On Cloudflare Workers, storage uses the R2 binding directly; presigned
|
|
874
|
-
are not exposed (browser uploads must go through a Worker
|
|
869
|
+
On Cloudflare Workers, storage uses the R2 binding directly; presigned
|
|
870
|
+
PUTs are not exposed (browser uploads must go through a Worker
|
|
871
|
+
endpoint).
|
|
875
872
|
|
|
876
873
|
### Static assets — `static/`
|
|
877
874
|
|
|
878
875
|
Large, rarely-changing files (3D models, textures, audio, fonts) go in the
|
|
879
876
|
project-root `static/` folder. `ugly-app build:static` hashes them and
|
|
880
|
-
generates `shared/StaticAssets.ts`; publish uploads only the files whose
|
|
881
|
-
|
|
877
|
+
generates `shared/StaticAssets.ts`; publish uploads only the files whose bytes
|
|
878
|
+
changed.
|
|
882
879
|
|
|
883
880
|
```ts
|
|
884
881
|
import { staticUrl } from '../shared/StaticAssets'
|
|
@@ -888,14 +885,15 @@ const url = staticUrl('models/char2.ugm')
|
|
|
888
885
|
```
|
|
889
886
|
|
|
890
887
|
The bucket gets its own custom domain, so **Cloudflare's CDN serves these
|
|
891
|
-
directly** — no Worker invocation per request, and R2 answers `Range`
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
888
|
+
directly** — no Worker invocation per request, and R2 answers `Range` natively
|
|
889
|
+
for progressive loaders. (A Worker reading through the R2 binding bypasses the
|
|
890
|
+
edge cache, which is why these do not go through it.) Publish also sets the
|
|
891
|
+
bucket CORS policy and a `Cross-Origin-Resource-Policy: cross-origin` response
|
|
892
|
+
rule, without which COEP `require-corp` would silently block them.
|
|
895
893
|
|
|
896
894
|
Unlike `/{buildId}/assets/*`, these URLs do not change when you deploy, so
|
|
897
|
-
returning users do not re-download unchanged files. Assets that no build
|
|
898
|
-
|
|
895
|
+
returning users do not re-download unchanged files. Assets that no build from
|
|
896
|
+
the last `retentionDays` (default 7) references are deleted from R2
|
|
899
897
|
automatically, along with expired `builds/` prefixes.
|
|
900
898
|
|
|
901
899
|
Requires a provisioned app domain — publish fails rather than emitting URLs
|
|
@@ -905,8 +903,8 @@ nothing serves. Configure in `.uglyapp`:
|
|
|
905
903
|
"staticAssets": { "dir": "static", "retentionDays": 7 }
|
|
906
904
|
```
|
|
907
905
|
|
|
908
|
-
Dev serves the same files off disk at same-origin, so a newly added asset
|
|
909
|
-
|
|
906
|
+
Dev serves the same files off disk at same-origin, so a newly added asset works
|
|
907
|
+
before it has ever been published.
|
|
910
908
|
|
|
911
909
|
### `hostBundle` — serve the vite bundle from the CDN too
|
|
912
910
|
|
|
@@ -921,20 +919,19 @@ prefixed, a byte-identical chunk keeps its URL across deploys — so returning
|
|
|
921
919
|
users re-download only what actually changed, and each deploy uploads a
|
|
922
920
|
handful of KB instead of the whole bundle.
|
|
923
921
|
|
|
924
|
-
The first deploy only provisions; the bundle moves to the CDN on the next
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
**Workers are handled for you, but know why.** The bundle is then
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
same-origin.
|
|
922
|
+
The first deploy only provisions; the bundle moves to the CDN on the next one.
|
|
923
|
+
That is deliberate — a single pass would point the bundle at a hostname whose
|
|
924
|
+
TLS certificate may not have issued yet.
|
|
925
|
+
|
|
926
|
+
**Workers are handled for you, but know why.** The bundle is then cross-origin,
|
|
927
|
+
and `new Worker(url)` *rejects a cross-origin script URL outright* — that is
|
|
928
|
+
structural, and no CORS or CORP header lifts it. `bootstrapApp` installs a
|
|
929
|
+
shim that routes such URLs through a same-origin `blob:` importing the real
|
|
930
|
+
script, so `new Worker(new URL('./x.worker.ts', import.meta.url))` keeps
|
|
931
|
+
working. `SharedWorker` is **not** shimmed: every blob URL is unique, so the
|
|
932
|
+
same trick would give each caller a private worker and break sharing. Register
|
|
933
|
+
service workers by root-absolute path (`/sw.js`) — `register()` resolves
|
|
934
|
+
against the document, so those stay same-origin.
|
|
938
935
|
|
|
939
936
|
---
|
|
940
937
|
|
|
@@ -966,15 +963,16 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
|
|
|
966
963
|
configurator.setWorkers(cronTasks, cronHandlers);
|
|
967
964
|
```
|
|
968
965
|
|
|
969
|
-
Each worker can have `inputSchema`, `outputSchema`, `schedule`,
|
|
970
|
-
`description`. Workers without a schedule are still
|
|
971
|
-
`POST /_workers/run` (auth: localhost in dev,
|
|
972
|
-
in prod). Scheduled workers also
|
|
973
|
-
orchestrator.
|
|
966
|
+
Each worker can have `inputSchema`, `outputSchema`, `schedule`,
|
|
967
|
+
`timeout`, `description`. Workers without a schedule are still
|
|
968
|
+
invocable via `POST /_workers/run` (auth: localhost in dev,
|
|
969
|
+
`Authorization: Bearer $CRON_SECRET` in prod). Scheduled workers also
|
|
970
|
+
appear in `/_cron/manifest` for the deploy orchestrator.
|
|
974
971
|
|
|
975
|
-
On the Workers adapter, scheduled workers dispatch through Cloudflare
|
|
976
|
-
Triggers and durable queueing goes through Cloudflare Queues. On
|
|
977
|
-
adapter, `enqueueWorker` runs the handler inline (in-process
|
|
972
|
+
On the Workers adapter, scheduled workers dispatch through Cloudflare
|
|
973
|
+
Cron Triggers and durable queueing goes through Cloudflare Queues. On
|
|
974
|
+
the Node adapter, `enqueueWorker` runs the handler inline (in-process
|
|
975
|
+
queues only).
|
|
978
976
|
|
|
979
977
|
---
|
|
980
978
|
|
|
@@ -989,9 +987,9 @@ configurator.setStrings({
|
|
|
989
987
|
});
|
|
990
988
|
```
|
|
991
989
|
|
|
992
|
-
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`,
|
|
993
|
-
`window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()`
|
|
994
|
-
`useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
|
|
990
|
+
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`,
|
|
991
|
+
and `window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()`
|
|
992
|
+
/ `useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
|
|
995
993
|
|
|
996
994
|
---
|
|
997
995
|
|
|
@@ -1016,9 +1014,10 @@ export const experiments: Experiment[] = [
|
|
|
1016
1014
|
configurator.setExperiments(experiments);
|
|
1017
1015
|
```
|
|
1018
1016
|
|
|
1019
|
-
Bucketing is deterministic: `hash(experimentId + userId)` (or
|
|
1020
|
-
for unauthenticated users). The framework's `initSession`
|
|
1021
|
-
requests automatically tag events with the user's
|
|
1017
|
+
Bucketing is deterministic: `hash(experimentId + userId)` (or
|
|
1018
|
+
`sessionId` for unauthenticated users). The framework's `initSession`
|
|
1019
|
+
/ `captureEvent` requests automatically tag events with the user's
|
|
1020
|
+
branch assignments.
|
|
1022
1021
|
|
|
1023
1022
|
---
|
|
1024
1023
|
|
|
@@ -1044,15 +1043,17 @@ requests automatically tag events with the user's branch assignments.
|
|
|
1044
1043
|
|
|
1045
1044
|
## Two-adapter architecture
|
|
1046
1045
|
|
|
1047
|
-
The same developer source compiles for two runtimes via
|
|
1046
|
+
The same developer source compiles for two runtimes via
|
|
1047
|
+
`src/server/adapter/`:
|
|
1048
1048
|
|
|
1049
1049
|
- **Adapter A — Node + TCP** (`src/server/adapter/node/`): default for `npm run dev` and any non-Workers deploy. Wraps `pg.Pool`, `nats.js`, AWS S3 SDK.
|
|
1050
1050
|
- **Adapter B — Cloudflare Workers** (`src/server/adapter/workers/`): used when the Studio publish flow deploys to Cloudflare. Hono router + Durable Objects (one `CollectionDO` per project-collection, plus a `SessionDO` per user WS) + `@neondatabase/serverless` HTTP driver + R2 binding + Cloudflare Cron Triggers + Cloudflare Queues.
|
|
1051
1051
|
|
|
1052
|
-
Developer-facing APIs (`createTypedDB`, `subscribeDoc`,
|
|
1053
|
-
`setCronTasks`, `setWorkers`) don't change
|
|
1054
|
-
boots via `npm run dev:workers`,
|
|
1055
|
-
|
|
1052
|
+
Developer-facing APIs (`createTypedDB`, `subscribeDoc`,
|
|
1053
|
+
`createStorageClient`, `setCronTasks`, `setWorkers`) don't change
|
|
1054
|
+
between adapters. Local Workers dev boots via `npm run dev:workers`,
|
|
1055
|
+
which starts a small Node HTTP proxy speaking Neon's wire format so
|
|
1056
|
+
the Worker can talk to a real Postgres.
|
|
1056
1057
|
|
|
1057
1058
|
---
|
|
1058
1059
|
|
|
@@ -1107,8 +1108,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via
|
|
|
1107
1108
|
| `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
|
|
1108
1109
|
| `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
|
|
1109
1110
|
|
|
1110
|
-
Inside a scaffolded project, the same commands are available via `npm
|
|
1111
|
-
scripts — see `templates/CLAUDE.md`.
|
|
1111
|
+
Inside a scaffolded project, the same commands are available via `npm
|
|
1112
|
+
run …` scripts — see `templates/CLAUDE.md`.
|
|
1112
1113
|
|
|
1113
1114
|
---
|
|
1114
1115
|
|