Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d78f9c5
Bring over RR cookies code into @remix-run/cookies package
brophdawg11 Oct 22, 2025
7ec1d0f
Update build and exports
brophdawg11 Oct 22, 2025
84ee613
Rename @remix-run/cookies -> @remix-run/cookie
brophdawg11 Oct 22, 2025
a34c1da
Reorder package.json
brophdawg11 Oct 22, 2025
c076ab6
Add README
brophdawg11 Oct 22, 2025
21ac5f8
Updates
brophdawg11 Oct 22, 2025
8a930b9
Remove stale links
brophdawg11 Oct 22, 2025
e0e33c5
bundle cookie dep and move to a devDependency
brophdawg11 Oct 23, 2025
62a2355
PR feedback
brophdawg11 Oct 23, 2025
9d7d434
Refactor createCookie() -> new Cookie()
brophdawg11 Oct 29, 2025
9f0f508
Convert any's to unknown's
brophdawg11 Oct 29, 2025
b76dfdd
Remove stale code block from README
brophdawg11 Oct 29, 2025
6f1d7fb
Remove API docs from README
brophdawg11 Nov 3, 2025
b8db250
Remove JSON encoding from cookies
brophdawg11 Nov 3, 2025
852e17d
cookie changes
brophdawg11 Nov 3, 2025
2a0c91c
Add @remix-run/session package
brophdawg11 Nov 3, 2025
c0eb64c
Add session middleware to fetch-router
brophdawg11 Nov 3, 2025
50515e1
Update bookstore demo to use new session middleware
brophdawg11 Nov 3, 2025
3d1181f
Move from devDeps to deps in bookstore demo
brophdawg11 Nov 4, 2025
d8f2f3f
Move session storages to separate exports
brophdawg11 Nov 4, 2025
031f2eb
Change cookie dep from from workspace:^ to workspace:*
brophdawg11 Nov 4, 2025
9af9d97
Remove Session type generics
brophdawg11 Nov 4, 2025
e19df03
Update bookstore demo without session types
brophdawg11 Nov 4, 2025
30abf08
Remove redundant sesson 'new' status
brophdawg11 Nov 4, 2025
933929d
Update test
brophdawg11 Nov 4, 2025
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
8 changes: 4 additions & 4 deletions demos/bookstore/app/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ describe('account handlers', () => {
})

