remix 3.0.0-beta.4 → 3.0.0-beta.6

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.
Files changed (143) hide show
  1. package/README.md +4 -2
  2. package/dist/assets/types/hmr.d.ts +2 -0
  3. package/dist/cli-entry.js +1 -1
  4. package/dist/data-table/cli.d.ts +2 -0
  5. package/dist/data-table/cli.d.ts.map +1 -0
  6. package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
  7. package/dist/node-hmr/runtime.d.ts +2 -0
  8. package/dist/node-hmr/runtime.d.ts.map +1 -0
  9. package/dist/node-hmr/runtime.js +2 -0
  10. package/dist/node-hmr/types.d.ts +2 -0
  11. package/dist/node-hmr.d.ts +2 -0
  12. package/dist/node-hmr.d.ts.map +1 -0
  13. package/dist/{ui/glyph.js → node-hmr.js} +1 -1
  14. package/dist/ui/accordion/primitives.d.ts +2 -0
  15. package/dist/ui/accordion/primitives.d.ts.map +1 -0
  16. package/dist/ui/accordion/primitives.js +2 -0
  17. package/dist/ui/button.d.ts +1 -0
  18. package/dist/ui/button.d.ts.map +1 -1
  19. package/dist/ui/button.js +1 -0
  20. package/dist/ui/checkbox.d.ts +3 -0
  21. package/dist/ui/checkbox.d.ts.map +1 -0
  22. package/dist/ui/checkbox.js +3 -0
  23. package/dist/ui/combobox/primitives.d.ts +2 -0
  24. package/dist/ui/combobox/primitives.d.ts.map +1 -0
  25. package/dist/ui/combobox/primitives.js +2 -0
  26. package/dist/ui/dev/refresh.d.ts +2 -0
  27. package/dist/ui/dev/refresh.d.ts.map +1 -0
  28. package/dist/ui/dev/refresh.js +2 -0
  29. package/dist/ui/input.d.ts +3 -0
  30. package/dist/ui/input.d.ts.map +1 -0
  31. package/dist/ui/input.js +3 -0
  32. package/dist/ui/menu/primitives.d.ts +2 -0
  33. package/dist/ui/menu/primitives.d.ts.map +1 -0
  34. package/dist/ui/menu/primitives.js +2 -0
  35. package/dist/ui/radio.d.ts +3 -0
  36. package/dist/ui/radio.d.ts.map +1 -0
  37. package/dist/ui/radio.js +3 -0
  38. package/dist/ui/select/primitives.d.ts +2 -0
  39. package/dist/ui/select/primitives.d.ts.map +1 -0
  40. package/dist/ui/select/primitives.js +2 -0
  41. package/dist/ui/tabs/primitives.d.ts +2 -0
  42. package/dist/ui/tabs/primitives.d.ts.map +1 -0
  43. package/dist/ui/tabs/primitives.js +2 -0
  44. package/dist/ui/tabs.d.ts +2 -0
  45. package/dist/ui/tabs.d.ts.map +1 -0
  46. package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
  47. package/dist/ui/toggle/primitives.d.ts +2 -0
  48. package/dist/ui/toggle/primitives.d.ts.map +1 -0
  49. package/dist/ui/toggle/primitives.js +2 -0
  50. package/dist/ui/toggle.d.ts +3 -0
  51. package/dist/ui/toggle.d.ts.map +1 -0
  52. package/dist/ui/toggle.js +3 -0
  53. package/dist/ui-hmr/assets.d.ts +2 -0
  54. package/dist/ui-hmr/assets.d.ts.map +1 -0
  55. package/dist/ui-hmr/assets.js +2 -0
  56. package/dist/ui-hmr/node.d.ts +3 -0
  57. package/dist/ui-hmr/node.d.ts.map +1 -0
  58. package/dist/ui-hmr/node.js +3 -0
  59. package/dist/ui-hmr/runtime/browser.d.ts +2 -0
  60. package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
  61. package/dist/ui-hmr/runtime/browser.js +2 -0
  62. package/dist/ui-hmr/runtime/server.d.ts +2 -0
  63. package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
  64. package/dist/ui-hmr/runtime/server.js +2 -0
  65. package/dist/ui-hmr.d.ts +2 -0
  66. package/dist/ui-hmr.d.ts.map +1 -0
  67. package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
  68. package/package.json +122 -142
  69. package/src/assets/README.md +322 -56
  70. package/src/assets/types/hmr.d.ts +2 -0
  71. package/src/cli/README.md +105 -1
  72. package/src/cookie/README.md +4 -4
  73. package/src/data-table/README.md +202 -68
  74. package/src/data-table/cli.ts +2 -0
  75. package/src/data-table-mysql/README.md +46 -17
  76. package/src/data-table-postgres/README.md +39 -13
  77. package/src/data-table-sqlite/README.md +38 -20
  78. package/src/fetch-proxy/README.md +25 -0
  79. package/src/form-data-parser/README.md +4 -4
  80. package/src/mime/README.md +8 -1
  81. package/src/node-fetch-server/README.md +39 -13
  82. package/src/node-hmr/README.md +307 -0
  83. package/src/node-hmr/runtime.ts +2 -0
  84. package/src/node-hmr/types.d.ts +2 -0
  85. package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
  86. package/src/route-pattern/README.md +141 -13
  87. package/src/session/README.md +1 -1
  88. package/src/session-middleware/README.md +9 -7
  89. package/src/test/README.md +161 -115
  90. package/src/ui/README.md +116 -157
  91. package/src/ui/accordion/README.md +50 -14
  92. package/src/ui/accordion/primitives/README.md +202 -0
  93. package/src/ui/accordion/primitives.ts +2 -0
  94. package/src/ui/anchor/README.md +37 -2
  95. package/src/ui/breadcrumbs/README.md +4 -4
  96. package/src/ui/button/README.md +26 -26
  97. package/src/ui/button.ts +1 -0
  98. package/src/ui/checkbox/README.md +59 -0
  99. package/src/ui/checkbox.ts +3 -0
  100. package/src/ui/combobox/README.md +58 -9
  101. package/src/ui/combobox/primitives/README.md +194 -0
  102. package/src/ui/combobox/primitives.ts +2 -0
  103. package/src/ui/dev/refresh.ts +2 -0
  104. package/src/ui/input/README.md +52 -0
  105. package/src/ui/input.ts +3 -0
  106. package/src/ui/listbox/README.md +9 -41
  107. package/src/ui/menu/README.md +55 -14
  108. package/src/ui/menu/primitives/README.md +161 -0
  109. package/src/ui/menu/primitives.ts +2 -0
  110. package/src/ui/popover/README.md +20 -39
  111. package/src/ui/radio/README.md +53 -0
  112. package/src/ui/radio.ts +3 -0
  113. package/src/ui/select/README.md +29 -19
  114. package/src/ui/select/primitives/README.md +117 -0
  115. package/src/ui/select/primitives.ts +2 -0
  116. package/src/ui/tabs/README.md +141 -0
  117. package/src/ui/tabs/primitives/README.md +141 -0
  118. package/src/ui/tabs/primitives.ts +2 -0
  119. package/src/ui/tabs.ts +2 -0
  120. package/src/ui/test/README.md +151 -60
  121. package/src/ui/toggle/README.md +56 -0
  122. package/src/ui/toggle/primitives/README.md +56 -0
  123. package/src/ui/toggle/primitives.ts +2 -0
  124. package/src/ui/toggle.ts +3 -0
  125. package/src/ui-hmr/README.md +119 -0
  126. package/{dist/ui/separator.js → src/ui-hmr/assets.ts} +1 -1
  127. package/src/ui-hmr/node.ts +3 -0
  128. package/src/ui-hmr/runtime/browser.ts +2 -0
  129. package/src/ui-hmr/runtime/server.ts +2 -0
  130. package/src/ui-hmr.ts +2 -0
  131. package/dist/ui/glyph.d.ts +0 -2
  132. package/dist/ui/glyph.d.ts.map +0 -1
  133. package/dist/ui/scroll-lock.d.ts +0 -2
  134. package/dist/ui/scroll-lock.d.ts.map +0 -1
  135. package/dist/ui/separator.d.ts +0 -2
  136. package/dist/ui/separator.d.ts.map +0 -1
  137. package/dist/ui/theme.d.ts +0 -2
  138. package/dist/ui/theme.d.ts.map +0 -1
  139. package/src/ui/glyph/README.md +0 -72
  140. package/src/ui/scroll-lock/README.md +0 -33
  141. package/src/ui/scroll-lock.ts +0 -2
  142. package/src/ui/separator.ts +0 -2
  143. package/src/ui/theme/README.md +0 -103
