Skip to content

Commit 23d4c6d

Browse files
committed
Merge branch 'main' into markdalgleish/compression-middleware
2 parents 85ede3b + 4980a73 commit 23d4c6d

File tree

96 files changed

+2292
-285
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+2292
-285
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
- **Imports**: Always use `import type { X }` for types (separate from value imports); use `export type { X }` for type exports; include `.ts` extensions
2323
- **Variables**: Prefer `let` for locals, `const` only at module scope; never use `var`
24+
- **Functions**: Use regular function declarations/expressions by default. Only use arrow functions as callbacks (e.g., route handlers, array methods) where preserving lexical `this` is beneficial or the syntax is more concise
2425
- **Classes**: Use native fields (omit `public`), `#private` for private members (no TypeScript accessibility modifiers)
2526
- **Formatting**: Prettier (printWidth: 100, no semicolons, single quotes, spaces not tabs)
2627
- **TypeScript**: Strict mode, ESNext target, ES2022 modules, bundler resolution, verbatimModuleSyntax

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,25 @@ The benefit is code that's not just reusable, but **future-proof**.
3636

3737
We currently publish the following packages:
3838

39+
- [async-context-middleware](packages/async-context-middleware): Middleware for storing request context in AsyncLocalStorage
3940
- [cookie](packages/cookie): A comprehensive toolkit for effortlessly managing HTTP cookies
4041
- [fetch-proxy](packages/fetch-proxy): Seamlessly construct HTTP proxies with the familiar `fetch()` API
4142
- [fetch-router](packages/fetch-router): A minimal, composable router built on the web Fetch API and `route-pattern`.
4243
- [file-storage](packages/file-storage): Robust key/value storage tailored for JavaScript `File` objects, simplifying file management
44+
- [form-data-middleware](packages/form-data-middleware): Middleware for parsing FormData from request bodies
4345
- [form-data-parser](packages/form-data-parser): An enhanced `request.formData()` wrapper enabling efficient, streaming file uploads
4446
- [headers](packages/headers): A comprehensive toolkit for effortlessly managing HTTP headers
4547
- [html-template](packages/html-template): A safe HTML template tag with auto-escaping for JavaScript
4648
- [interaction](packages/interaction): Semantic, declarative events and Interactions
4749
- [lazy-file](packages/lazy-file): Optimize performance with lazy-loaded, streaming `Blob`s and `File`s for JavaScript
50+
- [logger-middleware](packages/logger-middleware): Middleware for logging HTTP requests and responses
51+
- [method-override-middleware](packages/method-override-middleware): Middleware for overriding HTTP request methods from form data
4852
- [multipart-parser](packages/multipart-parser): High-performance, streaming parser for multipart messages, perfect for handling complex form data
4953
- [node-fetch-server](packages/node-fetch-server): Build Node.js HTTP servers using the web-standard `fetch()` API, promoting code consistency
5054
- [route-pattern](packages/route-pattern): A powerful and flexible URL pattern matching library
5155
- [session](packages/session): A full-featured session management library for JavaScript
56+
- [session-middleware](packages/session-middleware): Middleware for managing sessions with cookie-based storage
57+
- [static-middleware](packages/static-middleware): Middleware for serving static files from the filesystem
5258
- [tar-parser](packages/tar-parser): A fast, streaming parser for tar archives, designed for efficient data extraction
5359

5460
## Contributing

demos/bookstore/app/auth.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,48 @@ describe('auth handlers', () => {
4747
assertContains(html, 'Invalid email or password')
4848
})
4949

