remix 3.0.0-beta.0 → 3.0.0-beta.2
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/fetch-router.d.ts +7 -0
- package/dist/fetch-router.d.ts.map +1 -1
- package/dist/node-tsx/load-module.d.ts +2 -0
- package/dist/node-tsx/load-module.d.ts.map +1 -0
- package/dist/node-tsx/load-module.js +2 -0
- package/dist/node-tsx.d.ts +3 -0
- package/dist/node-tsx.d.ts.map +1 -0
- package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
- package/dist/render-middleware.d.ts +2 -0
- package/dist/render-middleware.d.ts.map +1 -0
- package/dist/render-middleware.js +2 -0
- package/dist/route-pattern/href.d.ts +2 -0
- package/dist/route-pattern/href.d.ts.map +1 -0
- package/dist/route-pattern/href.js +2 -0
- package/dist/route-pattern/join.d.ts +2 -0
- package/dist/route-pattern/join.d.ts.map +1 -0
- package/dist/route-pattern/join.js +2 -0
- package/dist/route-pattern/match.d.ts +2 -0
- package/dist/route-pattern/match.d.ts.map +1 -0
- package/dist/route-pattern/match.js +2 -0
- package/package.json +158 -44
- package/src/assert/README.md +109 -0
- package/src/assets/README.md +539 -0
- package/src/async-context-middleware/README.md +100 -0
- package/src/auth/README.md +445 -0
- package/src/auth-middleware/README.md +246 -0
- package/src/cli/README.md +78 -0
- package/src/compression-middleware/README.md +176 -0
- package/src/cookie/README.md +106 -0
- package/src/cop-middleware/README.md +117 -0
- package/src/cors-middleware/README.md +174 -0
- package/src/csrf-middleware/README.md +99 -0
- package/src/data-schema/README.md +422 -0
- package/src/data-table/README.md +552 -0
- package/src/data-table-mysql/README.md +97 -0
- package/src/data-table-postgres/README.md +74 -0
- package/src/data-table-sqlite/README.md +84 -0
- package/src/fetch-proxy/README.md +46 -0
- package/src/fetch-router/README.md +902 -0
- package/src/fetch-router.ts +7 -0
- package/src/file-storage/README.md +57 -0
- package/src/file-storage-s3/README.md +47 -0
- package/src/form-data-middleware/README.md +109 -0
- package/src/form-data-parser/README.md +160 -0
- package/src/fs/README.md +60 -0
- package/src/headers/README.md +629 -0
- package/src/html-template/README.md +101 -0
- package/src/lazy-file/README.md +109 -0
- package/src/logger-middleware/README.md +132 -0
- package/src/method-override-middleware/README.md +71 -0
- package/src/mime/README.md +110 -0
- package/src/multipart-parser/README.md +241 -0
- package/src/node-fetch-server/README.md +352 -0
- package/src/node-tsx/README.md +79 -0
- package/src/node-tsx/load-module.ts +2 -0
- package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
- package/src/render-middleware/README.md +99 -0
- package/src/render-middleware.ts +2 -0
- package/src/route-pattern/README.md +291 -0
- package/src/route-pattern/href.ts +2 -0
- package/src/route-pattern/join.ts +2 -0
- package/src/route-pattern/match.ts +2 -0
- package/src/session/README.md +171 -0
- package/src/session-middleware/README.md +109 -0
- package/src/session-storage-memcache/README.md +37 -0
- package/src/session-storage-redis/README.md +37 -0
- package/src/static-middleware/README.md +89 -0
- package/src/tar-parser/README.md +74 -0
- package/src/terminal/README.md +92 -0
- package/src/test/README.md +430 -0
- package/src/ui/README.md +219 -0
- package/src/ui/accordion/README.md +166 -0
- package/src/ui/anchor/README.md +153 -0
- package/src/ui/animation/README.md +316 -0
- package/src/ui/breadcrumbs/README.md +55 -0
- package/src/ui/button/README.md +44 -0
- package/src/ui/combobox/README.md +145 -0
- package/src/ui/glyph/README.md +72 -0
- package/src/ui/listbox/README.md +115 -0
- package/src/ui/menu/README.md +96 -0
- package/src/ui/popover/README.md +122 -0
- package/src/ui/scroll-lock/README.md +33 -0
- package/src/ui/select/README.md +107 -0
- package/src/ui/server/README.md +90 -0
- package/src/ui/test/README.md +107 -0
- package/src/ui/theme/README.md +103 -0
- package/dist/node-serve.d.ts +0 -2
- package/dist/node-serve.d.ts.map +0 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# render-middleware
|
|
2
|
+
|
|
3
|
+
Request-scoped renderer middleware for Remix. It stores a renderer in `fetch-router` request context so route actions can render responses without passing request-specific rendering details through every action.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Generic renderers** - Render any input type to a `Response`
|
|
8
|
+
- **Request-scoped setup** - Create renderers from the current `RequestContext`
|
|
9
|
+
- **Typed context** - Typed `context.render` (or `context.get(Renderer)`)
|
|
10
|
+
- **Small runtime** - The package only stores a renderer in request context
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i remix
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Use `renderWith()` to add a renderer to `context.render` and `context.get(Renderer)`.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createRouter, type MiddlewareContext } from 'remix/router'
|
|
24
|
+
import { renderWith } from 'remix/middleware/render'
|
|
25
|
+
|
|
26
|
+
const render = renderWith(
|
|
27
|
+
(context) =>
|
|
28
|
+
function render(value: string, init?: ResponseInit) {
|
|
29
|
+
return new Response(`${context.url.pathname}: ${value}`, init)
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
type AppContext = MiddlewareContext<[typeof render]>
|
|
34
|
+
|
|
35
|
+
const router = createRouter<AppContext>({
|
|
36
|
+
middleware: [render],
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
router.get('/hello', (context) => {
|
|
40
|
+
return context.render('Hello')
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `context.render(...)` (or `context.get(Renderer)(...)`).
|
|
45
|
+
|
|
46
|
+
Renderers may render any value type, not just UI nodes.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { renderWith } from 'remix/middleware/render'
|
|
50
|
+
|
|
51
|
+
const json = renderWith(
|
|
52
|
+
() =>
|
|
53
|
+
function render(data: unknown, init?: ResponseInit) {
|
|
54
|
+
return Response.json(data, init)
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
router.get('/api', (context) => {
|
|
59
|
+
return context.render({ ok: true })
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For Remix UI, create a renderer that owns frame resolution and response creation.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { createHtmlResponse } from 'remix/response/html'
|
|
67
|
+
import { renderWith } from 'remix/middleware/render'
|
|
68
|
+
import type { RemixNode } from 'remix/ui'
|
|
69
|
+
import { renderToStream } from 'remix/ui/server'
|
|
70
|
+
|
|
71
|
+
const render = renderWith(
|
|
72
|
+
({ router, url }) =>
|
|
73
|
+
function render(node: RemixNode, init?: ResponseInit) {
|
|
74
|
+
let stream = renderToStream(node, {
|
|
75
|
+
async resolveFrame(src) {
|
|
76
|
+
let response = await router.fetch(new URL(src, url))
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
return `<pre>Frame error: ${response.status}</pre>`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response.body ?? response.text()
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return createHtmlResponse(stream, init)
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Related Packages
|
|
92
|
+
|
|
93
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Request routing and context
|
|
94
|
+
- [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) - Remix UI rendering primitives
|
|
95
|
+
- [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# route-pattern
|
|
2
|
+
|
|
3
|
+
Type-safe URL matching and href generation for JavaScript. `route-pattern` supports path variables, wildcards, optionals, search constraints, and full-URL patterns with predictable ranking.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type-safe** - Infer params from patterns for compile-time correctness
|
|
8
|
+
- **Expressive** - Variables, wildcards, optionals, and search constraints
|
|
9
|
+
- **Full URL support** - Match protocol, hostname, port, pathname, and search
|
|
10
|
+
- **Simple & deterministic ranking** - Predictable left-to-right priority for static, variable, and wildcard patterns
|
|
11
|
+
- **Fast** - Trie-based matching for scalable performance
|
|
12
|
+
- **Modular** - Import only the features you need to for smaller bundles
|
|
13
|
+
- **Runtime agnostic** - Works across Node.js, Bun, Deno, Cloudflare Workers, and browsers
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm i remix
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick example
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createMultiMatcher } from 'remix/route-pattern/match'
|
|
25
|
+
|
|
26
|
+
let matcher = createMultiMatcher<{ name: string }>()
|
|
27
|
+
|
|
28
|
+
matcher.add('blog/:slug', { name: 'blog-post' })
|
|
29
|
+
matcher.add('api(/v:version)/*path', { name: 'api' })
|
|
30
|
+
matcher.add('http(s)://:region.cdn.com/assets/*file.:ext', { name: 'assets' })
|
|
31
|
+
|
|
32
|
+
let match = matcher.match('https://example.com/blog/v3')
|
|
33
|
+
match?.pattern.toString()
|
|
34
|
+
// /blog/:slug
|
|
35
|
+
match?.params
|
|
36
|
+
// { slug: 'v3' }
|
|
37
|
+
match?.data
|
|
38
|
+
// { name: 'blog-post' }
|
|
39
|
+
|
|
40
|
+
import { createHref } from 'remix/route-pattern/href'
|
|
41
|
+
|
|
42
|
+
createHref('blog/:slug', { slug: 'v3' })
|
|
43
|
+
// '/blog/v3'
|
|
44
|
+
|
|
45
|
+
createHref('api(/v:version)/*path', { version: '2', path: 'users/profile' })
|
|
46
|
+
// '/api/v2/users/profile'
|
|
47
|
+
|
|
48
|
+
createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
|
|
49
|
+
region: 'us-west',
|
|
50
|
+
file: 'images/logo',
|
|
51
|
+
ext: 'png',
|
|
52
|
+
})
|
|
53
|
+
// 'https://us-west.cdn.com/assets/images/logo.png'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API at a glance
|
|
57
|
+
|
|
58
|
+
**remix/route-pattern** - Parse and stringify patterns.
|
|
59
|
+
|
|
60
|
+
**remix/route-pattern/href** - Generate hrefs for patterns with type safe params.
|
|
61
|
+
|
|
62
|
+
**remix/route-pattern/match** - Match against one pattern with type inference for params. Or match against many patterns with deterministic ranking and attached data.
|
|
63
|
+
|
|
64
|
+
**remix/route-pattern/join** - Combine two patterns into one. Override protocol, hostname, port. Join pathnames. Merge search constraints.
|
|
65
|
+
|
|
66
|
+
**remix/route-pattern/specificity** - Rank matches by [specificity](#ranking-matches-by-specificity).
|
|
67
|
+
|
|
68
|
+
For in-depth reference, visit the [`route-pattern` API docs](https://api.remix.run/api/remix/route-pattern)
|
|
69
|
+
|
|
70
|
+
## Pattern syntax
|
|
71
|
+
|
|
72
|
+
### Protocol
|
|
73
|
+
|
|
74
|
+
Protocol must be `http`, `https`, or `http(s)`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
'https://example.com' // matches https://example.com
|
|
78
|
+
'http(s)://example.com' // matches http://example.com, https://example.com
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Hostname & pathname
|
|
82
|
+
|
|
83
|
+
**Variables** capture dynamic segments using `:name`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
'users/:id' // matches /users/123
|
|
87
|
+
'blog/:year-:month-:day/:slug' // matches /blog/2024-01-15/hello
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Wildcards** match multi-segment paths using `*name`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
'files/*path' // matches /files/images/logo.png
|
|
94
|
+
'node_modules/*package/dist/index.js' // matches /node_modules/@remix-run/router/dist/index.js
|
|
95
|
+
'files/*' // matches any path under /files, but doesn't capture the wildcard value
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Optionals** make parts optional using `()`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
'api(/v:version)/users' // matches /api/users, /api/v2/users
|
|
102
|
+
'blog/:slug(.html)' // matches /blog/hello, /blog/hello.html
|
|
103
|
+
'docs(/guides/:category)' // matches /docs, /docs/guides/routing
|
|
104
|
+
'api(/v:major(.:minor))' // matches /api, /api/v2, /api/v2.1
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
While variables, wilcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
':tenant.example.com/dashboard' // matches acme.example.com/dashboard
|
|
111
|
+
'(www.)example.com/blog/:slug(.html)' // matches example.com/blog/hello, www.example.com/blog/hello.html
|
|
112
|
+
'*.example.com/files/*path' // matches cdn.example.com/files/images/logo.png
|
|
113
|
+
'(:locale.)example.com/docs(/:section)' // matches en.example.com/docs, en.example.com/docs/guides
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Search
|
|
117
|
+
|
|
118
|
+
**Search constraints** narrow matches using `?key` or `?key=value`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
'search?q' // key must be present
|
|
122
|
+
'search?q=routing' // requires ?q=routing exactly
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Match URLs
|
|
126
|
+
|
|
127
|
+
### Match against a single pattern
|
|
128
|
+
|
|
129
|
+
Use `createMatcher` when you have one pattern and want params inferred from that exact pattern.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { createMatcher } from 'remix/route-pattern/match'
|
|
133
|
+
|
|
134
|
+
const url: string | URL = /* ... */
|
|
135
|
+
|
|
136
|
+
let blogMatcher = createMatcher('blog/:slug')
|
|
137
|
+
blogMatcher.match(url)?.params
|
|
138
|
+
// Type safe params ^? { slug: string } | undefined
|
|
139
|
+
|
|
140
|
+
let docsMatcher = createMatcher('://(:tenant.)host.com/docs/*path.:ext')
|
|
141
|
+
docsMatcher.match(url)?.params
|
|
142
|
+
// Type safe params ^? { tenant: string | undefined, path: string, ext: string } | undefined
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Match against multiple patterns
|
|
146
|
+
|
|
147
|
+
Use `createMultiMatcher` when you need to match many patterns and attach your own data to each match.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { createMultiMatcher } from 'remix/route-pattern/match'
|
|
151
|
+
|
|
152
|
+
let matcher = createMultiMatcher<string>()
|
|
153
|
+
// Any data type you want! 👆
|
|
154
|
+
|
|
155
|
+
matcher.add('/', 'home')
|
|
156
|
+
matcher.add('blog/:slug', 'blog-post')
|
|
157
|
+
matcher.add('api(/v:version)/*path', 'api')
|
|
158
|
+
|
|
159
|
+
matcher.match('https://example.com/blog/v3')
|
|
160
|
+
// { params: { slug: 'v3' }, data: 'blog-post' }
|
|
161
|
+
|
|
162
|
+
matcher.match('https://example.com/api/v2/users/profile')
|
|
163
|
+
// { params: { version: '2', path: 'users/profile' }, data: 'api' }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The matched pattern is only known at runtime, so matched `params` are not inferred when matching with `createMultiMatcher`.
|
|
167
|
+
|
|
168
|
+
### Ranking matches by specificity
|
|
169
|
+
|
|
170
|
+
When multiple patterns match the same URL, `route-pattern` chooses the most specific match deterministically. Matches are ranked left-to-right, character-by-character:
|
|
171
|
+
|
|
172
|
+
- Static characters are more specific than variables.
|
|
173
|
+
- Variables are more specific than wildcards.
|
|
174
|
+
- Earliest difference decides the winner.
|
|
175
|
+
|
|
176
|
+
This is the same ranking used by `createMultiMatcher`.
|
|
177
|
+
|
|
178
|
+
For advanced use cases, `/specificity` provides comparison utilities: `lessThan`, `greaterThan`, `equal`, `descending`, `ascending`, `compare`.
|
|
179
|
+
For example:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { createMultiMatcher } from 'remix/route-pattern/match'
|
|
183
|
+
import { descending } from 'remix/route-pattern/specificity'
|
|
184
|
+
|
|
185
|
+
let matcher = createMultiMatcher()
|
|
186
|
+
matcher.add('files/*path', null)
|
|
187
|
+
matcher.add('files/:name', null)
|
|
188
|
+
matcher.add('files/readme.md', null)
|
|
189
|
+
|
|
190
|
+
let matches = matcher.matchAll('https://example.com/files/readme.md')
|
|
191
|
+
|
|
192
|
+
matches.sort(descending).map((match) => match.pattern.toString())
|
|
193
|
+
// ['/files/readme.md', '/files/:name', '/files/*path']
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Generate hrefs
|
|
197
|
+
|
|
198
|
+
`createHref` turns a pattern and params into a URL string.
|
|
199
|
+
Required variables and wildcards must be provided, while params inside optional groups may be omitted.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { createHref } from 'remix/route-pattern/href'
|
|
203
|
+
|
|
204
|
+
createHref('blog/:slug', { slug: 'v3' })
|
|
205
|
+
// '/blog/v3'
|
|
206
|
+
|
|
207
|
+
createHref('api(/v:version)/*path', { path: 'users/profile' })
|
|
208
|
+
// '/api/users/profile'
|
|
209
|
+
|
|
210
|
+
createHref('api(/v:version)/*path', { version: '2', path: 'users/profile' })
|
|
211
|
+
// '/api/v2/users/profile'
|
|
212
|
+
|
|
213
|
+
createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
|
|
214
|
+
region: 'us-west',
|
|
215
|
+
file: 'images/logo',
|
|
216
|
+
ext: 'png',
|
|
217
|
+
})
|
|
218
|
+
// 'https://us-west.cdn.com/assets/images/logo.png'
|
|
219
|
+
|
|
220
|
+
createHref('blog/:slug?ref=docs', { slug: 'v3' }, { utm_source: 'newsletter' })
|
|
221
|
+
// '/blog/v3?utm_source=newsletter&ref=docs'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Note:** optional groups without params are included in the generated href:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
createHref('todos(/new)')
|
|
228
|
+
// '/todos/new'
|
|
229
|
+
|
|
230
|
+
createHref('products(.json)')
|
|
231
|
+
// '/products.json'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Parse & stringify patterns
|
|
235
|
+
|
|
236
|
+
You can explicitly parse and stringify patterns:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { RoutePattern } from 'remix/route-pattern'
|
|
240
|
+
|
|
241
|
+
let pattern = RoutePattern.parse('://example.com/blog/:slug')
|
|
242
|
+
// ^? RoutePattern
|
|
243
|
+
|
|
244
|
+
pattern.toString()
|
|
245
|
+
// '://example.com/blog/:slug'
|
|
246
|
+
|
|
247
|
+
pattern.toJSON()
|
|
248
|
+
// { hostname: 'example.com', pathname: 'blog/:slug', ... }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
All APIs that take a `pattern` arg accept `string` or a parsed `RoutePattern`.
|
|
252
|
+
|
|
253
|
+
**TIP:** For high-performance scenarios, you can parse patterns ahead of time to avoid reparsing them on every call.
|
|
254
|
+
|
|
255
|
+
## Combine patterns
|
|
256
|
+
|
|
257
|
+
`joinPatterns` builds a new pattern from a base pattern.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { joinPatterns } from 'remix/route-pattern/join'
|
|
261
|
+
|
|
262
|
+
let user = joinPatterns('users', ':id')
|
|
263
|
+
|
|
264
|
+
user.toString()
|
|
265
|
+
// '/users/:id'
|
|
266
|
+
|
|
267
|
+
let apiUser = joinPatterns('api(/v:version)', '://remix.run/users/:id')
|
|
268
|
+
|
|
269
|
+
apiUser.toString()
|
|
270
|
+
// '://remix.run/api(/v:version)/users/:id'
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
- **Protocol:** if second pattern has a protocol, overrides base pattern
|
|
274
|
+
- **Hostname:** if second pattern has a hostname, overrides base pattern
|
|
275
|
+
- **Port:** if second pattern has a port, overrides base pattern
|
|
276
|
+
- **Pathname:** concatenates pathnames, adding a `/` in between as necessary
|
|
277
|
+
- **Search constraints:** merges search constraints by key
|
|
278
|
+
|
|
279
|
+
## Benchmarks
|
|
280
|
+
|
|
281
|
+
Benchmarks live in [`bench/`](./bench/).
|
|
282
|
+
|
|
283
|
+
## Related Work
|
|
284
|
+
|
|
285
|
+
- [`path-to-regexp`](https://www.npmjs.com/package/path-to-regexp)
|
|
286
|
+
- [`find-my-way`](https://github.com/delvedor/find-my-way)
|
|
287
|
+
- [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# session
|
|
2
|
+
|
|
3
|
+
A session management library for JavaScript. This package provides a flexible and secure way to manage user sessions in server-side applications with a flexible API for different session storage strategies.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple Storage Strategies:** Includes memory, cookie, and file-based [session storage strategies](#storage-strategies) for different use cases
|
|
8
|
+
- **Flash Messages:** Support for [flash data](#flash-messages) that persists only for the next request
|
|
9
|
+
- **Session Security:** Built-in protection against [session fixation attacks](#regenerating-session-ids)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i remix
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
The following example shows how to use a session to persist data across requests.
|
|
20
|
+
|
|
21
|
+
The standard pattern when working with sessions is to read the session from the request, modify it, and save it back to storage and write the session cookie to the response.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
|
|
25
|
+
|
|
26
|
+
// Create a session storage. This is used to store session data across requests.
|
|
27
|
+
let storage = createCookieSessionStorage()
|
|
28
|
+
|
|
29
|
+
// This function simulates a typical request flow where the session is read from
|
|
30
|
+
// the request cookie, modified, and the new cookie is returned in the response.
|
|
31
|
+
async function handleRequest(cookie: string | null) {
|
|
32
|
+
let session = await storage.read(cookie)
|
|
33
|
+
session.set('count', Number(session.get('count') ?? 0) + 1)
|
|
34
|
+
return {
|
|
35
|
+
session, // The session data from this "request"
|
|
36
|
+
cookie: await storage.save(session), // The cookie to use on the next request
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let response1 = await handleRequest(null)
|
|
41
|
+
assert.equal(response1.session.get('count'), 1)
|
|
42
|
+
|
|
43
|
+
let response2 = await handleRequest(response1.cookie)
|
|
44
|
+
assert.equal(response2.session.get('count'), 2)
|
|
45
|
+
|
|
46
|
+
let response3 = await handleRequest(response2.cookie)
|
|
47
|
+
assert.equal(response3.session.get('count'), 3)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The example above is a low-level illustration of how to use this package for session management. In practice, you would use the `session` middleware in [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) to automatically manage the session for you.
|
|
51
|
+
|
|
52
|
+
### Flash Messages
|
|
53
|
+
|
|
54
|
+
Flash messages are values that persist only for the next request, perfect for displaying one-time notifications:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
async function requestIndex(cookie: string | null) {
|
|
58
|
+
let session = await storage.read(cookie)
|
|
59
|
+
return { session, cookie: await storage.save(session) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function requestSubmit(cookie: string | null) {
|
|
63
|
+
let session = await storage.read(cookie)
|
|
64
|
+
session.flash('message', 'success!')
|
|
65
|
+
return { session, cookie: await storage.save(session) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Flash data is undefined on the first request
|
|
69
|
+
let response1 = await requestIndex(null)
|
|
70
|
+
assert.equal(response1.session.get('message'), undefined)
|
|
71
|
+
|
|
72
|
+
// Flash data is undefined on the same request it is set. This response
|
|
73
|
+
// is typically a redirect to a route that displays the flash data.
|
|
74
|
+
let response2 = await requestSubmit(response1.cookie)
|
|
75
|
+
assert.equal(response2.session.get('message'), undefined)
|
|
76
|
+
|
|
77
|
+
// Flash data is available on the next request
|
|
78
|
+
let response3 = await requestIndex(response2.cookie)
|
|
79
|
+
assert.equal(response3.session.get('message'), 'success!')
|
|
80
|
+
|
|
81
|
+
// Flash data is not available on subsequent requests
|
|
82
|
+
let response4 = await requestIndex(response3.cookie)
|
|
83
|
+
assert.equal(response4.session.get('message'), undefined)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Regenerating Session IDs
|
|
87
|
+
|
|
88
|
+
For security, regenerate the session ID after privilege changes like a login. This helps prevent session fixation attacks by issuing a new session ID in the response.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createFsSessionStorage } from 'remix/session-storage/fs'
|
|
92
|
+
|
|
93
|
+
let sessionStorage = createFsSessionStorage('/tmp/sessions')
|
|
94
|
+
|
|
95
|
+
async function requestIndex(cookie: string | null) {
|
|
96
|
+
let session = await sessionStorage.read(cookie)
|
|
97
|
+
return { session, cookie: await sessionStorage.save(session) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function requestLogin(cookie: string | null) {
|
|
101
|
+
let session = await sessionStorage.read(cookie)
|
|
102
|
+
session.set('userId', 'mj')
|
|
103
|
+
session.regenerateId()
|
|
104
|
+
return { session, cookie: await sessionStorage.save(session) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let response1 = await requestIndex(null)
|
|
108
|
+
assert.equal(response1.session.get('userId'), undefined)
|
|
109
|
+
|
|
110
|
+
let response2 = await requestLogin(response1.cookie)
|
|
111
|
+
assert.notEqual(response2.session.id, response1.session.id)
|
|
112
|
+
|
|
113
|
+
let response3 = await requestIndex(response2.cookie)
|
|
114
|
+
assert.equal(response3.session.get('userId'), 'mj')
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To delete the old session data when the session is saved, use `session.regenerateId(true)`. This can help to prevent session fixation attacks by deleting the old session data when the session is saved. However, it may not be desirable in a situation with mobile clients on flaky connections that may need to resume the session using an old session ID.
|
|
118
|
+
|
|
119
|
+
### Destroying Sessions
|
|
120
|
+
|
|
121
|
+
When a user logs out, you should destroy the session using `session.destroy()`.
|
|
122
|
+
|
|
123
|
+
This will clear all session data from storage the next time it is saved. It also clears the session ID on the client in the next response, so it will start with a new session on the next request.
|
|
124
|
+
|
|
125
|
+
### Storage Strategies
|
|
126
|
+
|
|
127
|
+
Several strategies are provided out of the box for storing session data across requests, depending on your needs.
|
|
128
|
+
|
|
129
|
+
A session storage object must always be initialized with a _signed_ session cookie. This is used to identify the session and to store the session data in the response.
|
|
130
|
+
|
|
131
|
+
#### Filesystem Storage
|
|
132
|
+
|
|
133
|
+
Filesystem storage is a good choice for production environments. It requires access to a persistent filesystem, which is readily available on most servers. And it can scale to handle sessions with a lot of data easily.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { createFsSessionStorage } from 'remix/session-storage/fs'
|
|
137
|
+
|
|
138
|
+
let sessionStorage = createFsSessionStorage('/tmp/sessions')
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Cookie Storage
|
|
142
|
+
|
|
143
|
+
Cookie storage is suitable for production environments. In this strategy, all session data is stored directly in the session cookie itself, which means it doesn't require any additional storage.
|
|
144
|
+
|
|
145
|
+
The main limitation of cookie storage is that the total size of the session cookie is limited to the browser's maximum cookie size, typically 4096 bytes.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
|
|
149
|
+
|
|
150
|
+
let sessionStorage = createCookieSessionStorage()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Memory Storage
|
|
154
|
+
|
|
155
|
+
Memory storage is useful in testing and development environments. In this strategy, all session data is stored in memory, which means no additional storage is required. However, all session data is lost when the server restarts.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { createMemorySessionStorage } from 'remix/session-storage/memory'
|
|
159
|
+
|
|
160
|
+
let sessionStorage = createMemorySessionStorage()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Related Packages
|
|
164
|
+
|
|
165
|
+
- [`remix/cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) - Cookie parsing and serialization
|
|
166
|
+
- [`remix/router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router with built-in session middleware
|
|
167
|
+
- [`remix/session-storage/memcache`](https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache) - Memcache-backed session storage
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# session-middleware
|
|
2
|
+
|
|
3
|
+
Session middleware for Remix using signed cookies. It loads session state from incoming requests, exposes it as `context.session` (or `context.get(Session)`), and persists updates automatically.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Session Lifecycle Handling** - Reads and saves session state per request
|
|
8
|
+
- **Context Integration** - Exposes `context.session` (or `context.get(Session)`)
|
|
9
|
+
- **Secure Cookie Support** - Designed for signed session cookies
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i remix
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createRouter } from 'remix/router'
|
|
21
|
+
import { createCookie } from 'remix/cookie'
|
|
22
|
+
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
|
|
23
|
+
import { session } from 'remix/middleware/session'
|
|
24
|
+
|
|
25
|
+
let sessionCookie = createCookie('__session', {
|
|
26
|
+
secrets: ['s3cr3t'], // session cookies must be signed!
|
|
27
|
+
httpOnly: true,
|
|
28
|
+
secure: true,
|
|
29
|
+
sameSite: 'lax',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
let sessionStorage = createCookieSessionStorage()
|
|
33
|
+
|
|
34
|
+
let router = createRouter({
|
|
35
|
+
middleware: [session(sessionCookie, sessionStorage)],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
router.get('/', (context) => {
|
|
39
|
+
context.session.set('count', Number(context.session.get('count') ?? 0) + 1)
|
|
40
|
+
return new Response(`Count: ${context.session.get('count')}`)
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The middleware:
|
|
45
|
+
|
|
46
|
+
- Reads the session from the cookie on incoming requests
|
|
47
|
+
- Makes it available as `context.session` (or `context.get(Session)`)
|
|
48
|
+
- Automatically saves session changes and sets the cookie on responses
|
|
49
|
+
|
|
50
|
+
Use `context.session` (or `context.get(Session)`) for normal session reads and writes.
|
|
51
|
+
|
|
52
|
+
Note: The session cookie must be signed for security. This prevents tampering with the session data on the client.
|
|
53
|
+
|
|
54
|
+
### Login/Logout Flow
|
|
55
|
+
|
|
56
|
+
A basic login/logout flow could look like this:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import * as res from 'remix/router/response-helpers'
|
|
60
|
+
|
|
61
|
+
router.get('/login', ({ session }) => {
|
|
62
|
+
let error = session.get('error')
|
|
63
|
+
return res.html(`
|
|
64
|
+
<html>
|
|
65
|
+
<body>
|
|
66
|
+
<h1>Login</h1>
|
|
67
|
+
${typeof error === 'string' ? <div class="error">${error}</div> : null}
|
|
68
|
+
<form method="POST" action="/login">
|
|
69
|
+
<input type="text" name="username" placeholder="Username" />
|
|
70
|
+
<input type="password" name="password" placeholder="Password" />
|
|
71
|
+
<button type="submit">Login</button>
|
|
72
|
+
</form>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
75
|
+
`)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
router.post('/login', ({ get, session }) => {
|
|
79
|
+
let formData = get(FormData)
|
|
80
|
+
let username = formData.get('username')
|
|
81
|
+
let password = formData.get('password')
|
|
82
|
+
|
|
83
|
+
let user = authenticateUser(username, password)
|
|
84
|
+
if (!user) {
|
|
85
|
+
session.flash('error', 'Invalid username or password')
|
|
86
|
+
return res.redirect('/login')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
session.regenerateId()
|
|
90
|
+
session.set('userId', user.id)
|
|
91
|
+
|
|
92
|
+
return res.redirect('/dashboard')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
router.post('/logout', ({ session }) => {
|
|
96
|
+
session.destroy()
|
|
97
|
+
return res.redirect('/')
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Related Packages
|
|
102
|
+
|
|
103
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API
|
|
104
|
+
- [`session`](https://github.com/remix-run/remix/tree/main/packages/session) - Session management and storage
|
|
105
|
+
- [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) - Cookie parsing and serialization
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|