@@ -0,0 +1,307 @@
1
+ # node-hmr
2
+
3
+ Run Node.js applications with Hot Module Reloading.
4
+
5
+ ## Features
6
+
7
+ - **HMR Runtime**: Provides an `import.meta.hot` API for modules that can handle hot updates
8
+ - **Module Hook Friendly**: Use Node's module customization hooks API to automatically insert `import.meta.hot` usage
9
+ - **Restart Fallback**: Restarts the child Node process when updates aren't accepted
10
+ - **Fetch Proxy Support**: Wrap fetch handlers so requests are delayed/retried during server updates/restarts
11
+ - **Browser HMR Integration**: Optionally hosts browser HMR coordination that survives child restarts
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm i remix
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Create a development script that starts your app server with HMR enabled, along with any additional Node args, such as the `--import` flag to provide [Node module customization hooks](https://nodejs.org/api/module.html#customization-hooks) for [JSX syntax support](https://github.com/remix-run/remix/tree/main/packages/node-tsx) and [Remix component HMR](https://github.com/remix-run/remix/tree/main/packages/ui-hmr):
22
+
23
+ ```ts
24
+ // hmr.ts
25
+ import { run } from 'remix/node-hmr'
26
+
27
+ run('./server.ts', {
28
+ nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
29
+ watch: {
30
+ ignore: ['**/node_modules/**'],
31
+ },
32
+ })
33
+ ```
34
+
35
+ Then run the script with Node:
36
+
37
+ ```json
38
+ {
39
+ "scripts": {
40
+ "hmr": "NODE_ENV=development node hmr.ts"
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Fetch Proxy Support
46
+
47
+ During development, server updates can briefly leave your app unable to handle requests. In a server-only context, requests may be rejected while the child server is restarting. In a browser context, the browser may refresh or revalidate at the same time as a server restart, which can result in failed requests or a broken page.
48
+
49
+ A stable proxy server can avoid this by continuing to listen on the public port while `node-hmr` updates the child server behind it. `createHmrReadyFetch()` works with any fetch handler, so you can compose it with `createFetchProxy()` from [`remix/fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) to forward requests to the child server while delaying or retrying requests during updates.
50
+
51
+ ```ts
52
+ // hmr.ts
53
+ import * as http from 'node:http'
54
+
55
+ import { createFetchProxy } from 'remix/fetch-proxy'
56
+ import { run, createHmrReadyFetch } from 'remix/node-hmr'
57
+ import { createRequestListener } from 'remix/node-fetch-server'
58
+
59
+ const hmrProxyPort = 44100
60
+ const appPort = 44101
61
+
62
+ const hmrRunner = run('./server.ts', {
63
+ env: {
64
+ ...process.env,
65
+ PORT: String(appPort),
66
+ },
67
+ nodeArgs: ['--import', 'remix/node-tsx'],
68
+ })
69
+
70
+ const proxyFetch = createFetchProxy(`http://127.0.0.1:${appPort}`, {
71
+ xForwardedHeaders: true,
72
+ })
73
+
74
+ const server = http.createServer(createRequestListener(createHmrReadyFetch(hmrRunner, proxyFetch)))
75
+
76
+ server.listen(hmrProxyPort)
77
+ ```
78
+
79
+ By default, `createHmrReadyFetch()` retries `GET` and `HEAD` requests when the wrapped fetch handler throws or returns a `502`, `503`, or `504` response, but only if the server updated or restarted while the request was in flight. You can customize this policy with `shouldRetry`:
80
+
81
+ ```ts
82
+ let fetchWhenReady = createHmrReadyFetch(hmrRunner, proxyFetch, {
83
+ shouldRetry({ request, response }) {
84
+ if (request.method !== 'GET' && request.method !== 'HEAD') return false
85
+
86
+ return response === undefined || [502, 503, 504].includes(response.status)
87
+ },
88
+ })
89
+ ```
90
+
91
+ ## Browser HMR Integration
92
+
93
+ `node-hmr` can coordinate browser-facing HMR alongside server HMR. The parent process hosts the browser event stream, tracks files reported by asset servers in the child process, sends matching file events back to the child runtime, and emits the resulting browser updates to connected clients.
94
+
95
+ This is co-ordinated through the use of a browser HMR channel which can be created within the app server when running in `node-hmr` via the `remix/node-hmr/runtime` import:
96
+
97
+ ```ts
98
+ import { createBrowserHmrChannel } from 'remix/node-hmr/runtime'
99
+
100
+ let browserHmrChannel = await createBrowserHmrChannel()
101
+ ```
102
+
103
+ The `remix/node-hmr/runtime` API is only available inside a child process supervised by `node-hmr`. Importing it outside `node-hmr` throws. Supervised child processes automatically receive the `REMIX_NODE_HMR` environment variable which you can check before dynamically importing the runtime API:
104
+
105
+ ```ts
106
+ if (process.env.REMIX_NODE_HMR) {
107
+ let { createBrowserHmrChannel } = await import('remix/node-hmr/runtime')
108
+ let browserHmrChannel = await createBrowserHmrChannel()
109
+ }
110
+ ```
111
+
112
+ A browser HMR channel is scoped to the current child process. It gives browser HMR tooling an EventSource URL, a way to report the files it wants watched, and a way to respond to file changes with browser HMR events.
113
+
114
+ Browser asset servers can use this API to co-ordinate browser HMR with the server, for example, [`remix/assets`](https://github.com/remix-run/remix/tree/main/packages/assets) via its `hmr` option to `createAssetServer`:
115
+
116
+ ```ts
117
+ import { createAssetServer } from 'remix/assets'
118
+
119
+ let isDevelopment = process.env.NODE_ENV === 'development'
120
+
121
+ let assetServer = createAssetServer({
122
+ basePath: '/assets',
123
+ fileMap: { '/app/*path': 'app/*path' },
124
+ allowFiles: ['app/routes.ts', 'app/**/public/**'],
125
+ denyFiles: ['app/**/*.test.*'],
126
+ hmr:
127
+ isDevelopment && process.env.REMIX_NODE_HMR
128
+ ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
129
+ : undefined,
130
+ watch: isDevelopment,
131
+ })
132
+ ```
133
+
134
+ When `node-hmr` hot updates or restarts server code in a way that should refresh server-rendered UI, it sends a `server:update` event to connected clients.
135
+
136
+ Call `emitServerReady()` when your app server is ready to receive requests. This lets the parent process delay browser `server:update` events until a restarted app server has finished listening:
137
+
138
+ ```ts
139
+ server.listen(port, () => {
140
+ if (process.env.REMIX_NODE_HMR) {
141
+ import('remix/node-hmr/runtime').then((nodeHmr) => nodeHmr.emitServerReady())
142
+ }
143
+ })
144
+ ```
145
+
146
+ ## File Watching
147
+
148
+ The file system is watched automatically so server source changes can hot update or restart the child process.
149
+
150
+ You can optionally provide an array of glob patterns to the `watch.ignore` option.
151
+
152
+ ```ts
153
+ import { run } from 'remix/node-hmr'
154
+
155
+ run('./server.ts', {
156
+ nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
157
+ watch: {
158
+ ignore: ['**/node_modules/**'],
159
+ },
160
+ })
161
+ ```
162
+
163
+ You can also configure polling behavior. Polling defaults to `true` on Windows and `false` elsewhere:
164
+
165
+ ```ts
166
+ import { run } from 'remix/node-hmr'
167
+
168
+ run('./server.ts', {
169
+ nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
170
+ watch: {
171
+ poll: true,
172
+ pollInterval: 100,
173
+ },
174
+ })
175
+ ```
176
+
177
+ ## `import.meta.hot`
178
+
179
+ The `import.meta.hot` API provided by `node-hmr` is a small runtime contract for modules that can handle updates without restarting the process. It is primarily intended for transforms like [remix/ui-hmr](https://github.com/remix-run/remix/tree/main/packages/ui-hmr), but it can also be used directly.
180
+
181
+ To type `import.meta.hot`, add the HMR types to your TypeScript config:
182
+
183
+ ```json
184
+ {
185
+ "compilerOptions": {
186
+ "types": ["remix/node-hmr/types"]
187
+ }
188
+ }
189
+ ```
190
+
191
+ HMR accept calls are statically analyzed. Write them directly as `import.meta.hot.accept(...)`. Dependency accepts must use string literals or arrays of string literals; do not alias `import.meta.hot` or pass dynamically constructed dependency lists.
192
+
193
+ ```ts
194
+ if (import.meta.hot) {
195
+ import.meta.hot.accept()
196
+ }
197
+ ```
198
+
199
+ For consistency with browser HMR environments, `node-hmr` also implements `import.meta.hot.on(...)`, but no events are fired in server modules.
200
+
201
+ ### Accepting updates
202
+
203
+ Calling `accept()` makes the current module an HMR boundary. When the module changes, `node-hmr` evaluates the updated module and calls your callback with its exports.
204
+
205
+ ```ts
206
+ export let value = 1
207
+
208
+ if (import.meta.hot) {
209
+ import.meta.hot.accept((module) => {
210
+ if (typeof module.value !== 'number') {
211
+ import.meta.hot?.invalidate('Updated module no longer exports value')
212
+ return
213
+ }
214
+
215
+ value = module.value
216
+ })
217
+ }
218
+ ```
219
+
220
+ You can also accept updates from direct dependencies.
221
+
222
+ ```ts
223
+ import { value } from './value.ts'
224
+
225
+ let currentValue = value
226
+
227
+ export function readValue() {
228
+ return currentValue
229
+ }
230
+
231
+ if (import.meta.hot) {
232
+ import.meta.hot.accept('./value.ts', (module) => {
233
+ if (typeof module.value !== 'number') {
234
+ import.meta.hot?.invalidate('Updated dependency no longer exports value')
235
+ return
236
+ }
237
+
238
+ currentValue = module.value
239
+ })
240
+ }
241
+ ```
242
+
243
+ Multiple dependencies can be accepted at once. The callback receives an array where only the changed dependency is defined.
244
+
245
+ ```ts
246
+ if (import.meta.hot) {
247
+ import.meta.hot.accept(['./one.ts', './two.ts'], ([oneModule, twoModule]) => {
248
+ // oneModule is defined when ./one.ts changed.
249
+ // twoModule is defined when ./two.ts changed.
250
+ })
251
+ }
252
+ ```
253
+
254
+ ### Cleaning up
255
+
256
+ Register cleanup that should run before the module is replaced or disposed.
257
+
258
+ ```ts
259
+ let interval = setInterval(refreshCache, 30_000)
260
+
261
+ if (import.meta.hot) {
262
+ import.meta.hot.dispose(() => {
263
+ clearInterval(interval)
264
+ })
265
+ }
266
+ ```
267
+
268
+ The `data` object is preserved across updates for the same module. Use it for small pieces of state.
269
+
270
+ ```ts
271
+ let count = Number(import.meta.hot?.data.count ?? 0)
272
+
273
+ export function increment() {
274
+ count++
275
+ }
276
+
277
+ if (import.meta.hot) {
278
+ import.meta.hot.dispose((data) => {
279
+ data.count = count
280
+ })
281
+ }
282
+ ```
283
+
284
+ ### Invalidating updates
285
+
286
+ Call `invalidate()` inside an accept callback when the update cannot be applied safely. `node-hmr` falls back to a process restart.
287
+
288
+ ```ts
289
+ if (import.meta.hot) {
290
+ import.meta.hot.accept((module) => {
291
+ if (typeof module.value !== 'number') {
292
+ import.meta.hot?.invalidate('Updated module no longer exports value')
293
+ return
294
+ }
295
+ })
296
+ }
297
+ ```
298
+
299
+ ## Related Packages
300
+
301
+ - [`assets`](https://github.com/remix-run/remix/tree/main/packages/assets) - Consumes browser HMR channels for coordinating server and browser HMR updates
302
+ - [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - Creates fetch handlers for forwarding requests to another server
303
+ - [`ui-hmr`](https://github.com/remix-run/remix/tree/main/packages/ui-hmr) - Provides code transforms and runtime for HMR for Remix UI components
304
+
305
+ ## License
306
+
307
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/node-hmr/runtime'
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export type * from '@remix-run/node-hmr/types'
@@ -1,2 +1,2 @@
1
1
  // IMPORTANT: This file is auto-generated, please do not edit manually.
2
- export * from '@remix-run/ui/theme';
2
+ export * from '@remix-run/node-hmr'
@@ -8,7 +8,7 @@ Type-safe URL matching and href generation for JavaScript. `route-pattern` suppo
8
8
  - **Expressive** - Variables, wildcards, optionals, and search constraints
9
9
  - **Full URL support** - Match protocol, hostname, port, pathname, and search
10
10
  - **Simple & deterministic ranking** - Predictable left-to-right priority for static, variable, and wildcard patterns
11
- - **Fast** - Trie-based matching for scalable performance
11
+ - **Fast** - Indexed, bounded-state matching without variant expansion or regex backtracking
12
12
  - **Modular** - Import only the features you need to for smaller bundles
13
13
  - **Runtime agnostic** - Works across Node.js, Bun, Deno, Cloudflare Workers, and browsers
14
14
 
@@ -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
@@ -82,9 +84,14 @@ Protocol must be `http`, `https`, or `http(s)`:
82
84
 
83
85
  ```ts
