Skip to content
Closed
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
ee3a64e
Bring over RR cookies code into @remix-run/cookies package
brophdawg11 Oct 22, 2025
a10d448
Update build and exports
brophdawg11 Oct 22, 2025
f4a569b
Rename @remix-run/cookies -> @remix-run/cookie
brophdawg11 Oct 22, 2025
0fdf0f1
Reorder package.json
brophdawg11 Oct 22, 2025
9defa4d
Add README
brophdawg11 Oct 22, 2025
b2b44af
Updates
brophdawg11 Oct 22, 2025
74f2015
Remove stale links
brophdawg11 Oct 22, 2025
f54a3c1
Update copyright/author to Shopify/[email protected]
brophdawg11 Oct 23, 2025
ff73c5a
bundle cookie dep and move to a devDependency
brophdawg11 Oct 23, 2025
8c3a2be
PR feedback
brophdawg11 Oct 23, 2025
e8ac562
Remove space from license
brophdawg11 Oct 23, 2025
bf7b4a1
Refactor createCookie() -> new Cookie()
brophdawg11 Oct 29, 2025
3029560
Convert any's to unknown's
brophdawg11 Oct 29, 2025
e9134a0
Revert package.json author and license changes - to be done in main
brophdawg11 Oct 29, 2025
8652d7b
Remove stale code block from README
brophdawg11 Oct 29, 2025
80d6485
Add @remix-run/session package
brophdawg11 Oct 22, 2025
8d4d70c
Add README
brophdawg11 Oct 22, 2025
ad93a5d
Remove stale links
brophdawg11 Oct 22, 2025
32da854
Updates
brophdawg11 Oct 23, 2025
a51e480
Wire up automatic session management
brophdawg11 Oct 24, 2025
153abef
Automatic handling of session commit/destroy
brophdawg11 Oct 24, 2025
e8d31bf
Update bookstore to use built-in session
brophdawg11 Oct 24, 2025
b70e70f
Fix some type issues in tests
brophdawg11 Oct 24, 2025
83edda1
Fix lockfile
brophdawg11 Oct 24, 2025
76a38f5
Adopt NoOpSession approach
brophdawg11 Oct 26, 2025
1191d86
PR feedback
brophdawg11 Oct 28, 2025
9c9ed31
Convert session to a class, inline middleware logic
brophdawg11 Oct 28, 2025
62c9c25
Cleanup
brophdawg11 Oct 28, 2025
fe8c8ff
Fix up bookstore tests
brophdawg11 Oct 28, 2025
3dd1582
Add a few comments
brophdawg11 Oct 28, 2025
f9227a7
Support sub router sessions
brophdawg11 Oct 28, 2025
fac0731
Update to use new Cookie class API
brophdawg11 Oct 29, 2025
e4bb5a9
Bring over createFileSessionStorage
brophdawg11 Oct 29, 2025
976a06b
Fix lint issues
brophdawg11 Oct 29, 2025
b7cd2de
Move file storage to a sub export so it's not pulled in from an impor…
brophdawg11 Oct 29, 2025
acda0dc
Update bookstore demo session/cart handling
brophdawg11 Oct 29, 2025
2d87615
Fix tests based on new cart flow
brophdawg11 Oct 31, 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
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ MIT License

Copyright (c) 2024 Michael Jackson


Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand Down
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 { ensureCart } from './middleware/cart.ts'

export default {
use: [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 = cartId ? 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))
use: [ensureCart],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cart middleware that runs before all cart API handlers

handlers: {
async add({ session, 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(session.get('cartId')!, 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({ session, formData }) {
let bookId = formData.get('bookId')?.toString() ?? ''
let quantity = parseInt(formData.get('quantity')?.toString() ?? '1', 10)

let headers = new Headers()
setSessionCookie(headers, sessionId)
updateCartItem(session.get('cartId')!, 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({ session, 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(session.get('cartId')!, 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