it('GET /account returns account page when authenticated', async () => {
let sessionId = await loginAsCustomer(router)
let sessionCookie = await loginAsCustomer(router)

// Now access account page with session
let request = requestWithSession('http://localhost:3000/account', sessionId)
let request = requestWithSession('http://localhost:3000/account', sessionCookie)
let response = await router.fetch(request)

assert.equal(response.status, 200)
Expand All @@ -27,10 +27,10 @@ describe('account handlers', () => {
})

it('GET /account/orders/:orderId shows order for authenticated user', async () => {
let sessionId = await loginAsCustomer(router)
let sessionCookie = await loginAsCustomer(router)

// Access existing order
let request = requestWithSession('http://localhost:3000/account/orders/1001', sessionId)
let request = requestWithSession('http://localhost:3000/account/orders/1001', sessionCookie)
let response = await router.fetch(request)

assert.equal(response.status, 200)
Expand Down
4 changes: 2 additions & 2 deletions demos/bookstore/app/admin.books.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { loginAsAdmin, requestWithSession } from '../test/helpers.ts'

describe('admin books handlers', () => {
it('POST /admin/books creates new book when admin', async () => {
let sessionId = await loginAsAdmin(router)
let sessionCookie = await loginAsAdmin(router)

// Create new book
let createRequest = requestWithSession('http://localhost:3000/admin/books', sessionId, {
let createRequest = requestWithSession('http://localhost:3000/admin/books', sessionCookie, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down
4 changes: 2 additions & 2 deletions demos/bookstore/app/admin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ describe('admin handlers', () => {
})

it('GET /admin returns 403 for non-admin users', async () => {
let sessionId = await loginAsCustomer(router)
let sessionCookie = await loginAsCustomer(router)

// Try to access admin
let request = requestWithSession('http://localhost:3000/admin', sessionId)
let request = requestWithSession('http://localhost:3000/admin', sessionCookie)
let response = await router.fetch(request)

assert.equal(response.status, 403)
Expand Down
10 changes: 5 additions & 5 deletions demos/bookstore/app/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { router } from './router.ts'
import { getSessionCookie, assertContains } from '../test/helpers.ts'
import { assertContains, getSessionCookie } from '../test/helpers.ts'

describe('auth handlers', () => {
it('POST /login with valid credentials sets session cookie and redirects', async () => {
Expand All @@ -18,8 +18,8 @@ describe('auth handlers', () => {
assert.equal(response.status, 302)
assert.equal(response.headers.get('Location'), '/account')

let sessionId = getSessionCookie(response)
assert.ok(sessionId, 'Expected session cookie to be set')
let sessionCookie = getSessionCookie(response)
assert.ok(sessionCookie, 'Expected session cookie to be set')
})

it('POST /login with invalid credentials returns 401', async () => {
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('auth handlers', () => {
assert.equal(response.status, 302)
assert.equal(response.headers.get('Location'), '/account')

let sessionId = getSessionCookie(response)
assert.ok(sessionId, 'Expected session cookie to be set')
let sessionCookie = getSessionCookie(response)
assert.ok(sessionCookie, 'Expected session cookie to be set')
})
})
27 changes: 9 additions & 18 deletions demos/bookstore/app/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { RouteHandlers } from '@remix-run/fetch-router'
import { redirect } from '@remix-run/fetch-router/response-helpers'

import { routes } from '../routes.ts'
import { getSession, setSessionCookie, login, logout } from './utils/session.ts'
import { login, logout } from './utils/session.ts'
import {
authenticateUser,
createUser,
Expand Down Expand Up @@ -64,7 +64,7 @@ export default {
)
},

async action({ request, formData }) {
async action({ session, formData }) {
let email = formData.get('email')?.toString() ?? ''
let password = formData.get('password')?.toString() ?? ''
let user = authenticateUser(email, password)
Expand All @@ -85,13 +85,9 @@ export default {
)
}

let session = getSession(request)
login(session.sessionId, user)
login(session, user)

let headers = new Headers()
setSessionCookie(headers, session.sessionId)

return redirect(routes.account.index.href(), { headers })
return redirect(routes.account.index.href())
},
},

Expand Down Expand Up @@ -136,7 +132,7 @@ export default {
)
},

async action({ request, formData }) {
async action({ session, formData }) {
let name = formData.get('name')?.toString() ?? ''
let email = formData.get('email')?.toString() ?? ''
let password = formData.get('password')?.toString() ?? ''
Expand Down Expand Up @@ -167,19 +163,14 @@ export default {

let user = createUser(email, password, name)

let session = getSession(request)
login(session.sessionId, user)

let headers = new Headers()
setSessionCookie(headers, session.sessionId)
login(session, user)

return redirect(routes.account.index.href(), { headers })
return redirect(routes.account.index.href())
},
},

logout({ request }) {
let session = getSession(request)
logout(session.sessionId)
logout({ session }) {
logout(session)

return redirect(routes.home.href())
},
Expand Down
52 changes: 38 additions & 14 deletions demos/bookstore/app/cart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import * as assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { router } from './router.ts'
import { getSessionCookie, requestWithSession, assertContains } from '../test/helpers.ts'
import {
requestWithSession,
assertContains,
loginAsCustomer,
assertNotContains,
getSessionCookie,
} from '../test/helpers.ts'

describe('cart handlers', () => {
it('POST /cart/api/add adds book to cart', async () => {
Expand All @@ -20,55 +26,73 @@ describe('cart handlers', () => {
})

it('GET /cart shows cart items', async () => {
let sessionCookie = await loginAsCustomer(router)

let request = requestWithSession('http://localhost:3000/cart', sessionCookie)
let response = await router.fetch(request)

assert.equal(response.status, 200)
let html = await response.text()
assertContains(html, 'Shopping Cart')
assertNotContains(html, 'Heavy Metal Guitar Riffs')

// First, add item to cart to get a session
let addResponse = await router.fetch('http://localhost:3000/cart/api/add', {
response = await router.fetch('http://localhost:3000/cart/api/add', {
method: 'POST',
body: new URLSearchParams({
bookId: '002',
slug: 'heavy-metal',
}),
headers: {
Cookie: sessionCookie,
},
redirect: 'manual',
})

let sessionId = getSessionCookie(addResponse)
assert.ok(sessionId)
sessionCookie = getSessionCookie(response)!

// Now view cart with session
let request = requestWithSession('http://localhost:3000/cart', sessionId)
let response = await router.fetch(request)
request = requestWithSession('http://localhost:3000/cart', sessionCookie)
response = await router.fetch(request)

assert.equal(response.status, 200)
let html = await response.text()
html = await response.text()
assertContains(html, 'Shopping Cart')
assertContains(html, 'Heavy Metal Guitar Riffs')
})

it('cart persists state across requests with same session', async () => {
let sessionCookie = await loginAsCustomer(router)

// Add first item
let addResponse1 = await router.fetch('http://localhost:3000/cart/api/add', {
let response = await router.fetch('http://localhost:3000/cart/api/add', {
method: 'POST',
body: new URLSearchParams({
bookId: '001',
slug: 'bbq',
}),
headers: {
Cookie: sessionCookie,
},
redirect: 'manual',
})

let sessionId = getSessionCookie(addResponse1)
assert.ok(sessionId)
sessionCookie = getSessionCookie(response)!

// Add second item with same session
let addRequest2 = requestWithSession('http://localhost:3000/cart/api/add', sessionId, {
let addRequest2 = requestWithSession('http://localhost:3000/cart/api/add', sessionCookie, {
method: 'POST',
body: new URLSearchParams({
bookId: '003',
slug: 'three-ways',
}),
headers: {
Cookie: sessionCookie,
},
redirect: 'manual',
})
await router.fetch(addRequest2)

// View cart - should have both items
let cartRequest = requestWithSession('http://localhost:3000/cart', sessionId)
let cartRequest = requestWithSession('http://localhost:3000/cart', sessionCookie)
let cartResponse = await router.fetch(cartRequest)

let html = await cartResponse.text()
Expand Down
95 changes: 43 additions & 52 deletions demos/bookstore/app/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import { redirect } from '@remix-run/fetch-router/response-helpers'
import { routes } from '../routes.ts'

import { Layout } from './layout.tsx'
import { loadAuth, SESSION_ID_KEY } from './middleware/auth.ts'
import { loadAuth } from './middleware/auth.ts'
import { getBookById } from './models/books.ts'
import { getCart, addToCart, updateCartItem, removeFromCart, getCartTotal } from './models/cart.ts'
import type { User } from './models/users.ts'
import { getCurrentUser, getStorage } from './utils/context.ts'
import { getCurrentUser } from './utils/context.ts'
import { render } from './utils/render.ts'
import { setSessionCookie } from './utils/session.ts'
import { RestfulForm } from './components/restful-form.tsx'
import { CART_ID_KEY, ensureCart } from './middleware/cart.ts'

export default {
middleware: [loadAuth],
handlers: {
index() {
let sessionId = getStorage().get(SESSION_ID_KEY)
let cart = getCart(sessionId)
let total = getCartTotal(cart)
index({ session }) {
let cartId = session.get('cartId')
let cart = typeof cartId === 'string' ? getCart(cartId) : null
let total = cart ? getCartTotal(cart) : 0

let user: User | null = null
try {
Expand All @@ -33,7 +33,7 @@ export default {
<h1>Shopping Cart</h1>

<div class="card">
{cart.items.length > 0 ? (
{cart && cart.items.length > 0 ? (
<>
<table>
<thead>
Expand Down Expand Up @@ -137,64 +137,55 @@ export default {
},

api: {
async add({ storage, formData }) {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 1000))
middleware: [ensureCart],
handlers: {
async add({ storage, formData }) {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 1000))

let sessionId = storage.get(SESSION_ID_KEY)
let bookId = formData.get('bookId')?.toString() ?? ''
let bookId = formData.get('bookId')?.toString() ?? ''

let book = getBookById(bookId)
if (!book) {
return new Response('Book not found', { status: 404 })
}
let book = getBookById(bookId)
if (!book) {
return new Response('Book not found', { status: 404 })
}

addToCart(sessionId, book.id, book.slug, book.title, book.price, 1)
addToCart(storage.get(CART_ID_KEY), book.id, book.slug, book.title, book.price, 1)

let headers = new Headers()
setSessionCookie(headers, sessionId)
if (formData.get('redirect') === 'none') {
return new Response(null, { status: 204 })
}

if (formData.get('redirect') === 'none') {
return new Response(null, { status: 204 })
}
return redirect(routes.cart.index.href())
},

return redirect(routes.cart.index.href(), { headers })
},

async update({ storage, formData }) {
let sessionId = storage.get(SESSION_ID_KEY)
let bookId = formData.get('bookId')?.toString() ?? ''
let quantity = parseInt(formData.get('quantity')?.toString() ?? '1', 10)

updateCartItem(sessionId, bookId, quantity)
async update({ storage, formData }) {
let bookId = formData.get('bookId')?.toString() ?? ''
let quantity = parseInt(formData.get('quantity')?.toString() ?? '1', 10)

let headers = new Headers()
setSessionCookie(headers, sessionId)
updateCartItem(storage.get(CART_ID_KEY), bookId, quantity)

if (formData.get('redirect') === 'none') {
return new Response(null, { status: 204 })
}

return redirect(routes.cart.index.href(), { headers })
},
if (formData.get('redirect') === 'none') {
return new Response(null, { status: 204 })
}

async remove({ storage, formData }) {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 1000))
return redirect(routes.cart.index.href())
},

let sessionId = storage.get(SESSION_ID_KEY)
let bookId = formData.get('bookId')?.toString() ?? ''
async remove({ storage, formData }) {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 1000))

removeFromCart(sessionId, bookId)
let bookId = formData.get('bookId')?.toString() ?? ''

let headers = new Headers()
setSessionCookie(headers, sessionId)
removeFromCart(storage.get(CART_ID_KEY), bookId)

if (formData.get('redirect') === 'none') {
return new Response(null, { status: 204 })
}
if (formData.get('redirect') === 'none') {
return new Response(null, { status: 204 })
}

return redirect(routes.cart.index.href(), { headers })
return redirect(routes.cart.index.href())
},
},
},
},
Expand Down
Loading