84
86
  'users/:id' // matches /users/123
85
- 'blog/:year-:month-:day/:slug' // matches /blog/2024-01-15/hello
87
+ 'blog/:date/:slug' // matches /blog/2024-01-15/hello
88
+ 'files/:name.:ext' // matches /files/readme.md
86
89
  ```
87
90
 
91
+ Pathname variables possessively capture the largest non-empty run up to `/` or `.`. Hyphens are data, so UUIDs and slugs remain intact. A variable may have static text before it, but every path through following optionals must reach `/`, `.`, a wildcard, or the end of the hostname or pathname. Capture an inseparable value such as a date with one variable instead of `:year-:month-:day`.
92
+
93
+ Raw `/` and `.` are structural delimiters. Their percent-encoded forms remain data and are decoded in the resulting param. Static pattern text may use either decoded text or percent encoding, so `/café` and `/caf%C3%A9` match the same pathname text.
94
+
88
95
  **Wildcards** match multi-segment paths using `*name`:
89
96
 
90
97
  ```ts
@@ -93,6 +100,8 @@ Protocol must be `http`, `https`, or `http(s)`:
93
100
  'files/*' // matches any path under /files, but doesn't capture the wildcard value
94
101
  ```
95
102
 
103
+ Patterns may contain any number of wildcards when static text or a delimiter separates them. Adjacent wildcards such as `*left*right` are rejected because their capture boundary is ambiguous.
104
+
96
105
  **Optionals** make parts optional using `()`:
97
106
 
98
107
  ```ts
