remix 3.0.0-beta.3 → 3.0.0-beta.5
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/dist/ui/accordion/primitives.d.ts +2 -0
- package/dist/ui/accordion/primitives.d.ts.map +1 -0
- package/dist/ui/accordion/primitives.js +2 -0
- package/dist/ui/button.d.ts +1 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +1 -0
- package/dist/ui/checkbox.d.ts +3 -0
- package/dist/ui/checkbox.d.ts.map +1 -0
- package/dist/ui/checkbox.js +3 -0
- package/dist/ui/combobox/primitives.d.ts +2 -0
- package/dist/ui/combobox/primitives.d.ts.map +1 -0
- package/dist/ui/combobox/primitives.js +2 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/menu/primitives.d.ts +2 -0
- package/dist/ui/menu/primitives.d.ts.map +1 -0
- package/dist/ui/{glyph.js → menu/primitives.js} +1 -1
- package/dist/ui/radio.d.ts +3 -0
- package/dist/ui/radio.d.ts.map +1 -0
- package/dist/ui/radio.js +3 -0
- package/dist/ui/select/primitives.d.ts +2 -0
- package/dist/ui/select/primitives.d.ts.map +1 -0
- package/dist/ui/select/primitives.js +2 -0
- package/dist/ui/tabs/primitives.d.ts +2 -0
- package/dist/ui/tabs/primitives.d.ts.map +1 -0
- package/dist/ui/{theme.js → tabs/primitives.js} +1 -1
- package/dist/ui/tabs.d.ts +2 -0
- package/dist/ui/tabs.d.ts.map +1 -0
- package/{src/ui/glyph.ts → dist/ui/tabs.js} +1 -1
- package/dist/ui/toggle/primitives.d.ts +2 -0
- package/dist/ui/toggle/primitives.d.ts.map +1 -0
- package/dist/ui/toggle/primitives.js +2 -0
- package/dist/ui/toggle.d.ts +3 -0
- package/dist/ui/toggle.d.ts.map +1 -0
- package/dist/ui/toggle.js +3 -0
- package/package.json +76 -47
- package/src/assert/README.md +11 -4
- package/src/data-table-mysql/README.md +25 -0
- package/src/data-table-postgres/README.md +26 -0
- package/src/fetch-router/README.md +115 -35
- package/src/form-data-parser/README.md +4 -4
- package/src/mime/README.md +8 -1
- package/src/node-fetch-server/README.md +28 -2
- package/src/route-pattern/README.md +51 -6
- package/src/session/README.md +1 -1
- package/src/test/README.md +27 -0
- package/src/ui/README.md +50 -164
- package/src/ui/accordion/README.md +50 -14
- package/src/ui/accordion/primitives/README.md +202 -0
- package/src/ui/accordion/primitives.ts +2 -0
- package/src/ui/anchor/README.md +37 -2
- package/src/ui/breadcrumbs/README.md +4 -4
- package/src/ui/button/README.md +26 -26
- package/src/ui/button.ts +1 -0
- package/src/ui/checkbox/README.md +59 -0
- package/src/ui/checkbox.ts +3 -0
- package/src/ui/combobox/README.md +58 -9
- package/src/ui/combobox/primitives/README.md +194 -0
- package/src/ui/combobox/primitives.ts +2 -0
- package/src/ui/input/README.md +52 -0
- package/src/ui/input.ts +3 -0
- package/src/ui/listbox/README.md +9 -41
- package/src/ui/menu/README.md +55 -14
- package/src/ui/menu/primitives/README.md +161 -0
- package/{dist/ui/scroll-lock.js → src/ui/menu/primitives.ts} +1 -1
- package/src/ui/popover/README.md +20 -39
- package/src/ui/radio/README.md +53 -0
- package/src/ui/radio.ts +3 -0
- package/src/ui/select/README.md +29 -19
- package/src/ui/select/primitives/README.md +117 -0
- package/src/ui/select/primitives.ts +2 -0
- package/src/ui/tabs/README.md +141 -0
- package/src/ui/tabs/primitives/README.md +141 -0
- package/{dist/ui/separator.js → src/ui/tabs/primitives.ts} +1 -1
- package/src/ui/{theme.ts → tabs.ts} +1 -1
- package/src/ui/toggle/README.md +56 -0
- package/src/ui/toggle/primitives/README.md +56 -0
- package/src/ui/toggle/primitives.ts +2 -0
- package/src/ui/toggle.ts +3 -0
- package/dist/ui/glyph.d.ts +0 -2
- package/dist/ui/glyph.d.ts.map +0 -1
- package/dist/ui/scroll-lock.d.ts +0 -2
- package/dist/ui/scroll-lock.d.ts.map +0 -1
- package/dist/ui/separator.d.ts +0 -2
- package/dist/ui/separator.d.ts.map +0 -1
- package/dist/ui/theme.d.ts +0 -2
- package/dist/ui/theme.d.ts.map +0 -1
- package/src/ui/glyph/README.md +0 -72
- package/src/ui/scroll-lock/README.md +0 -33
- package/src/ui/scroll-lock.ts +0 -2
- package/src/ui/separator.ts +0 -2
- package/src/ui/theme/README.md +0 -103
|
@@ -8,7 +8,7 @@ A minimal, composable router built on the [web Fetch API](https://developer.mozi
|
|
|
8
8
|
- **Type-Safe Routing**: Leverage TypeScript for compile-time route validation and parameter inference
|
|
9
9
|
- **Typed Request Context**: Carry request-scoped context through routers, controllers, and actions
|
|
10
10
|
- **Declarative Route Maps**: Define your route structure upfront with type-safe route names and request methods
|
|
11
|
-
- **Flexible Middleware**:
|
|
11
|
+
- **Flexible Middleware**: Use router, controller, and action middleware for each request boundary
|
|
12
12
|
- **Easy Testing**: Use standard `fetch()` to test your routes - no special test harness required
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
@@ -237,6 +237,85 @@ router.map(routes.contact, {
|
|
|
237
237
|
})
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
### Composing Route Groups
|
|
241
|
+
|
|
242
|
+
As applications grow, it is useful to let one file own the routes for a specific area of the app while the top-level router decides where that area is mounted. Use `router.mount()` with a route installer to register a group of routes under a route pattern prefix.
|
|
243
|
+
|
|
244
|
+
A route installer receives a `RouteBuilder`, not a full `Router`, so it can register routes but cannot dispatch requests. The parent router remains the only router that owns request dispatch, the matcher, and the default handler.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { createRouter, type RouteBuilder } from 'remix/router'
|
|
248
|
+
import { get, route } from 'remix/routes'
|
|
249
|
+
|
|
250
|
+
const adminRouteDefs = {
|
|
251
|
+
index: get('/'),
|
|
252
|
+
users: {
|
|
253
|
+
show: get('/users/:id'),
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Use relative route groups inside installers. These routes are registered below
|
|
258
|
+
// the mount prefix when `installAdminRoutes()` runs.
|
|
259
|
+
const adminRouteGroup = route(adminRouteDefs)
|
|
260
|
+
|
|
261
|
+
// Use full app routes for links and redirects.
|
|
262
|
+
const routes = {
|
|
263
|
+
admin: route('/admin', adminRouteDefs),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function installAdminRoutes<context extends AppContext>(router: RouteBuilder<context>) {
|
|
267
|
+
router.map(adminRouteGroup, {
|
|
268
|
+
actions: {
|
|
269
|
+
index() {
|
|
270
|
+
return new Response('Admin')
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
router.map(adminRouteGroup.users, {
|
|
276
|
+
actions: {
|
|
277
|
+
show({ params, currentUser }) {
|
|
278
|
+
return new Response(`User ${params.id} for ${currentUser.id}`)
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let router = createRouter<AppContext>({ middleware })
|
|
285
|
+
|
|
286
|
+
router.mount('/admin', installAdminRoutes)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Mount prefixes are route patterns. They compose with routes registered inside the installer using `joinPatterns()`, so params from the mount prefix are available to mounted handlers:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
router.mount('/orgs/:orgId', (org) => {
|
|
293
|
+
org.get('/users/:userId', ({ params }) => {
|
|
294
|
+
// params is { orgId: string, userId: string }
|
|
295
|
+
return new Response(`${params.orgId}:${params.userId}`)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
When a mount prefix and child route use the same param name, the right-most route param wins, matching `route-pattern` behavior.
|
|
301
|
+
|
|
302
|
+
Middleware stays on routers, controllers, and actions. If an entire route group needs auth or another boundary, put that middleware on the controllers or actions owned by the installer:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
function installAdminRoutes<context extends AppContext>(router: RouteBuilder<context>) {
|
|
306
|
+
router.map(adminRouteGroup, {
|
|
307
|
+
middleware: [requireAdmin()],
|
|
308
|
+
actions: {
|
|
309
|
+
index({ admin }) {
|
|
310
|
+
return new Response(admin.id)
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Unknown paths below a mounted prefix fall through to the parent router's default handler. If a route group needs its own catch-all response, register one explicitly inside the installer.
|
|
318
|
+
|
|
240
319
|
### Declaring Routes
|
|
241
320
|
|
|
242
321
|
In addition to the `{ method, pattern }` syntax shown above, the router provides a few shorthand methods that help eliminate some of the boilerplate when building complex route maps:
|
|
@@ -504,10 +583,12 @@ type Routes = typeof routes
|
|
|
504
583
|
|
|
505
584
|
Middleware functions run code before and/or after actions. They are a powerful way to add functionality to your app.
|
|
506
585
|
|
|
586
|
+
Every middleware must either return a `Response`, return the response from `next()`, or call `await next()` before it returns. Middleware that returns without calling `next()` throws at runtime.
|
|
587
|
+
|
|
507
588
|
A basic logging middleware might look like this:
|
|
508
589
|
|
|
509
590
|
```ts
|
|
510
|
-
import type
|
|
591
|
+
import { type Middleware } from 'remix/router'
|
|
511
592
|
|
|
512
593
|
// You can use the `Middleware` type to type middleware functions.
|
|
513
594
|
function logger(): Middleware {
|
|
@@ -567,8 +648,9 @@ function loadDatabase(): Middleware<{
|
|
|
567
648
|
value: Database
|
|
568
649
|
property: 'db'
|
|
569
650
|
}> {
|
|
570
|
-
return async (context) => {
|
|
651
|
+
return async (context, next) => {
|
|
571
652
|
context.set(Database, await connectDatabase(), { property: 'db' })
|
|
653
|
+
return next()
|
|
572
654
|
}
|
|
573
655
|
}
|
|
574
656
|
|
|
@@ -580,13 +662,13 @@ router.get('/books', async (context) => {
|
|
|
580
662
|
|
|
581
663
|
Use `context.db` (or `context.get(Database)`). If two values use the same property name, the router throws.
|
|
582
664
|
|
|
583
|
-
Middleware
|
|
665
|
+
Middleware has three API-owned forms: router middleware, controller middleware, and action middleware.
|
|
584
666
|
|
|
585
|
-
|
|
667
|
+
Router middleware is added to the router when it is created using the `createRouter({ middleware })` option. This middleware runs before any routes are matched and is useful for doing things like logging, serving static files, profiling, and a variety of other things. Router middleware runs on every request, so it's important to keep it lightweight and fast.
|
|
586
668
|
|
|
587
|
-
Controller middleware runs for every direct action in a controller. Action middleware runs only for one action, whether that action is registered in a controller or directly with `router.map()` or one of the method-specific helpers like `router.get()`, `router.post()`, `router.put()`, `router.delete()`, etc. The object form for actions is `{ handler, middleware? }`, so you can omit `middleware` entirely when you do not need it.
|
|
669
|
+
Controller middleware runs for every direct action in a controller. Action middleware runs only for one action, whether that action is created with `createAction()`, registered in a controller, or registered directly with `router.map()` or one of the method-specific helpers like `router.get()`, `router.post()`, `router.put()`, `router.delete()`, etc. The object form for actions is `{ handler, middleware? }`, so you can omit `middleware` entirely when you do not need it.
|
|
588
670
|
|
|
589
|
-
A controller's `middleware` applies only to the direct route actions in that controller, and its `actions` object may not include nested route-map keys.
|
|
671
|
+
A controller's `middleware` applies only to the direct route actions in that controller, and its `actions` object may not include nested route-map keys. This is the router's scoped middleware model: map nested route maps explicitly so each controller owns the direct route actions for one route map, and share middleware arrays between controllers that need the same boundary.
|
|
590
672
|
|
|
591
673
|
```tsx
|
|
592
674
|
let routes = route({
|
|
@@ -598,21 +680,21 @@ let routes = route({
|
|
|
598
680
|
})
|
|
599
681
|
|
|
600
682
|
let router = createRouter({
|
|
601
|
-
//
|
|
683
|
+
// Router middleware runs on all requests.
|
|
602
684
|
middleware: [staticFiles('./public')],
|
|
603
685
|
})
|
|
604
686
|
|
|
605
687
|
router.map(routes.home, () => new Response('Home'))
|
|
606
688
|
|
|
607
689
|
router.map(routes.admin, {
|
|
608
|
-
//
|
|
690
|
+
// Controller middleware applies to all direct actions in this controller.
|
|
609
691
|
middleware: [auth({ token: 'secret' })],
|
|
610
692
|
actions: {
|
|
611
693
|
dashboard() {
|
|
612
694
|
return new Response('Dashboard')
|
|
613
695
|
},
|
|
614
696
|
settings: {
|
|
615
|
-
//
|
|
697
|
+
// Action middleware applies only to this action.
|
|
616
698
|
middleware: [requireAdmin()],
|
|
617
699
|
handler() {
|
|
618
700
|
return new Response('Settings')
|
|
@@ -655,17 +737,11 @@ router.get('/posts/:id', (context) => {
|
|
|
655
737
|
|
|
656
738
|
Route params are only half of a handler's type contract. In many apps, handlers also depend on values that middleware loads into request context, like sessions, database connections, or authenticated users.
|
|
657
739
|
|
|
658
|
-
`fetch-router` lets you carry that context contract through the router and into stored controllers and actions. A common pattern is to derive one
|
|
740
|
+
`fetch-router` lets you carry that context contract through the router and into direct route registration, stored controllers, and stored actions. A common pattern is to derive one application context type from your middleware, augment `RouterTypes.context` with it, then use `createAction()` and `createController()` to type stored handlers.
|
|
659
741
|
|
|
660
742
|
```ts
|
|
661
743
|
import { Auth, requireAuth } from 'remix/middleware/auth'
|
|
662
|
-
import {
|
|
663
|
-
createAction,
|
|
664
|
-
createController,
|
|
665
|
-
type AnyParams,
|
|
666
|
-
type ContextWithParams,
|
|
667
|
-
type MiddlewareContext,
|
|
668
|
-
} from 'remix/router'
|
|
744
|
+
import { createAction, createController, createRouter, type RouterContext } from 'remix/router'
|
|
669
745
|
import { route } from 'remix/routes'
|
|
670
746
|
import { loadDatabase } from './middleware/database.ts'
|
|
671
747
|
import { loadSession } from './middleware/session.ts'
|
|
@@ -675,12 +751,12 @@ let routes = route({
|
|
|
675
751
|
})
|
|
676
752
|
|
|
677
753
|
type AuthIdentity = { id: string }
|
|
678
|
-
type RootMiddleware = [ReturnType<typeof loadSession>, ReturnType<typeof loadDatabase>]
|
|
679
754
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
755
|
+
export const router = createRouter({
|
|
756
|
+
middleware: [loadSession(), loadDatabase()],
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
type AppContext = RouterContext<typeof router>
|
|
684
760
|
|
|
685
761
|
declare module 'remix/router' {
|
|
686
762
|
interface RouterTypes {
|
|
@@ -688,19 +764,16 @@ declare module 'remix/router' {
|
|
|
688
764
|
}
|
|
689
765
|
}
|
|
690
766
|
|
|
691
|
-
let
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
let accountAction = createAction<typeof routes.account, AccountContext>(routes.account, {
|
|
695
|
-
middleware: accountMiddleware,
|
|
767
|
+
let accountAction = createAction(routes.account, {
|
|
768
|
+
middleware: [requireAuth<AuthIdentity>()],
|
|
696
769
|
handler(context) {
|
|
697
770
|
let auth = context.get(Auth)
|
|
698
771
|
return Response.json({ id: auth.identity.id })
|
|
699
772
|
},
|
|
700
773
|
})
|
|
701
774
|
|
|
702
|
-
let accountController = createController
|
|
703
|
-
middleware:
|
|
775
|
+
let accountController = createController(routes, {
|
|
776
|
+
middleware: [requireAuth<AuthIdentity>()],
|
|
704
777
|
actions: {
|
|
705
778
|
account(context) {
|
|
706
779
|
let auth = context.get(Auth)
|
|
@@ -710,11 +783,13 @@ let accountController = createController<typeof routes, AccountContext>(routes,
|
|
|
710
783
|
})
|
|
711
784
|
```
|
|
712
785
|
|
|
713
|
-
In this example,
|
|
786
|
+
In this example, the router's inline middleware array defines the app context contract. `RouterContext<typeof router>` extracts the request context that the router provides, so `RouterTypes.context` can use that context without storing the middleware chain separately.
|
|
787
|
+
|
|
788
|
+
Prefer plain inline arrays for `middleware` options on routers, controllers, actions, and route helpers. Inline arrays already give TypeScript enough information to infer middleware-provided context for downstream handlers, so `createAction()` and direct action objects see action middleware context, and `createController()` sees controller middleware context without `createMiddleware()`.
|
|
714
789
|
|
|
715
|
-
|
|
790
|
+
Use `createMiddleware()` only when a middleware chain is stored somewhere and its exact tuple type needs to survive that boundary. The common cases are deriving `MiddlewareContext<typeof rootMiddleware>` without a router value, exporting a reusable chain, or returning a chain from a factory. A standalone array like `let middleware = [loadSession(), loadDatabase()]` widens to a normal array, so `MiddlewareContext<typeof middleware>` cannot derive the ordered middleware context.
|
|
716
791
|
|
|
717
|
-
When manually annotating stored handlers
|
|
792
|
+
When manually annotating stored handlers with `Action<typeof route, Context>` or `Controller<typeof routes, Context>`, compose any action or controller middleware chain into `Context` with `MiddlewareContext<typeof actionOrControllerMiddleware, AppContext>`.
|
|
718
793
|
|
|
719
794
|
#### Middleware Provider Guidance
|
|
720
795
|
|
|
@@ -729,7 +804,12 @@ If you're authoring a middleware package that stores values in request context,
|
|
|
729
804
|
Apps can derive request context from the middleware tuple with `MiddlewareContext`. If they need to describe a context shape without a middleware tuple, they can use the core `ContextWithEntry` and `ContextWithEntries` helpers directly.
|
|
730
805
|
|
|
731
806
|
```ts
|
|
732
|
-
import {
|
|
807
|
+
import {
|
|
808
|
+
createContextKey,
|
|
809
|
+
createMiddleware,
|
|
810
|
+
type Middleware,
|
|
811
|
+
type MiddlewareContext,
|
|
812
|
+
} from 'remix/router'
|
|
733
813
|
|
|
734
814
|
// The context key that consumers will need to read from `context.get(...)`
|
|
735
815
|
export const CurrentUser = createContextKey<User | null>()
|
|
@@ -747,7 +827,7 @@ export function loadCurrentUser(): Middleware<{
|
|
|
747
827
|
}
|
|
748
828
|
}
|
|
749
829
|
|
|
750
|
-
let middleware =
|
|
830
|
+
let middleware = createMiddleware(loadCurrentUser())
|
|
751
831
|
type AppContext = MiddlewareContext<typeof middleware>
|
|
752
832
|
|
|
753
833
|
// Use context.currentUser (or context.get(CurrentUser)).
|
|
@@ -7,7 +7,7 @@ A streaming `multipart/form-data` parser that solves memory issues with file upl
|
|
|
7
7
|
- **Drop-in replacement** for `request.formData()` with streaming file upload support
|
|
8
8
|
- **Minimal buffering** - processes file upload streams with minimal memory footprint
|
|
9
9
|
- **Standards-based** - built on the [web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) and [File API](https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
10
|
-
- **Smart fallback** -
|
|
10
|
+
- **Smart fallback** - parses `application/x-www-form-urlencoded` requests with the same total size and field count limits, and uses native `request.formData()` for other form requests
|
|
11
11
|
- **Storage agnostic** - works with any storage backend (local disk, S3, R2, etc.)
|
|
12
12
|
|
|
13
13
|
## Why You Need This
|
|
@@ -74,7 +74,7 @@ async function requestHandler(request: Request) {
|
|
|
74
74
|
|
|
75
75
|
To validate the resulting `FormData` object with `remix/data-schema`, use the `remix/data-schema/form-data` helpers.
|
|
76
76
|
|
|
77
|
-
To limit the overall shape of multipart requests, use the `maxHeaderSize`, `maxFileSize`, `maxFiles`, `maxParts`, and `maxTotalSize` options. By default, `parseFormData()` uses `maxFiles = 20`, `maxParts = 1000`, and `maxTotalSize = maxFiles * maxFileSize + 1 MiB`.
|
|
77
|
+
To limit the overall shape of multipart requests, use the `maxHeaderSize`, `maxFileSize`, `maxFiles`, `maxParts`, and `maxTotalSize` options. For `application/x-www-form-urlencoded` requests, `maxParts` limits the number of form fields and `maxTotalSize` limits the raw request body size. By default, `parseFormData()` uses `maxFiles = 20`, `maxParts = 1000`, and `maxTotalSize = maxFiles * maxFileSize + 1 MiB`.
|
|
78
78
|
|
|
79
79
|
Known limit errors are thrown directly so you can handle them with `instanceof` checks. Other failures while parsing the request body are wrapped in `FormDataParseError`, with the original error available as `error.cause`. Errors thrown or rejected by your `uploadHandler` are not wrapped.
|
|
80
80
|
|
|
@@ -106,9 +106,9 @@ try {
|
|
|
106
106
|
} else if (error instanceof MaxFileSizeExceededError) {
|
|
107
107
|
console.error(`Files may not be larger than 10 MiB`)
|
|
108
108
|
} else if (error instanceof MaxPartsExceededError) {
|
|
109
|
-
console.error(`Request may not contain more than 25 multipart parts`)
|
|
109
|
+
console.error(`Request may not contain more than 25 form fields or multipart parts`)
|
|
110
110
|
} else if (error instanceof MaxTotalSizeExceededError) {
|
|
111
|
-
console.error(`
|
|
111
|
+
console.error(`Form data request may not exceed 12 MiB of total content`)
|
|
112
112
|
} else if (error instanceof FormDataParseError) {
|
|
113
113
|
console.error(`Could not parse form data:`, error.cause ?? error)
|
|
114
114
|
} else {
|
package/src/mime/README.md
CHANGED
|
@@ -86,12 +86,19 @@ mimeTypeToContentType('image/png') // 'image/png'
|
|
|
86
86
|
Registers or overrides a MIME type for one or more file extensions.
|
|
87
87
|
|
|
88
88
|
```ts
|
|
89
|
-
import { defineMimeType } from 'remix/mime'
|
|
89
|
+
import { defineMimeType, detectMimeType } from 'remix/mime'
|
|
90
90
|
|
|
91
91
|
defineMimeType({
|
|
92
92
|
extensions: ['myformat'],
|
|
93
93
|
mimeType: 'application/x-myformat',
|
|
94
94
|
})
|
|
95
|
+
|
|
96
|
+
defineMimeType({
|
|
97
|
+
extensions: ['be.pit'],
|
|
98
|
+
mimeType: 'application/x-be-pit-document',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
detectMimeType('filename.be.pit') // 'application/x-be-pit-document'
|
|
95
102
|
```
|
|
96
103
|
|
|
97
104
|
You can also optionally configure the charset and whether the MIME type is compressible:
|
|
@@ -8,7 +8,8 @@ Build Node.js servers with web-standard Fetch API primitives. `node-fetch-server
|
|
|
8
8
|
- **Node.js HTTP Integration** - Works directly with `node:http`, `node:https`, and `node:http2`
|
|
9
9
|
- **Streaming Support** - Response support with `ReadableStream`
|
|
10
10
|
- **Custom Hostname** - Configuration for deployment flexibility
|
|
11
|
-
- **
|
|
11
|
+
- **Proxy Header Support** - Opt in to trusted `Forwarded`, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-For` headers
|
|
12
|
+
- **Client Info** - Access to client connection info (IP address, port, and trusted proxy address)
|
|
12
13
|
- **TypeScript** - Full TypeScript support with type definitions
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
@@ -146,9 +147,34 @@ let server = http.createServer(createRequestListener(handler, { host: hostname }
|
|
|
146
147
|
server.listen(3000)
|
|
147
148
|
```
|
|
148
149
|
|
|
150
|
+
### Trusted Proxy Headers
|
|
151
|
+
|
|
152
|
+
If your app runs behind a trusted reverse proxy, Node.js sees the proxy connection instead of the original client connection. Enable `trustProxy` to use trusted proxy headers when constructing `request.url` and client information:
|
|
153
|
+
|
|
154
|
+
- `Forwarded: proto` and `X-Forwarded-Proto` can provide the original request protocol.
|
|
155
|
+
- `Forwarded: host` and `X-Forwarded-Host` can provide the original request host.
|
|
156
|
+
- `Forwarded: for` and `X-Forwarded-For` can provide the original client address.
|
|
157
|
+
|
|
158
|
+
Only enable this option when your server is reachable exclusively through a trusted proxy that overwrites these headers. Otherwise, clients can spoof the host, protocol, and client address.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import * as http from 'node:http'
|
|
162
|
+
import { createRequestListener } from 'remix/node-fetch-server'
|
|
163
|
+
|
|
164
|
+
let server = http.createServer(
|
|
165
|
+
createRequestListener(handler, {
|
|
166
|
+
trustProxy: true,
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
server.listen(3000)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When `host` or `protocol` are set, those fixed options take precedence over trusted proxy headers.
|
|
174
|
+
|
|
149
175
|
### Accessing Client Information
|
|
150
176
|
|
|
151
|
-
Get client connection details (IP address, port) for logging or security:
|
|
177
|
+
Get client connection details (IP address, port) for logging or security. When `trustProxy` is enabled, `client.address` uses trusted `Forwarded` or `X-Forwarded-For` values when present:
|
|
152
178
|
|
|
153
179
|
```ts
|
|
154
180
|
import { type FetchHandler } from 'remix/node-fetch-server'
|
|
@@ -65,6 +65,8 @@ createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
|
|
|
65
65
|
|
|
66
66
|
For in-depth reference, visit the [`route-pattern` API docs](https://api.remix.run/api/remix/route-pattern)
|
|
67
67
|
|
|
68
|
+
Examples in this README use `remix/route-pattern/*` imports. The same APIs are also available from the direct package entrypoints: `@remix-run/route-pattern`, `@remix-run/route-pattern/href`, `@remix-run/route-pattern/match`, `@remix-run/route-pattern/join`, and `@remix-run/route-pattern/specificity`.
|
|
69
|
+
|
|
68
70
|
## Pattern syntax
|
|
69
71
|
|
|
70
72
|
### Protocol
|
|
@@ -149,6 +151,8 @@ docsMatcher.match(url)?.params
|
|
|
149
151
|
// Type safe params ^? { tenant: string | undefined, path: string, ext: string } | undefined
|
|
150
152
|
```
|
|
151
153
|
|
|
154
|
+
Matchers accept absolute URL strings or `URL` objects. Relative strings such as `/blog/v3` are not accepted because matching uses the platform `URL` parser; wrap relative paths with a known origin before matching them.
|
|
155
|
+
|
|
152
156
|
### Match against multiple patterns
|
|
153
157
|
|
|
154
158
|
Use `createMultiMatcher` when you need to match many patterns and attach your own data to each match.
|
|
@@ -172,17 +176,38 @@ matcher.match('https://example.com/api/v2/users/profile')
|
|
|
172
176
|
|
|
173
177
|
The matched pattern is only known at runtime, so matched `params` are not inferred when matching with `createMultiMatcher`.
|
|
174
178
|
|
|
179
|
+
Each match returns:
|
|
180
|
+
|
|
181
|
+
- `url`: the `URL` object that was matched
|
|
182
|
+
- `pattern`: the matched `RoutePattern`
|
|
183
|
+
- `data`: the data attached with `matcher.add(pattern, data)`
|
|
184
|
+
- `params`: captured param values
|
|
185
|
+
- `paramsMeta`: hostname and pathname param metadata
|
|
186
|
+
|
|
187
|
+
`paramsMeta.hostname` and `paramsMeta.pathname` are arrays of `{ type, name, value, begin, end }` entries. The offsets are measured after URL normalization. A pattern with no hostname matches any hostname, represented in `paramsMeta.hostname` as an unnamed wildcard entry.
|
|
188
|
+
|
|
189
|
+
Set `ignoreCase: true` to make pathname matching case-insensitive. Hostname matching is always case-insensitive, and search constraints are always case-sensitive.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
let matcher = createMatcher('/Docs/:slug', { ignoreCase: true })
|
|
193
|
+
|
|
194
|
+
matcher.match('https://example.com/docs/Intro')?.params
|
|
195
|
+
// { slug: 'Intro' }
|
|
196
|
+
```
|
|
197
|
+
|
|
175
198
|
### Ranking matches by specificity
|
|
176
199
|
|
|
177
200
|
When multiple patterns match the same URL, `route-pattern` chooses the most specific match deterministically. Matches are ranked left-to-right, character-by-character:
|
|
178
201
|
|
|
202
|
+
- Explicit protocol and port constraints are more specific than omitted constraints.
|
|
203
|
+
- Static hostnames are more specific than dynamic hostnames, which are more specific than omitted hostnames.
|
|
179
204
|
- Static characters are more specific than variables.
|
|
180
205
|
- Variables are more specific than wildcards.
|
|
181
206
|
- Earliest difference decides the winner.
|
|
182
207
|
|
|
183
208
|
This is the same ranking used by `createMultiMatcher`.
|
|
184
209
|
|
|
185
|
-
For advanced use cases, `/specificity` provides comparison utilities: `lessThan`, `greaterThan`, `equal`, `descending`, `ascending`, `compare`. For example:
|
|
210
|
+
For advanced use cases, `/specificity` provides comparison utilities: `lessThan`, `greaterThan`, `equal`, `descending`, `ascending`, `compare`. `lessThan(a, b)` returns `true` when match `a` is less specific than match `b`. For example:
|
|
186
211
|
|
|
187
212
|
```ts
|
|
188
213
|
import { createMultiMatcher } from 'remix/route-pattern/match'
|
|
@@ -226,6 +251,10 @@ createHref('blog/:slug?ref=docs', { slug: 'v3' }, { utm_source: 'newsletter' })
|
|
|
226
251
|
// '/blog/v3?utm_source=newsletter&ref=docs'
|
|
227
252
|
```
|
|
228
253
|
|
|
254
|
+
`createHref()` throws `CreateHrefError` when it cannot safely generate an href. The error exposes stable structured details on `error.details`; the string message is for humans.
|
|
255
|
+
|
|
256
|
+
Common failures include missing required params, nameless wildcards, invalid hostname params, empty pathname variables, and origin patterns that specify a protocol or port without a concrete hostname.
|
|
257
|
+
|
|
229
258
|
**Note:** optional groups without params are included in the generated href:
|
|
230
259
|
|
|
231
260
|
```ts
|
|
@@ -238,25 +267,41 @@ createHref('products(.json)')
|
|
|
238
267
|
|
|
239
268
|
## Parse & stringify patterns
|
|
240
269
|
|
|
241
|
-
You can explicitly parse and stringify patterns
|
|
270
|
+
You can explicitly parse and stringify patterns. Create a `RoutePattern` with `RoutePattern.parse` and use the methods and helpers below instead of reading parsed token internals.
|
|
242
271
|
|
|
243
272
|
```ts
|
|
244
|
-
import { RoutePattern } from 'remix/route-pattern'
|
|
273
|
+
import { getRoutePatternCaptures, RoutePattern } from 'remix/route-pattern'
|
|
245
274
|
|
|
246
|
-
let pattern = RoutePattern.parse('
|
|
275
|
+
let pattern = RoutePattern.parse('://:tenant.example.com/blog/:slug(/*path)')
|
|
247
276
|
// ^? RoutePattern
|
|
248
277
|
|
|
249
278
|
pattern.toString()
|
|
250
|
-
// '
|
|
279
|
+
// '://:tenant.example.com/blog/:slug(/*path)'
|
|
251
280
|
|
|
252
281
|
pattern.toJSON()
|
|
253
|
-
// { hostname: 'example.com', pathname: 'blog/:slug', ... }
|
|
282
|
+
// { hostname: ':tenant.example.com', pathname: 'blog/:slug(/*path)', ... }
|
|
283
|
+
|
|
284
|
+
getRoutePatternCaptures(pattern)
|
|
285
|
+
// [
|
|
286
|
+
// { part: 'hostname', type: ':', name: 'tenant', optional: false },
|
|
287
|
+
// { part: 'pathname', type: ':', name: 'slug', optional: false },
|
|
288
|
+
// { part: 'pathname', type: '*', name: 'path', optional: true },
|
|
289
|
+
// ]
|
|
254
290
|
```
|
|
255
291
|
|
|
256
292
|
All APIs that take a `pattern` arg accept `string` or a parsed `RoutePattern`.
|
|
257
293
|
|
|
258
294
|
**TIP:** For high-performance scenarios, you can parse patterns ahead of time to avoid reparsing them on every call.
|
|
259
295
|
|
|
296
|
+
`RoutePattern.toJSON()` returns a `RoutePatternJSON` object with serialized `protocol`, `hostname`, `port`, `pathname`, and `search` fields. `RoutePattern.parse()` throws `ParseError` for malformed sources; the error exposes stable `type`, `source`, and `index` fields.
|
|
297
|
+
|
|
298
|
+
The public support types are:
|
|
299
|
+
|
|
300
|
+
- `RoutePatternCapture` from `remix/route-pattern`
|
|
301
|
+
- `RoutePatternJSON` from `remix/route-pattern`
|
|
302
|
+
- `CreateHrefErrorDetails` from `remix/route-pattern/href`
|
|
303
|
+
- `MatchParamMeta` from `remix/route-pattern/match`
|
|
304
|
+
|
|
260
305
|
## Combine patterns
|
|
261
306
|
|
|
262
307
|
`joinPatterns` builds a new pattern from a base pattern.
|
package/src/session/README.md
CHANGED
|
@@ -126,7 +126,7 @@ This will clear all session data from storage the next time it is saved. It also
|
|
|
126
126
|
|
|
127
127
|
Several strategies are provided out of the box for storing session data across requests, depending on your needs.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Session storage objects read and save cookie values. Use the `session` middleware with a signed `Cookie` to parse the incoming `Cookie` header, expose the session on request context, and serialize any saved value back into a `Set-Cookie` response header.
|
|
130
130
|
|
|
131
131
|
#### Filesystem Storage
|
|
132
132
|
|
package/src/test/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A test framework for JavaScript and TypeScript projects.
|
|
|
9
9
|
- Playwright E2E testing via `t.serve`
|
|
10
10
|
- In-browser component testing (pair with `render` from `remix/ui/test`)
|
|
11
11
|
- Mock functions and method spies via `t.mock.fn` / `t.mock.method`
|
|
12
|
+
- Per-test and hook timeouts with `t.signal` abort support
|
|
12
13
|
- Unified code coverage reporting across unit and E2E tests
|
|
13
14
|
- Watch mode
|
|
14
15
|
- Config file support (`remix-test.config.ts`)
|
|
@@ -193,6 +194,11 @@ describe('My Test Suite', () => {
|
|
|
193
194
|
afterEach(() => {})
|
|
194
195
|
|
|
195
196
|
it('tests something', () => {})
|
|
197
|
+
it('skips with a reason', { skip: 'requires API credentials' }, () => {})
|
|
198
|
+
it('tracks planned work', { todo: 'add retry coverage' }, () => {})
|
|
199
|
+
it('fails if it takes too long', { timeout: 5_000 }, async (t) => {
|
|
200
|
+
await fetchSomething({ signal: t.signal })
|
|
201
|
+
})
|
|
196
202
|
it('tests something else', () => {})
|
|
197
203
|
})
|
|
198
204
|
```
|
|
@@ -229,6 +235,9 @@ Each test callback receives a `TestContext` (`t`) as its first argument with hel
|
|
|
229
235
|
```ts
|
|
230
236
|
// from 'remix/test'
|
|
231
237
|
interface TestContext {
|
|
238
|
+
// Aborts when the test times out or when the user-provided test signal aborts
|
|
239
|
+
signal: AbortSignal
|
|
240
|
+
|
|
232
241
|
// Register a cleanup function to run after the test completes
|
|
233
242
|
after(fn: () => void): void
|
|
234
243
|
|
|
@@ -284,6 +293,24 @@ it('cleanup', (t) => {
|
|
|
284
293
|
})
|
|
285
294
|
```
|
|
286
295
|
|
|
296
|
+
#### Timeouts and Signals
|
|
297
|
+
|
|
298
|
+
Pass `{ timeout: ms }` to `it()` or after any lifecycle hook callback to fail that work if it takes too long. Timed-out tests abort `t.signal`, so async code that accepts an `AbortSignal` can cancel promptly.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
it('loads data', { timeout: 5_000 }, async (t) => {
|
|
302
|
+
let response = await fetch('/api/data', { signal: t.signal })
|
|
303
|
+
assert.equal(response.status, 200)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
beforeEach(
|
|
307
|
+
async () => {
|
|
308
|
+
await resetDatabase()
|
|
309
|
+
},
|
|
310
|
+
{ timeout: 1_000 },
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
287
314
|
#### Fake Timers
|
|
288
315
|
|
|
289
316
|
`t.useFakeTimers()` replaces the global timer functions (`setTimeout`, `setInterval`, etc.) with controllable fakes that are automatically restored after the test. It works in any test environment — server unit tests, browser tests, or E2E setup code.
|