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.
- package/README.md +4 -2
- package/dist/assets/types/hmr.d.ts +2 -0
- package/dist/cli-entry.js +1 -1
- package/dist/data-table/cli.d.ts +2 -0
- package/dist/data-table/cli.d.ts.map +1 -0
- package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
- package/dist/node-hmr/runtime.d.ts +2 -0
- package/dist/node-hmr/runtime.d.ts.map +1 -0
- package/dist/node-hmr/runtime.js +2 -0
- package/dist/node-hmr/types.d.ts +2 -0
- package/dist/node-hmr.d.ts +2 -0
- package/dist/node-hmr.d.ts.map +1 -0
- package/dist/{ui/glyph.js → node-hmr.js} +1 -1
- 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/dev/refresh.d.ts +2 -0
- package/dist/ui/dev/refresh.d.ts.map +1 -0
- package/dist/ui/dev/refresh.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/menu/primitives.js +2 -0
- 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/tabs/primitives.js +2 -0
- package/dist/ui/tabs.d.ts +2 -0
- package/dist/ui/tabs.d.ts.map +1 -0
- package/{src/ui/theme.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/dist/ui-hmr/assets.d.ts +2 -0
- package/dist/ui-hmr/assets.d.ts.map +1 -0
- package/dist/ui-hmr/assets.js +2 -0
- package/dist/ui-hmr/node.d.ts +3 -0
- package/dist/ui-hmr/node.d.ts.map +1 -0
- package/dist/ui-hmr/node.js +3 -0
- package/dist/ui-hmr/runtime/browser.d.ts +2 -0
- package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/browser.js +2 -0
- package/dist/ui-hmr/runtime/server.d.ts +2 -0
- package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/server.js +2 -0
- package/dist/ui-hmr.d.ts +2 -0
- package/dist/ui-hmr.d.ts.map +1 -0
- package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
- package/package.json +122 -142
- package/src/assets/README.md +322 -56
- package/src/assets/types/hmr.d.ts +2 -0
- package/src/cli/README.md +105 -1
- package/src/cookie/README.md +4 -4
- package/src/data-table/README.md +202 -68
- package/src/data-table/cli.ts +2 -0
- package/src/data-table-mysql/README.md +46 -17
- package/src/data-table-postgres/README.md +39 -13
- package/src/data-table-sqlite/README.md +38 -20
- package/src/fetch-proxy/README.md +25 -0
- package/src/form-data-parser/README.md +4 -4
- package/src/mime/README.md +8 -1
- package/src/node-fetch-server/README.md +39 -13
- package/src/node-hmr/README.md +307 -0
- package/src/node-hmr/runtime.ts +2 -0
- package/src/node-hmr/types.d.ts +2 -0
- package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
- package/src/route-pattern/README.md +141 -13
- package/src/session/README.md +1 -1
- package/src/session-middleware/README.md +9 -7
- package/src/test/README.md +161 -115
- package/src/ui/README.md +116 -157
- 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/dev/refresh.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/src/ui/menu/primitives.ts +2 -0
- 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/src/ui/tabs/primitives.ts +2 -0
- package/src/ui/tabs.ts +2 -0
- package/src/ui/test/README.md +151 -60
- 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/src/ui-hmr/README.md +119 -0
- package/{dist/ui/separator.js → src/ui-hmr/assets.ts} +1 -1
- package/src/ui-hmr/node.ts +3 -0
- package/src/ui-hmr/runtime/browser.ts +2 -0
- package/src/ui-hmr/runtime/server.ts +2 -0
- package/src/ui-hmr.ts +2 -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
|
@@ -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)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// IMPORTANT: This file is auto-generated, please do not edit manually.
|
|
2
|
-
export * from '@remix-run/
|
|
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** -
|
|
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/:
|
|
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
|
-
|
|
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
|
|
263
|
+
matcher.add('files/readme', null)
|
|
195
264
|
|
|
196
|
-
let matches = matcher.matchAll('https://example.com/files/readme
|
|
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
|
|
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(
|
|
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('
|
|
352
|
+
let pattern = RoutePattern.parse('://:tenant.example.com/blog/:slug(/*path)')
|
|
247
353
|
// ^? RoutePattern
|
|
248
354
|
|
|
249
355
|
pattern.toString()
|
|
250
|
-
// '
|
|
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.
|
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
|
|
|
@@ -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
|
|
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
|
|
65
|
+
return createHtmlResponse(html`
|
|
64
66
|
<html>
|
|
65
67
|
<body>
|
|
66
68
|
<h1>Login</h1>
|
|
67
|
-
${typeof error === 'string' ?
|
|
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
|
|
88
|
+
return redirect('/login')
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
session.regenerateId()
|
|
90
92
|
session.set('userId', user.id)
|
|
91
93
|
|
|
92
|
-
return
|
|
94
|
+
return redirect('/dashboard')
|
|
93
95
|
})
|
|
94
96
|
|
|
95
97
|
router.post('/logout', ({ session }) => {
|
|
96
98
|
session.destroy()
|
|
97
|
-
return
|
|
99
|
+
return redirect('/')
|
|
98
100
|
})
|
|
99
101
|
```
|
|
100
102
|
|