@@ -102,7 +111,9 @@ Protocol must be `http`, `https`, or `http(s)`:
102
111
  'api(/v:major(.:minor))' // matches /api, /api/v2, /api/v2.1
103
112
  ```
104
113
 
105
- While variables, wilcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
114
+ Optionals compile as state branches rather than concrete variants, so independent and nested optionals do not cause exponential matcher construction. Empty optionals and adjacent optional branches that give the same URL different capture schemas are rejected.
115
+
116
+ While variables, wildcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
106
117
 
107
118
  ```ts
108
119
  ':tenant.example.com/dashboard' // matches acme.example.com/dashboard
@@ -111,6 +122,19 @@ While variables, wilcards, and optionals are most prevalent in pathnames, you ca
111
122
  '(:locale.)example.com/docs(/:section)' // matches en.example.com/docs, en.example.com/docs/guides
112
123
  ```
113
124
 
125
+ Capture names may repeat. `params` uses the last participating capture in pattern order, while `paramsMeta` retains every participating capture:
126
+
127
+ ```ts
128
+ let matcher = createMatcher('/:id/:id')
129
+ let match = matcher.match('https://example.com/first/second')
130
+
131
+ match?.params
132
+ // { id: 'second' }
133
+
134
+ match?.paramsMeta.pathname.map(({ name, value }) => ({ name, value }))
135
+ // [{ name: 'id', value: 'first' }, { name: 'id', value: 'second' }]
136
+ ```
137
+
114
138
  **Escape characters** with `\`:
115
139
 
116
140
  ```ts
