Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5b39f2a
Add core SSR pipeline: renderToStream document, app-root clientEntry,…
cursoragent Jul 4, 2026
cbd90fe
Convert account, admin, and auth routes to SSR with shared loaders an…
cursoragent Jul 4, 2026
d8c9948
Add SSR perf layer (public data cache, per-request auth dedup, versio…
cursoragent Jul 4, 2026
2a5ef2d
Fix same-origin fetches in account secrets (placeholder-origin URLs b…
cursoragent Jul 4, 2026
03d69a4
Fix oxfmt formatting on ssr stub and admin-rbac strict-mode locator (…
cursoragent Jul 4, 2026
b7ecf6e
Bugbot fixes: scope SSR notFound to the rendered URL and silently rev…
cursoragent Jul 4, 2026
df85471
Address review feedback: gate hydration session refresh, guard-before…
cursoragent Jul 4, 2026
11b6160
Fix token selection navigation to notify the router and guard communi…
cursoragent Jul 4, 2026
bbd116e
Render community grid whenever listings are ready to avoid blank grid…
cursoragent Jul 4, 2026
8191cb0
Fix stale session cookie redirect loop
cursoragent Jul 4, 2026
54f1338
Show loading state when route URL diverges from loaded data to avoid …
cursoragent Jul 4, 2026
43f2238
Sync lastLoadedHref before navigate to avoid redundant token list ref…
cursoragent Jul 4, 2026
8648468
Embed OAuth authorize info in SSR to fix loading state
cursoragent Jul 4, 2026
52530dd
Fix OAuth authorize embed consumption on first render
cursoragent Jul 4, 2026
6eddb97
Spike: Remix Frame SSR + reload inside hydrated app stack
cursoragent Jul 4, 2026
abce83f
Generalize Remix frame plumbing with registry and guardrails
cursoragent Jul 4, 2026
b111ebd
Extend frame registry to match parameterized route patterns
cursoragent Jul 4, 2026
df87bbd
Convert community index grid to a named frame
cursoragent Jul 4, 2026
753805c
Convert community detail data region to a named frame
cursoragent Jul 4, 2026
ef72429
Add E2E coverage for community frame revalidation
cursoragent Jul 4, 2026
e8c5dc4
Make community detail frame E2E resilient to accumulated D1 state
cursoragent Jul 4, 2026
9b05ff8
Remove frame-spike scratch route now that community frames are the ex…
cursoragent Jul 4, 2026
2509562
Refresh session after username save so the nav updates immediately
cursoragent Jul 4, 2026
7f4c310
Only apply immutable asset caching when URLs are versioned by a real …
cursoragent Jul 4, 2026
1b9a5da
Pass through 304 asset revalidations instead of falling through to th…
cursoragent Jul 4, 2026
23e7756
Retry E2E D1 commands on SQLITE_BUSY lock contention
cursoragent Jul 4, 2026
653dd4e
Let the lint rule carry the frame-src enforcement instead of docs pro…
cursoragent Jul 4, 2026
b9945fd
Merge origin/main into cursor/remix-ssr-hydration-c537
cursoragent Jul 4, 2026
d932b1d
Merge branch 'cursor/remix-ssr-hydration-c537' into cursor/remix-fram…
cursoragent Jul 4, 2026
05d6c33
Guard detail shell against stale render and refetch loops
cursoragent Jul 4, 2026
6e7f63f
Dedupe in-flight shell fetches and revalidate shell with the frame
cursoragent Jul 4, 2026
ee3efac
Remix frames: server-rendered fragments with reload-based revalidatio…
kentcdodds Jul 4, 2026
d570a9c
Address Bugbot feedback: URL-driven token selection, dedupe loader da…
cursoragent Jul 4, 2026
66b383b
Merge branch 'cursor/remix-frames-revalidation-c537' into cursor/remi…
cursoragent Jul 4, 2026
cdc2d9e
Merge remote-tracking branch 'origin/main' into cursor/remix-ssr-hydr…
cursoragent Jul 4, 2026
53915ea
Guard admin route loads against duplicate in-flight fetches and error…
cursoragent Jul 4, 2026
292cd61
Show loading shell immediately on listing change and gate shell rende…
cursoragent Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/contributing/frames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Remix frames

Remix 3 `<Frame>` lets a page embed a server-rendered HTML fragment and reload
it without full navigation (`frame.reload()`). Kody routes frame fetches through
the same handler as the parent page so auth and user scoping stay consistent.

## Naming

- **`name`** (client): app-wide unique frame id passed to `<Frame name={...}>`.
Register it once in `frame-registry.ts` via `registerFrame(name, ...)`.
Duplicate names throw at module load.
- **`src`** (client): the page URL that serves the fragment. Always build with
`routes.<routeKey>.href(...)` (the `kody-custom/no-literal-frame-src` lint
rule enforces this).

## Registry and handlers

1. Add `packages/worker/src/app/frames/<name>.ts` calling `registerFrame` with
`routePathname: routes.<key>.href()` and a `render` function that returns
fragment HTML (usually `renderToString` of a server component).
2. Import the module from `frame-registrations.ts`.
3. In the route handler, before `renderAppPage`:

```ts
const frameResponse = await handleFrameRequest(request, env, routes.foo.href())
if (frameResponse) return frameResponse
```

`handleFrameRequest` checks `x-remix-target` (see `frame-constants.ts`). When
the header matches a frame registered for that pathname, it returns bare
fragment HTML with `Cache-Control: no-store`. Otherwise it returns `null` and
the handler falls through to the full page.

SSR inlines frames via the same registry: `ssr-render.tsx` calls
`resolveRegisteredFrameHtml` inside `resolveFrame`. Unknown targets throw during
SSR (fail loud in dev).

## Target header contract

- Constant: `REMIX_FRAME_TARGET_HEADER` (`x-remix-target`) in
`frame-constants.ts`.
- Client `entry.tsx` `resolveFrame` sets the header to the frame `name` when
fetching `src`.
- Server `handleFrameRequest` reads the header and selects the registered frame.

## Auth scoping

Frames that render user-scoped data must derive auth from the **same `Request`**
as the full page — use `loadResolvedRequestAuth`, `readAuthenticatedAppUser`, or
the same loader helpers the page handler uses. Never read user state from global
module variables.

## Client typed routes

Client code imports `routes` from `#app/routes.ts` (included in
`tsconfig-client.json`). The route table is isomorphic plain data from
`remix/routes`; esbuild bundles it via the root `package.json` `imports` map.

Example (`client/routes/community.tsx`):

```tsx
<Frame name={COMMUNITY_LISTINGS_TARGET} src={frameSrc} />
```

`COMMUNITY_LISTINGS_TARGET` lives in `community-frame-constants.ts` alongside
the server-side `registerFrame` call in `frames/community-listings.ts`.
2 changes: 1 addition & 1 deletion docs/contributing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ style, tests, MCP capabilities, and runtime architecture.
- [Optional Cloudflare offerings](./cloudflare-offerings.md)
- [Setup](./setup.md), [code style](./code-style.md),
[testing](./testing-principles.md)
- [Remix skills](./remix.md)
- [Remix skills](./remix.md), [frames](./frames.md)
- [Packages and manifests](./packages-and-manifests.md)
- [Community packages](./community-packages.md)
- [External package invocation API](./package-invocation-api.md)
Expand Down
4 changes: 3 additions & 1 deletion docs/contributing/remix.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ The repo-local skill lives at:

Load that skill before changing Remix routes, controllers, middleware, data
access, validation, auth, sessions, file uploads, server setup, UI components,
hydration, navigation, or tests.
hydration, navigation, frames, or tests.

For `<Frame>` partial reloads, see [frames](./frames.md).

`npx remix@next new <app>` copies this skill from the Remix CLI bootstrap
template. The CLI does not expose a standalone `remix skills install` command.
19 changes: 19 additions & 0 deletions e2e/admin-rbac.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ test('admin RBAC controls access, role assignment, and privacy boundaries', asyn
await expect(
page.getByRole('link', { name: 'Admin', exact: true }),
).toBeVisible()

await page.evaluate(() => {
;(window as Window & { __e2eMarker?: boolean }).__e2eMarker = true
})
await page.getByRole('link', { name: 'Admin', exact: true }).click()
await expect(page).toHaveURL(/\/admin\/users\/?$/)
await expect(page.getByRole('heading', { name: 'Admin users' })).toBeVisible()
// The first user is auto-selected, so the email can render in both the
// list and the detail panel — assert on the list entry specifically.
await expect(
page.getByRole('button', { name: adminUser.username }),
).toBeVisible()
await expect(page.getByText('Unable to load admin users.')).toHaveCount(0)
expect(
await page.evaluate(
() => (window as Window & { __e2eMarker?: boolean }).__e2eMarker,
),
).toBe(true)

await page.goto('/admin/users?pageSize=100')
await expect(page.getByRole('heading', { name: 'Admin users' })).toBeVisible()
await expect(page.getByText(memberUser.email)).toBeVisible()
Expand Down
123 changes: 123 additions & 0 deletions e2e/community-frames.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { expect, test } from '@playwright/test'
import { ensurePrimaryUserExists, primaryTestUser } from './auth-test-user.ts'
import {
seedCommunityForkInE2eDatabase,
seedCommunityListingInE2eDatabase,
updateCommunityListingDescriptionInE2eDatabase,
} from './d1-utils.ts'

const staleListing = {
listingId: 'e2e-frame-stale-listing',
name: '@kody/frame-stale-package',
description: 'Original description before back-navigation.',
tags: ['stale', 'frame'],
}

test('community listings frame reloads fresh data on back-navigation', async ({
page,
request,
}) => {
await ensurePrimaryUserExists(request)
await seedCommunityListingInE2eDatabase({
...staleListing,
ownerEmail: primaryTestUser.email,
})

await page.goto('/community')
await expect(
page.getByTestId(`community-listing-description-${staleListing.listingId}`),
).toHaveText(staleListing.description)

await page.evaluate(() => {
;(window as Window & { __e2eStaleMarker?: boolean }).__e2eStaleMarker = true
})

await page.getByRole('link', { name: staleListing.name }).click()
await expect(page).toHaveURL(
new RegExp(`/community/${staleListing.listingId}$`),
)

const updatedDescription = 'Updated description after detail visit.'
updateCommunityListingDescriptionInE2eDatabase({
listingId: staleListing.listingId,
description: updatedDescription,
})

const frameReload = page.waitForResponse(
(response) =>
response.request().headers()['x-remix-target'] === 'community-listings' &&
response.url().includes('/community') &&
response.status() === 200,
)

await page.getByRole('link', { name: 'Community packages' }).click()
await expect(page).toHaveURL(/\/community$/)
await frameReload

await expect(
page.getByTestId(`community-listing-description-${staleListing.listingId}`),
).toHaveText(updatedDescription)
expect(
await page.evaluate(
() =>
(window as Window & { __e2eStaleMarker?: boolean }).__e2eStaleMarker,
),
).toBe(true)
})

test('community detail frame reloads fresh fork counts on return navigation', async ({
page,
request,
}) => {
await ensurePrimaryUserExists(request)
await seedCommunityListingInE2eDatabase({
...staleListing,
listingId: 'e2e-frame-detail-stale-listing',
name: '@kody/frame-detail-stale-package',
ownerEmail: primaryTestUser.email,
})

const listingId = 'e2e-frame-detail-stale-listing'

await page.goto(`/community/${listingId}`)
const initialForkCount = Number(
(await page.getByTestId('community-detail-forks').textContent()) ?? '0',
)

await page.evaluate(() => {
;(window as Window & { __e2eDetailMarker?: boolean }).__e2eDetailMarker =
true
})

await page.getByRole('link', { name: 'Community packages' }).click()
await expect(page).toHaveURL(/\/community$/)

await seedCommunityForkInE2eDatabase({
listingId,
forkerEmail: primaryTestUser.email,
forkId: `fork-${listingId}-${Date.now()}`,
})

const frameReload = page.waitForResponse(
(response) =>
response.request().headers()['x-remix-target'] === 'community-detail' &&
response.url().includes(`/community/${listingId}`) &&
response.status() === 200,
)

await page
.getByRole('link', { name: '@kody/frame-detail-stale-package' })
.click()
await expect(page).toHaveURL(new RegExp(`/community/${listingId}$`))
await frameReload

await expect(page.getByTestId('community-detail-forks')).toHaveText(
String(initialForkCount + 1),
)
expect(
await page.evaluate(
() =>
(window as Window & { __e2eDetailMarker?: boolean }).__e2eDetailMarker,
),
).toBe(true)
})
Loading
Loading