50+
it('flash error message is cleared after being displayed once', async () => {
51+
// POST invalid credentials to trigger flash message
52+
let response = await router.fetch('https://remix.run/login', {
53+
method: 'POST',
54+
body: new URLSearchParams({
55+
56+
password: 'wrongpassword',
57+
}),
58+
redirect: 'manual',
59+
})
60+
61+
assert.equal(response.status, 302)
62+
assert.equal(response.headers.get('Location'), '/login')
63+
64+
// Follow redirect to see the error message (first request)
65+
let sessionCookie = getSessionCookie(response)
66+
let firstFollowUp = await router.fetch('https://remix.run/login', {
67+
headers: {
68+
Cookie: `session=${sessionCookie}`,
69+
},
70+
})
71+
72+
let firstHtml = await firstFollowUp.text()
73+
assertContains(firstHtml, 'Invalid email or password')
74+
75+
// Get updated session cookie (session should be updated to clear flash)
76+
let updatedSessionCookie = getSessionCookie(firstFollowUp) || sessionCookie
77+
78+
// Refresh the page (second request) - error should NOT be shown
79+
let secondFollowUp = await router.fetch('https://remix.run/login', {
80+
headers: {
81+
Cookie: `session=${updatedSessionCookie}`,
82+
},
83+
})
84+
85+
let secondHtml = await secondFollowUp.text()
86+
assert.ok(
87+
!secondHtml.includes('Invalid email or password'),
88+
'Expected flash error to be cleared after first display',
89+
)
90+
})
91+
5092
it('POST /register creates new user and sets session', async () => {
5193
let uniqueEmail = `newuser-${Date.now()}@example.com`
5294

@@ -135,4 +177,71 @@ describe('auth handlers', () => {
135177
assertContains(html, 'Invalid email or password')
136178
assertContains(html, 'returnTo=' + encodeURIComponent('/checkout'))
137179
})
180+
181+
it('POST /reset-password with mismatched passwords redirects back with error', async () => {
182+
// First, request a password reset to get a token
183+
let forgotPasswordResponse = await router.fetch('https://remix.run/forgot-password', {
184+
method: 'POST',
185+
body: new URLSearchParams({
186+
187+
}),
188+
})
189+
190+
let html = await forgotPasswordResponse.text()
191+
// Extract token from the reset link in the demo response
192+
let tokenMatch = html.match(/\/reset-password\/([^"]+)/)
193+
assert.ok(tokenMatch, 'Expected to find reset token in response')
194+
let token = tokenMatch[1]
195+
196+
// Try to reset password with mismatched passwords
197+
let response = await router.fetch(`https://remix.run/reset-password/${token}`, {
198+
method: 'POST',
199+
body: new URLSearchParams({
200+
password: 'newpassword123',
201+
confirmPassword: 'differentpassword',
202+
}),
203+
redirect: 'manual',
204+
})
205+
206+
assert.equal(response.status, 302)
207+
assert.equal(response.headers.get('Location'), `/reset-password/${token}`)
208+
209+
// Follow redirect to see the error message
210+
let sessionCookie = getSessionCookie(response)
211+
let followUpResponse = await router.fetch(`https://remix.run/reset-password/${token}`, {
212+
headers: {
213+
Cookie: `session=${sessionCookie}`,
214+
},
215+
})
216+
217+
let errorHtml = await followUpResponse.text()
218+
assertContains(errorHtml, 'Passwords do not match')
219+
})
220+
221+
it('POST /reset-password with invalid token redirects back with error', async () => {
222+
let invalidToken = 'invalid-token-12345'
223+
224+
let response = await router.fetch(`https://remix.run/reset-password/${invalidToken}`, {
225+
method: 'POST',
226+
body: new URLSearchParams({
227+
password: 'newpassword123',
228+
confirmPassword: 'newpassword123',
229+
}),
230+
redirect: 'manual',
231+
})
232+
233+
assert.equal(response.status, 302)
234+
assert.equal(response.headers.get('Location'), `/reset-password/${invalidToken}`)
235+
236+
// Follow redirect to see the error message
237+
let sessionCookie = getSessionCookie(response)
238+
let followUpResponse = await router.fetch(`https://remix.run/reset-password/${invalidToken}`, {
239+
headers: {
240+
Cookie: `session=${sessionCookie}`,
241+
},
242+
})
243+
244+
let errorHtml = await followUpResponse.text()
245+
assertContains(errorHtml, 'Invalid or expired reset token')
246+
})
138247
})

demos/bookstore/app/auth.tsx

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ export default {
7878
async action({ session, formData, url }) {
7979
let email = formData.get('email')?.toString() ?? ''
8080
let password = formData.get('password')?.toString() ?? ''
81-
let user = authenticateUser(email, password)
8281
let returnTo = url.searchParams.get('returnTo')
8382

83+
let user = authenticateUser(email, password)
8484
if (!user) {
8585
session.flash('error', 'Invalid email or password. Please try again.')
8686
return redirect(routes.auth.login.index.href(undefined, { returnTo }))
@@ -239,15 +239,22 @@ export default {
239239
},
240240

241241
resetPassword: {
242-
index({ params }) {
242+
index({ params, session }) {
243243
let token = params.token
244+
let error = session.get('error')
244245

245246
return render(
246247
<Document>
247248
<div class="card" style="max-width: 500px; margin: 2rem auto;">
248249
<h1>Reset Password</h1>
249250
<p>Enter your new password below.</p>
250251

252+
{typeof error === 'string' ? (
253+
<div class="alert alert-error" style="margin-bottom: 1.5rem;">
254+
{error}
255+
</div>
256+
) : null}
257+
251258
<form method="POST" action={routes.auth.resetPassword.action.href({ token })}>
252259
<div class="form-group">
253260
<label for="password">New Password</label>
@@ -280,45 +287,20 @@ export default {
280287
)
281288
},
282289

283-
async action({ formData, params }) {
290+
async action({ session, formData, params }) {
284291
let password = formData.get('password')?.toString() ?? ''
285292
let confirmPassword = formData.get('confirmPassword')?.toString() ?? ''
286293

287294
if (password !== confirmPassword) {
288-
return render(
289-
<Document>
290-
<div class="card" style="max-width: 500px; margin: 2rem auto;">
291-
<div class="alert alert-error">Passwords do not match.</div>
292-
<p>
293-
<a
294-
href={routes.auth.resetPassword.index.href({ token: params.token })}
295-
class="btn"
296-
>
297-
Try Again
298-
</a>
299-
</p>
300-
</div>
301-
</Document>,
302-
{ status: 400 },
303-
)
295+
session.flash('error', 'Passwords do not match.')
296+
return redirect(routes.auth.resetPassword.index.href({ token: params.token }))
304297
}
305298

306299
let success = resetPassword(params.token, password)
307300

308301
if (!success) {
309-
return render(
310-
<Document>
311-
<div class="card" style="max-width: 500px; margin: 2rem auto;">
312-
<div class="alert alert-error">Invalid or expired reset token.</div>
313-
<p>
314-
<a href={routes.auth.forgotPassword.index.href()} class="btn">
315-
Request New Link
316-
</a>
317-
</p>
318-
</div>
319-
</Document>,
320-
{ status: 400 },
321-
)
302+
session.flash('error', 'Invalid or expired reset token.')
303+
return redirect(routes.auth.resetPassword.index.href({ token: params.token }))
322304
}
323305

324306
return render(

demos/bookstore/app/router.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { createRouter } from '@remix-run/fetch-router'
2-
import { asyncContext } from '@remix-run/fetch-router/async-context-middleware'
2+
import { asyncContext } from '@remix-run/async-context-middleware'
33
import { compression } from '@remix-run/fetch-router/compression-middleware'
4-
import { formData } from '@remix-run/fetch-router/form-data-middleware'
5-
import { logger } from '@remix-run/fetch-router/logger-middleware'
6-
import { methodOverride } from '@remix-run/fetch-router/method-override-middleware'
7-
import { session } from '@remix-run/fetch-router/session-middleware'
8-
import { staticFiles } from '@remix-run/fetch-router/static-middleware'
4+
import { formData } from '@remix-run/form-data-middleware'
5+
import { logger } from '@remix-run/logger-middleware'
6+
import { methodOverride } from '@remix-run/method-override-middleware'
7+
import { session } from '@remix-run/session-middleware'
8+
import { staticFiles } from '@remix-run/static-middleware'
99

1010
import { routes } from '../routes.ts'
1111
import { sessionCookie, sessionStorage } from './utils/session.ts'
@@ -33,7 +33,6 @@ middleware.push(
3333
cacheControl: 'no-store, must-revalidate',
3434
etag: false,
3535
lastModified: false,
36-
acceptRanges: false,
3736
}),
3837
)
3938
middleware.push(formData({ uploadHandler }))

demos/bookstore/app/utils/context.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { createStorageKey } from '@remix-run/fetch-router'
2-
import { getContext } from '@remix-run/fetch-router/async-context-middleware'
2+
import { getContext } from '@remix-run/async-context-middleware'
33

4+
import { type Cart, getCart } from '../models/cart.ts'
45
import type { User } from '../models/users.ts'
5-
import type { Cart } from '../models/cart.ts'
6-
import { getCart } from '../models/cart.ts'
76

87
// Storage key for attaching user data to request context
98
let USER_KEY = createStorageKey<User>()

demos/bookstore/app/utils/uploads.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { dirname, resolve } from 'node:path'
22
import { fileURLToPath } from 'node:url'
3-
import type { FileUpload } from '@remix-run/fetch-router/form-data-middleware'
3+
import type { FileUpload } from '@remix-run/form-data-middleware'
44
import { LocalFileStorage } from '@remix-run/file-storage/local'
55

66
const __dirname = dirname(fileURLToPath(import.meta.url))

demos/bookstore/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
"private": true,
44
"type": "module",
55
"dependencies": {
6+
"@remix-run/async-context-middleware": "workspace:*",
67
"@remix-run/cookie": "workspace:*",
78
"@remix-run/dom": "jam",
89
"@remix-run/events": "jam",
910
"@remix-run/fetch-router": "workspace:*",
1011
"@remix-run/file-storage": "workspace:*",
12+
"@remix-run/form-data-middleware": "workspace:*",
1113
"@remix-run/headers": "workspace:*",
1214
"@remix-run/lazy-file": "workspace:*",
15+
"@remix-run/logger-middleware": "workspace:*",
16+
"@remix-run/method-override-middleware": "workspace:*",
1317
"@remix-run/node-fetch-server": "workspace:*",
14-
"@remix-run/session": "workspace:*"
18+
"@remix-run/session": "workspace:*",
19+
"@remix-run/session-middleware": "workspace:*",
20+
"@remix-run/static-middleware": "workspace:*"
1521
},
1622
"devDependencies": {
1723
"@types/node": "^24.6.0",

demos/sse/app/router.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createRouter } from '@remix-run/fetch-router'
22
import { compression } from '@remix-run/fetch-router/compression-middleware'
3-
import { logger } from '@remix-run/fetch-router/logger-middleware'
4-
import { staticFiles } from '@remix-run/fetch-router/static-middleware'
3+
import { logger } from '@remix-run/logger-middleware'
4+
import { staticFiles } from '@remix-run/static-middleware'
55

66
import { routes } from '../routes.ts'
77
import { home } from './home.tsx'
@@ -16,7 +16,7 @@ if (process.env.NODE_ENV === 'development') {
1616
middleware.push(compression())
1717
middleware.push(
1818
staticFiles('./public', {
19-
cacheControl: 'no-store, must-revalidate',
19+
cacheControl: 'no-store',
2020
etag: false,
2121
lastModified: false,
2222
}),

demos/sse/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"@remix-run/events": "jam",
88
"@remix-run/fetch-router": "workspace:*",
99
"@remix-run/interaction": "workspace:*",
10-
"@remix-run/node-fetch-server": "workspace:*"
10+
"@remix-run/logger-middleware": "workspace:*",
11+
"@remix-run/node-fetch-server": "workspace:*",
12+
"@remix-run/static-middleware": "workspace:*"
1113
},
1214
"devDependencies": {
1315
"@types/node": "^24.6.0",

0 commit comments

Comments
 (0)