@@ -149,6 +173,22 @@ docsMatcher.match(url)?.params
149
173
  // Type safe params ^? { tenant: string | undefined, path: string, ext: string } | undefined
150
174
  ```
151
175
 
176
+ Matchers accept absolute URL strings or `URL` objects. To match a relative URL reference, pass an absolute `baseURL`; the input is resolved with the same semantics as `new URL(input, baseURL)`, and the resolved URL is returned on the match.
177
+
178
+ ```ts
179
+ let match = blogMatcher.match('../blog/v3', {
180
+ baseURL: 'https://example.com/admin/settings',
181
+ })
182
+
183
+ match?.params
184
+ // { slug: 'v3' }
185
+
186
+ match?.url.href
187
+ // 'https://example.com/blog/v3'
188
+ ```
189
+
190
+ This works for root-relative, path-relative, query-relative, and network-path references. Without `baseURL`, string inputs must still be absolute.
191
+
152
192
  ### Match against multiple patterns
153
193
 
154
194
  Use `createMultiMatcher` when you need to match many patterns and attach your own data to each match.
@@ -172,17 +212,46 @@ matcher.match('https://example.com/api/v2/users/profile')
172
212
 
173
213
  The matched pattern is only known at runtime, so matched `params` are not inferred when matching with `createMultiMatcher`.
174
214
 
215
+ Each match returns:
216
+
217
+ - `url`: the `URL` object that was matched
218
+ - `pattern`: the matched `RoutePattern`
219
+ - `data`: the data attached with `matcher.add(pattern, data)`
220
+ - `params`: captured param values
221
+ - `paramsMeta`: hostname and pathname param metadata
222
+
223
+ `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.
224
+
225
+ Set `ignoreCase: true` to make pathname matching case-insensitive. Hostname matching is always case-insensitive, and search constraints are always case-sensitive.
226
+
227
+ ```ts
228
+ let matcher = createMatcher('/Docs/:slug', { ignoreCase: true })
229
+
230
+ matcher.match('https://example.com/docs/Intro')?.params
231
+ // { slug: 'Intro' }
232
+ ```
233
+
234
+ Matchers limit individual pattern size, total matcher size, and the work performed by one match. Pattern and matcher sizes are measured in UTF-8 bytes. Direct package consumers may lower or raise individual limits. Exceeding one throws `MatcherResourceError` with structured `details` instead of silently abandoning matching:
235
+
236
+ ```ts
237
+ let matcher = createMultiMatcher({
238
+ limits: { maxPatternSize: 4096, maxMatchWork: 100_000 },
239
+ })
240
+ ```
241
+
175
242
  ### Ranking matches by specificity
