remix 3.0.0-beta.5 → 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/data-table/cli.js +2 -0
- 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/node-hmr.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-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/dist/ui-hmr.js +2 -0
- package/package.json +78 -126
- 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 +22 -18
- package/src/data-table-postgres/README.md +14 -14
- package/src/data-table-sqlite/README.md +38 -20
- package/src/fetch-proxy/README.md +25 -0
- package/src/node-fetch-server/README.md +11 -11
- 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/src/node-hmr.ts +2 -0
- package/src/route-pattern/README.md +91 -8
- package/src/session-middleware/README.md +9 -7
- package/src/test/README.md +161 -115
- package/src/ui/README.md +74 -1
- package/src/ui/dev/refresh.ts +2 -0
- package/src/ui/test/README.md +151 -60
- package/src/ui-hmr/README.md +119 -0
- package/src/ui-hmr/assets.ts +2 -0
- 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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# data-table-postgres
|
|
2
2
|
|
|
3
|
-
PostgreSQL
|
|
3
|
+
PostgreSQL database driver for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table), backed by `pg`.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Native `pg` Integration**:
|
|
7
|
+
- **Native `pg` Integration**: Creates a pool from `pg` configuration or uses an existing pool or client
|
|
8
8
|
- **Full `data-table` API Support**: Queries, relations, writes, and transactions
|
|
9
|
-
- **
|
|
9
|
+
- **PostgreSQL Compiler**: SQL compilation is handled automatically for PostgreSQL
|
|
10
10
|
- **Multi-Statement Migrations**: `executeScript()` runs `up.sql` / `down.sql` files natively via `pg`
|
|
11
11
|
- **Postgres Capabilities Enabled By Default**:
|
|
12
12
|
- `returning: true`
|
|
@@ -24,20 +24,20 @@ npm i remix pg
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import {
|
|
28
|
-
import { createDatabase } from 'remix/data-table'
|
|
29
|
-
import { createPostgresDatabaseAdapter } from 'remix/data-table/postgres'
|
|
27
|
+
import { createPostgresDatabase } from 'remix/data-table/postgres'
|
|
30
28
|
|
|
31
|
-
let
|
|
29
|
+
let db = createPostgresDatabase({
|
|
32
30
|
connectionString: process.env.DATABASE_URL,
|
|
33
31
|
})
|
|
34
|
-
|
|
35
|
-
let db = createDatabase(createPostgresDatabaseAdapter(pool))
|
|
36
32
|
```
|
|
37
33
|
|
|
38
34
|
Use `db.query(...)`, relation loading, and transactions from `remix/data-table`. Import any driver-specific types you need directly from `pg`.
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
Config-backed databases support `db.wipe()` and `db.reset()`. Call `await db.close()` during application shutdown to close the internally created pool. You may pass an existing `pg` pool or client when your application owns the driver lifecycle; `db.close()` leaves supplied clients alone, and destructive lifecycle methods are unavailable in that mode. `db.wipe()` requires a database name resolvable from the connection config (`database`, the path of `connectionString`, or the `PGDATABASE` environment variable) and throws when none is present.
|
|
37
|
+
|
|
38
|
+
Migration runs reserve one connection for the PostgreSQL advisory lock, migration SQL, and journal updates. Lock acquisition waits up to 60 seconds (via `lock_timeout`) and fails with an error instead of blocking forever. After a successful run the connection is unlocked and returned to the pool; if the migration or unlock fails, the reserved connection is destroyed instead of being reused, so a dirty session can never leak back into the pool. Nested migration lock acquisition throws instead of deadlocking.
|
|
39
|
+
|
|
40
|
+
## Database Capabilities
|
|
41
41
|
|
|
42
42
|
`data-table-postgres` reports this capability set by default:
|
|
43
43
|
|
|
@@ -51,7 +51,7 @@ Use `db.query(...)`, relation loading, and transactions from `remix/data-table`.
|
|
|
51
51
|
|
|
52
52
|
### Transaction Options
|
|
53
53
|
|
|
54
|
-
Transaction options are passed through to
|
|
54
|
+
Transaction options are passed through to PostgreSQL as hints.
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
await db.transaction(async (txDb) => txDb.exec('select 1'), {
|
|
@@ -77,7 +77,7 @@ Then run:
|
|
|
77
77
|
|
|
78
78
|
```sh
|
|
79
79
|
REMIX_DATA_TABLE_POSTGRES_TEST_URL=postgres://postgres:postgres@127.0.0.1:5432/remix \
|
|
80
|
-
pnpm test src/lib/
|
|
80
|
+
pnpm test src/lib/driver.integration.test.ts
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Remove the container when you are done:
|
|
@@ -90,8 +90,8 @@ podman rm -f postgres
|
|
|
90
90
|
|
|
91
91
|
- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
|
|
92
92
|
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation
|
|
93
|
-
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL
|
|
94
|
-
- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite
|
|
93
|
+
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL database driver
|
|
94
|
+
- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite database driver
|
|
95
95
|
|
|
96
96
|
## License
|
|
97
97
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# data-table-sqlite
|
|
2
2
|
|
|
3
|
-
SQLite
|
|
3
|
+
SQLite database driver for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table), backed by a synchronous SQLite client.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Native Runtime SQLite Support**:
|
|
7
|
+
- **Native Runtime SQLite Support**: Opens a configured filename with Node's `node:sqlite` or Bun's `bun:sqlite`, or uses a compatible synchronous SQLite client
|
|
8
8
|
- **Full `data-table` API Support**: Queries, relations, writes, and transactions
|
|
9
|
-
- **
|
|
9
|
+
- **SQLite Compiler**: SQL compilation is handled automatically for SQLite
|
|
10
10
|
- **Multi-Statement Migrations**: `executeScript()` runs `up.sql` / `down.sql` files via `Database.exec()`
|
|
11
11
|
- **SQLite Capabilities Enabled By Default**:
|
|
12
12
|
- `returning: true`
|
|
@@ -23,31 +23,42 @@ npm i remix
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
|
-
### Node
|
|
27
|
-
|
|
28
26
|
```ts
|
|
29
|
-
import {
|
|
30
|
-
import { createDatabase } from 'remix/data-table'
|
|
31
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
27
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
32
28
|
|
|
33
|
-
let
|
|
34
|
-
|
|
29
|
+
let db = createSqliteDatabase({
|
|
30
|
+
filename: 'app.db',
|
|
31
|
+
foreignKeys: true,
|
|
32
|
+
})
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
The config-backed database uses `node:sqlite` in Node.js and `bun:sqlite` in Bun. It supports `db.wipe()` and `db.reset()` because it can close and reopen the database file. Call `await db.close()` during application shutdown to release the connection and its file handle.
|
|
36
|
+
|
|
37
|
+
Foreign key enforcement defaults to off on every runtime. When `foreignKeys` is enabled, the database restores foreign key enforcement each time it opens the connection, including after destructive lifecycle operations.
|
|
38
|
+
|
|
39
|
+
The database also applies `pragma busy_timeout = 5000` whenever it opens the connection, so writes wait for a locked database instead of failing immediately with `SQLITE_BUSY`. Use `busyTimeout` to override the timeout in milliseconds (`0` disables the wait).
|
|
40
|
+
|
|
41
|
+
You may also pass an existing synchronous client when your application owns its lifecycle:
|
|
38
42
|
|
|
39
43
|
```ts
|
|
40
44
|
import { Database } from 'bun:sqlite'
|
|
41
|
-
import {
|
|
42
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
45
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
43
46
|
|
|
44
47
|
let sqlite = new Database('app.db')
|
|
45
|
-
let db =
|
|
48
|
+
let db = createSqliteDatabase(sqlite)
|
|
49
|
+
|
|
50
|
+
// Leaves the supplied client open.
|
|
51
|
+
await db.close()
|
|
52
|
+
|
|
53
|
+
// The application closes the client it owns.
|
|
54
|
+
sqlite.close()
|
|
46
55
|
```
|
|
47
56
|
|
|
57
|
+
Destructive lifecycle methods are unavailable when you pass an existing client.
|
|
58
|
+
|
|
48
59
|
This is a good fit for local development, embedded deployments, and single-node services. Import any driver-specific types you need directly from your runtime's SQLite module.
|
|
49
60
|
|
|
50
|
-
##
|
|
61
|
+
## Database Capabilities
|
|
51
62
|
|
|
52
63
|
`data-table-sqlite` reports this capability set by default:
|
|
53
64
|
|
|
@@ -59,23 +70,30 @@ This is a good fit for local development, embedded deployments, and single-node
|
|
|
59
70
|
|
|
60
71
|
## Advanced Usage
|
|
61
72
|
|
|
73
|
+
### Destructive Lifecycle And Locking
|
|
74
|
+
|
|
75
|
+
`db.wipe()` and `db.reset()` assume a single process owns the database file. Stop other processes before wiping: on POSIX systems another process keeps writing to the deleted inode, and on Windows an open handle blocks deletion entirely. Wiping removes the `-wal`, `-shm`, and `-journal` sidecar files along with the main database file so a freshly created database never associates with stale sidecars.
|
|
76
|
+
|
|
77
|
+
SQLite migrations run without a cross-process migration lock (`migrationLock: false`), so run migrations from one process at a time.
|
|
78
|
+
|
|
79
|
+
`filename` resolves against the current working directory — for `remix db` commands, wherever you invoke the CLI. Prefer absolute paths or paths derived from `import.meta.dirname`.
|
|
80
|
+
|
|
62
81
|
### In-Memory Database For Tests
|
|
63
82
|
|
|
64
83
|
```ts
|
|
65
84
|
import { DatabaseSync } from 'node:sqlite'
|
|
66
|
-
import {
|
|
67
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
85
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
68
86
|
|
|
69
87
|
let sqlite = new DatabaseSync(':memory:')
|
|
70
|
-
let db =
|
|
88
|
+
let db = createSqliteDatabase(sqlite)
|
|
71
89
|
```
|
|
72
90
|
|
|
73
91
|
## Related Packages
|
|
74
92
|
|
|
75
93
|
- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
|
|
76
94
|
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation
|
|
77
|
-
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL
|
|
78
|
-
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL
|
|
95
|
+
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL database driver
|
|
96
|
+
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL database driver
|
|
79
97
|
|
|
80
98
|
## License
|
|
81
99
|
|
|
@@ -7,6 +7,7 @@ HTTP proxy utilities built on the web [Fetch API](https://developer.mozilla.org/
|
|
|
7
7
|
- **Web Standards** - Built on the standard [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
|
8
8
|
- **Cookie Rewriting** - Supports rewriting `Set-Cookie` headers received from target server
|
|
9
9
|
- **Forwarding Headers** - Supports `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Port` headers
|
|
10
|
+
- **Encoding Headers** - Strips stale encoding and framing headers from proxied responses
|
|
10
11
|
- **Custom Fetch** - Supports custom `fetch` implementations
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
@@ -36,9 +37,33 @@ let title = text.match(/<title>([^<]+)<\/title>/)[1]
|
|
|
36
37
|
assert(title.includes('Remix'))
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
## Encoding and Framing Headers
|
|
41
|
+
|
|
42
|
+
Since proxying is done via `fetch` rather than raw HTTP messages, some encoding and framing headers need to be removed.
|
|
43
|
+
|
|
44
|
+
The incoming `Accept-Encoding` request header describes the final client, so it is not forwarded to the target server.
|
|
45
|
+
|
|
46
|
+
Since `fetch` can decompress upstream responses and does not expose raw HTTP transfer framing, `fetch-proxy` strips response headers that may no longer describe the returned body: `Content-Encoding`, related `Content-Length`, and `Transfer-Encoding`.
|
|
47
|
+
|
|
48
|
+
To support serving compressed responses to the final client, you'll need to compress the response after the proxy returns it, e.g. with the [`compressResponse` helper from `remix/response`](https://github.com/remix-run/remix/tree/main/packages/response#compress-responses):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createFetchProxy } from 'remix/fetch-proxy'
|
|
52
|
+
import { compressResponse } from 'remix/response/compress'
|
|
53
|
+
|
|
54
|
+
let proxy = createFetchProxy('https://remix.run')
|
|
55
|
+
|
|
56
|
+
async function handleFetch(request: Request): Promise<Response> {
|
|
57
|
+
let response = await proxy(request)
|
|
58
|
+
|
|
59
|
+
return compressResponse(response, request)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
39
63
|
## Related Packages
|
|
40
64
|
|
|
41
65
|
- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - Build HTTP servers for Node.js using the web fetch API
|
|
66
|
+
- [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - Create, transform, and compress Fetch API responses
|
|
42
67
|
|
|
43
68
|
## License
|
|
44
69
|
|
|
@@ -334,9 +334,9 @@ pnpm run bench:update-readme
|
|
|
334
334
|
|
|
335
335
|
<!-- benchmarks:start -->
|
|
336
336
|
|
|
337
|
-
Last updated: 2026-
|
|
337
|
+
Last updated: 2026-07-23T20:29:32.942Z
|
|
338
338
|
|
|
339
|
-
Environment: Darwin 25.
|
|
339
|
+
Environment: Darwin 25.4.0, Apple M5 Pro, Node.js v24.18.0
|
|
340
340
|
|
|
341
341
|
Command: `wrk -t12 -c400 -d30s`
|
|
342
342
|
|
|
@@ -346,9 +346,9 @@ Simple HTML response benchmarks without inspecting the incoming request.
|
|
|
346
346
|
|
|
347
347
|
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|
|
348
348
|
| ------------------------- | --------: | -----------: | ----------: | -----------: |
|
|
349
|
-
| `node:http` | `24.
|
|
350
|
-
| `remix/node-fetch-server` | `0.
|
|
351
|
-
| `express` | `5.2.1` | `
|
|
349
|
+
| `node:http` | `24.18.0` | `66,594` | `5.96ms` | `13.65MB` |
|
|
350
|
+
| `remix/node-fetch-server` | `0.14.0` | `61,587` | `7.88ms` | `12.51MB` |
|
|
351
|
+
| `express` | `5.2.1` | `58,424` | `8.42ms` | `14.10MB` |
|
|
352
352
|
|
|
353
353
|
### Small Body
|
|
354
354
|
|
|
@@ -356,9 +356,9 @@ POST benchmarks that read and print the request method, headers, and a small bod
|
|
|
356
356
|
|
|
357
357
|
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|
|
358
358
|
| ------------------------- | --------: | -----------: | ----------: | -----------: |
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `
|
|
359
|
+
| `node:http` | `24.18.0` | `35,303` | `15.60ms` | `7.24MB` |
|
|
360
|
+
| `express` | `5.2.1` | `32,614` | `16.93ms` | `7.87MB` |
|
|
361
|
+
| `remix/node-fetch-server` | `0.14.0` | `29,521` | `18.98ms` | `6.00MB` |
|
|
362
362
|
|
|
363
363
|
### Large Body
|
|
364
364
|
|
|
@@ -366,9 +366,9 @@ POST benchmarks that read and print the request method, headers, and a 1 MB body
|
|
|
366
366
|
|
|
367
367
|
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|
|
368
368
|
| ------------------------- | --------: | -----------: | ----------: | -----------: |
|
|
369
|
-
| `
|
|
370
|
-
| `node
|
|
371
|
-
| `express` | `5.2.1` | `1,
|
|
369
|
+
| `node:http` | `24.18.0` | `1,798` | `206.65ms` | `377.42KB` |
|
|
370
|
+
| `remix/node-fetch-server` | `0.14.0` | `1,752` | `167.69ms` | `364.40KB` |
|
|
371
|
+
| `express` | `5.2.1` | `1,731` | `223.19ms` | `427.67KB` |
|
|
372
372
|
|
|
373
373
|
<!-- benchmarks:end -->
|
|
374
374
|
|
|
@@ -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)
|
package/src/node-hmr.ts
ADDED