176
243
 
177
244
  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
245
 
246
+ - Explicit protocol and port constraints are more specific than omitted constraints.
247
+ - Static hostnames are more specific than dynamic hostnames, which are more specific than omitted hostnames.
179
248
  - Static characters are more specific than variables.
180
249
  - Variables are more specific than wildcards.
181
250
  - Earliest difference decides the winner.
182
251
 
183
252
  This is the same ranking used by `createMultiMatcher`.
184
253
 
185
- For advanced use cases, `/specificity` provides comparison utilities: `lessThan`, `greaterThan`, `equal`, `descending`, `ascending`, `compare`. For example:
254
+ 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
255
 
187
256
  ```ts
188
257
  import { createMultiMatcher } from 'remix/route-pattern/match'
@@ -191,12 +260,12 @@ import { descending } from 'remix/route-pattern/specificity'
191
260
  let matcher = createMultiMatcher()
192
261
  matcher.add('files/*path', null)
193
262
  matcher.add('files/:name', null)
194
- matcher.add('files/readme.md', null)
263
+ matcher.add('files/readme', null)
195
264
 
196
- let matches = matcher.matchAll('https://example.com/files/readme.md')
265
+ let matches = matcher.matchAll('https://example.com/files/readme')
197
266
 
198
267
  matches.sort(descending).map((match) => match.pattern.toString())
199
- // ['/files/readme.md', '/files/:name', '/files/*path']
268
+ // ['/files/readme', '/files/:name', '/files/*path']
200
269
  ```
201
270
 
202
271
  ## Generate hrefs
@@ -222,10 +291,47 @@ createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
222
291
  })
223
292
  // 'https://us-west.cdn.com/assets/images/logo.png'
224
293
 
225
- createHref('blog/:slug?ref=docs', { slug: 'v3' }, { utm_source: 'newsletter' })
294
+ createHref(
295
+ 'blog/:slug?ref=docs',
296
+ { slug: 'v3' },
297
+ {
298
+ searchParams: { utm_source: 'newsletter' },
299
+ },
300
+ )
226
301
  // '/blog/v3?utm_source=newsletter&ref=docs'
302
+
303
+ createHref('users/:id', { id: 'a.b' })
304
+ // '/users/a%2Eb' (the encoded dot remains variable data when matched)
305
+ ```
306
+
307
+ Pass `baseURL` to generate a path-relative reference to a same-origin route. Patterns with a different origin remain absolute.
308
+
309
+ ```ts
310
+ let baseURL = new URL('https://example.com/admin/settings')
311
+
312
+ createHref('users/:id', { id: '123' }, { baseURL })
313
+ // '../users/123'
314
+
315
+ createHref('https://cdn.example.com/assets/*path', { path: 'logo.svg' }, { baseURL })
316
+ // 'https://cdn.example.com/assets/logo.svg'
317
+ ```
318
+
319
+ The `searchParams` option accepts a plain object or `URLSearchParams`. Use `URLSearchParams` when duplicate keys or their order matter:
320
+
321
+ ```ts
322
+ let searchParams = new URLSearchParams([
323
+ ['tag', 'featured'],
324
+ ['tag', 'popular'],
325
+ ])
326
+
327
+ createHref('search', undefined, { searchParams })
328
+ // '/search?tag=featured&tag=popular'
227
329
  ```
228
330
 
331
+ `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.
332
+
333
+ 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.
334
+
229
335
  **Note:** optional groups without params are included in the generated href:
230
336
 
231
337
  ```ts
@@ -238,25 +344,47 @@ createHref('products(.json)')
238
344
 
239
345
  ## Parse & stringify patterns
240
346
 
241
- You can explicitly parse and stringify patterns:
347
+ 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
348
 
243
349
  ```ts
244
- import { RoutePattern } from 'remix/route-pattern'
350
+ import { getRoutePatternCaptures, RoutePattern } from 'remix/route-pattern'
245
351
 
246
- let pattern = RoutePattern.parse('://example.com/blog/:slug')
352
+ let pattern = RoutePattern.parse('://:tenant.example.com/blog/:slug(/*path)')
247
353
  // ^? RoutePattern
248
354
 
249
355
  pattern.toString()
250
- // '://example.com/blog/:slug'
356
+ // '://:tenant.example.com/blog/:slug(/*path)'
251
357
 
252
358
  pattern.toJSON()
253
- // { hostname: 'example.com', pathname: 'blog/:slug', ... }
359
+ // { hostname: ':tenant.example.com', pathname: 'blog/:slug(/*path)', ... }
360
+
361
+ getRoutePatternCaptures(pattern)
362
+ // [
363
+ // { part: 'hostname', type: ':', name: 'tenant', optional: false },
364
+ // { part: 'pathname', type: ':', name: 'slug', optional: false },
365
+ // { part: 'pathname', type: '*', name: 'path', optional: true },
366
+ // ]
254
367
  ```
255
368
 
256
369
  All APIs that take a `pattern` arg accept `string` or a parsed `RoutePattern`.
257
370
 
258
371
  **TIP:** For high-performance scenarios, you can parse patterns ahead of time to avoid reparsing them on every call.
259
372
 
373
+ `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.
374
+
375
+ The public support types are:
376
+
377
+ - `RoutePatternCapture` from `remix/route-pattern`
378
+ - `RoutePatternJSON` from `remix/route-pattern`
379
+ - `CreateHrefErrorDetails` from `remix/route-pattern/href`
380
+ - `CreateHrefOptions` and `CreateHrefSearchParams` from `remix/route-pattern/href`
381
+ - `MatchParamMeta` from `remix/route-pattern/match`
382
+ - `MatchOptions` from `remix/route-pattern/match`
383
+ - `MatcherLimits` from `remix/route-pattern/match`
384
+ - `MatcherResourceError` and `MatcherResourceErrorDetails` from `remix/route-pattern/match`
385
+
386
+ Literal patterns are validated and infer named params until the type-level parser reaches its 64-step complexity budget. Larger runtime-valid patterns remain accepted and fall back to safe general pattern types instead of risking a TypeScript excessive-instantiation error.
387
+
260
388
  ## Combine patterns
261
389
 
262
390
  `joinPatterns` builds a new pattern from a base pattern.
@@ -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
- 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.
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
 
@@ -24,7 +24,6 @@ import { session } from 'remix/middleware/session'
24
24
 
25
25
  let sessionCookie = createCookie('__session', {
26
26
  secrets: ['s3cr3t'], // session cookies must be signed!
27
- httpOnly: true,
28
27
  secure: true,
29
28
  sameSite: 'lax',
30
29
  })
@@ -50,21 +49,24 @@ The middleware:
50
49
  Use `context.session` (or `context.get(Session)`) for normal session reads and writes.
51
50
 
52
51
  Note: The session cookie must be signed for security. This prevents tampering with the session data on the client.
52
+ Session cookies are HTTP-only by default.
53
53
 
54
54
  ### Login/Logout Flow
55
55
 
56
56
  A basic login/logout flow could look like this:
57
57
 
58
58
  ```ts
59
- import * as res from 'remix/router/response-helpers'
59
+ import { html } from 'remix/html-template'
60
+ import { createHtmlResponse } from 'remix/response/html'
61
+ import { redirect } from 'remix/response/redirect'
60
62
 
61
63
  router.get('/login', ({ session }) => {
62
64
  let error = session.get('error')
63
- return res.html(`
65
+ return createHtmlResponse(html`
64
66
  <html>
65
67
  <body>
66
68
  <h1>Login</h1>
67
- ${typeof error === 'string' ? <div class="error">${error}</div> : null}
69
+ ${typeof error === 'string' ? html`<div class="error">${error}</div>` : null}
68
70
  <form method="POST" action="/login">
69
71
  <input type="text" name="username" placeholder="Username" />
70
72
  <input type="password" name="password" placeholder="Password" />
@@ -83,18 +85,18 @@ router.post('/login', ({ get, session }) => {
83
85
  let user = authenticateUser(username, password)
84
86
  if (!user) {
85
87
  session.flash('error', 'Invalid username or password')
86
- return res.redirect('/login')
88
+ return redirect('/login')
87
89
  }
88
90
 
89
91
  session.regenerateId()
90
92
  session.set('userId', user.id)
91
93
 
92
- return res.redirect('/dashboard')
94
+ return redirect('/dashboard')
93
95
  })
94
96
 
95
97
  router.post('/logout', ({ session }) => {
96
98
  session.destroy()
97
- return res.redirect('/')
99
+ return redirect('/')
98
100
  })
99
101
  ```
100
102