remix 3.0.0-beta.9 → 3.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/multiple-import-maps-polyfill.d.ts +2 -0
- package/dist/multiple-import-maps-polyfill.d.ts.map +1 -0
- package/dist/multiple-import-maps-polyfill.js +2 -0
- package/dist/spa.d.ts +2 -0
- package/dist/spa.d.ts.map +1 -0
- package/{src/static-files-middleware.ts → dist/spa.js} +1 -1
- package/dist/static-middleware.d.ts +2 -0
- package/dist/static-middleware.d.ts.map +1 -0
- package/dist/{static-files-middleware.js → static-middleware.js} +1 -1
- package/package.json +44 -33
- package/schema/remix.json +290 -0
- package/src/assets/README.md +185 -104
- package/src/auth/README.md +51 -15
- package/src/cli/README.md +52 -3
- package/src/data-table/README.md +18 -5
- package/src/fetch-router/README.md +39 -0
- package/src/multiple-import-maps-polyfill/README.md +107 -0
- package/src/multiple-import-maps-polyfill.ts +2 -0
- package/src/node-hmr/README.md +0 -1
- package/src/render-middleware/README.md +71 -56
- package/src/spa/README.md +96 -0
- package/src/spa.ts +2 -0
- package/src/{static-files-middleware → static-middleware}/README.md +1 -1
- package/src/static-middleware.ts +2 -0
- package/src/test/README.md +1 -1
- package/src/ui/README.md +90 -14
- package/src/ui/server/README.md +25 -5
- package/src/ui-hmr/README.md +6 -2
- package/dist/static-files-middleware.d.ts +0 -2
- package/dist/static-files-middleware.d.ts.map +0 -1
package/src/cli/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Command-line interface for creating and managing Remix projects.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Create new Remix projects with `npx remix@next new` or installed `remix new`
|
|
8
|
+
- List browser-reachable files and inspect URL mappings with `remix assets`
|
|
8
9
|
- Print shell completion scripts with `remix completion`
|
|
9
10
|
- Check project environment and Remix app conventions with `remix doctor`
|
|
10
11
|
- Apply available low-risk project fixes with `remix doctor --fix`
|
|
@@ -46,10 +47,13 @@ The rest of the CLI is available through the installed `remix` command:
|
|
|
46
47
|
|
|
47
48
|
```sh
|
|
48
49
|
remix new my-remix-app
|
|
50
|
+
remix assets
|
|
51
|
+
remix assets inspect /assets/app/actions/public/entry.ts
|
|
49
52
|
remix completion bash >> ~/.bashrc
|
|
50
53
|
remix doctor
|
|
51
54
|
remix doctor --fix
|
|
52
55
|
remix db migrate
|
|
56
|
+
remix db rollback
|
|
53
57
|
remix db status
|
|
54
58
|
remix db reset --force
|
|
55
59
|
remix routes
|
|
@@ -66,10 +70,13 @@ You can also run the CLI programmatically:
|
|
|
66
70
|
import { runRemix } from 'remix/cli'
|
|
67
71
|
|
|
68
72
|
await runRemix(['new', 'my-remix-app'])
|
|
73
|
+
await runRemix(['assets'])
|
|
74
|
+
await runRemix(['assets', 'inspect', '/assets/app/actions/public/entry.ts'])
|
|
69
75
|
await runRemix(['completion', 'bash'])
|
|
70
76
|
await runRemix(['doctor'])
|
|
71
77
|
await runRemix(['doctor', '--fix'])
|
|
72
78
|
await runRemix(['db', 'migrate'])
|
|
79
|
+
await runRemix(['db', 'rollback'])
|
|
73
80
|
await runRemix(['db', 'status'])
|
|
74
81
|
await runRemix(['db', 'reset', '--force'])
|
|
75
82
|
await runRemix(['routes'])
|
|
@@ -81,8 +88,12 @@ await runRemix(['version'])
|
|
|
81
88
|
|
|
82
89
|
Destructive database commands (`remix db wipe` and `remix db reset`) refuse to run without `--force`.
|
|
83
90
|
|
|
91
|
+
`remix db rollback` reverts the most recent migration by default. Use `--step <count>` or `--to <migration>` to select a bound, and use `--dry-run` to report what would be reverted without changing the database.
|
|
92
|
+
|
|
84
93
|
`runRemix()` returns the CLI exit code as a promise.
|
|
85
94
|
|
|
95
|
+
`remix assets` lists each browser-reachable asset as `URL -> file`, one per line. Run `remix assets inspect <url-or-file>` to see one asset's resolved mapping and whether it is reachable, denied, unsupported, missing, or unmapped. Denied assets also show the matching deny rule.
|
|
96
|
+
|
|
86
97
|
## Configuration
|
|
87
98
|
|
|
88
99
|
The CLI loads an optional `remix.json`. The file is parsed as JSONC, so it may contain comments and
|
|
@@ -90,7 +101,22 @@ trailing commas. Every top-level field is optional:
|
|
|
90
101
|
|
|
91
102
|
```jsonc
|
|
92
103
|
{
|
|
93
|
-
"$schema": "
|
|
104
|
+
"$schema": "./node_modules/remix/schema/remix.json",
|
|
105
|
+
|
|
106
|
+
"assets": {
|
|
107
|
+
"rootDir": ".",
|
|
108
|
+
"basePath": "/assets",
|
|
109
|
+
"mounts": {
|
|
110
|
+
"app": "app",
|
|
111
|
+
"npm": "node_modules",
|
|
112
|
+
},
|
|
113
|
+
"allowFiles": ["app/routes.ts", "app/**/public/**"],
|
|
114
|
+
"allowPackages": ["remix"],
|
|
115
|
+
"denyFiles": ["app/**/*.test.*"],
|
|
116
|
+
"files": {
|
|
117
|
+
"extensions": [".svg", ".png", ".jpg", ".woff2"],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
94
120
|
|
|
95
121
|
"db": {
|
|
96
122
|
"adapter": {
|
|
@@ -152,6 +178,8 @@ trailing commas. Every top-level field is optional:
|
|
|
152
178
|
}
|
|
153
179
|
```
|
|
154
180
|
|
|
181
|
+
The local `$schema` association uses the schema shipped with the directly installed `remix` package, so editor validation matches that version and remains available offline. It intentionally does not reference the transitive `@remix-run/cli` package, which may not be linked at the project root by package managers such as pnpm.
|
|
182
|
+
|
|
155
183
|
Explicit command flags and positional arguments override configured values. Repeated flags replace
|
|
156
184
|
configured arrays, while nested Playwright and coverage settings merge by field. Relative paths and
|
|
157
185
|
globs are resolved from the directory containing the config file. Use `remix doctor --no-strict` to
|
|
@@ -162,8 +190,9 @@ disable configured strict mode for one run.
|
|
|
162
190
|
a string or an object naming an environment variable with an optional default. `db.seed` names a
|
|
163
191
|
SQL file that `remix db seed` and `remix db reset` run against the database. Database flags such as
|
|
164
192
|
`--migrations`, `--seed`, `--journal-table`, and `--connection-env` override the corresponding
|
|
165
|
-
config for one invocation.
|
|
166
|
-
nearest `remix.json` by walking up from the
|
|
193
|
+
config for one invocation. Rollbacks also accept `--step`, `--to`, and `--dry-run`. When no global
|
|
194
|
+
`--config` is provided, database commands find the nearest `remix.json` by walking up from the
|
|
195
|
+
working directory.
|
|
167
196
|
|
|
168
197
|
Use the global `--config` option to select another JSONC file. The option itself is resolved from the
|
|
169
198
|
CLI working directory and may appear before or after the command:
|
|
@@ -177,6 +206,26 @@ A missing default `remix.json` is ignored. A missing explicitly selected file, m
|
|
|
177
206
|
unknown property, or invalid value is reported as a CLI error. The optional `$schema` field enables
|
|
178
207
|
editor completion and validation; it has no runtime effect.
|
|
179
208
|
|
|
209
|
+
Load the complete validated config from application code with `loadConfig()`:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { createAssetServer } from 'remix/assets'
|
|
213
|
+
import { loadConfig } from 'remix/cli'
|
|
214
|
+
|
|
215
|
+
let config = await loadConfig(import.meta.dirname)
|
|
216
|
+
if (config.assets === undefined) throw new Error('Missing assets configuration')
|
|
217
|
+
|
|
218
|
+
let assetServer = createAssetServer({
|
|
219
|
+
...config.assets,
|
|
220
|
+
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Pass a config file to load it directly, or a directory to search upward for the nearest
|
|
225
|
+
`remix.json`. Asset paths are resolved relative to the config file, so `config.assets` can be spread
|
|
226
|
+
directly into `createAssetServer()`. Add runtime-only options such as transforms, caches, HMR, or
|
|
227
|
+
error handlers in application code.
|
|
228
|
+
|
|
180
229
|
## License
|
|
181
230
|
|
|
182
231
|
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
package/src/data-table/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Typed relational query toolkit for JavaScript runtimes.
|
|
|
11
11
|
- **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, `hasManyThrough`, and nested eager loading
|
|
12
12
|
- **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in a transaction
|
|
13
13
|
- **First-Class Migrations**: Plain SQL `up.sql`/`down.sql` files with a journaling runner and dry-run planning
|
|
14
|
-
- **Database Lifecycle Commands**: Wipe, migrate, inspect, seed, and reset through `remix db`
|
|
14
|
+
- **Database Lifecycle Commands**: Wipe, migrate, rollback, inspect, seed, and reset through `remix db`
|
|
15
15
|
- **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\`...\`)`
|
|
16
16
|
|
|
17
17
|
`data-table` gives you two complementary APIs:
|
|
@@ -409,7 +409,7 @@ environment variable at command runtime, and paths are resolved relative to `rem
|
|
|
409
409
|
|
|
410
410
|
```jsonc
|
|
411
411
|
{
|
|
412
|
-
"$schema": "
|
|
412
|
+
"$schema": "./node_modules/remix/schema/remix.json",
|
|
413
413
|
"db": {
|
|
414
414
|
"adapter": {
|
|
415
415
|
"type": "postgres",
|
|
@@ -433,12 +433,16 @@ Run lifecycle commands through the Remix CLI:
|
|
|
433
433
|
remix db status
|
|
434
434
|
remix db migrate
|
|
435
435
|
remix db migrate --to 20260301113000_add_user_status
|
|
436
|
+
remix db rollback
|
|
437
|
+
remix db rollback --step 2
|
|
438
|
+
remix db rollback --to 20260301113000_add_user_status
|
|
439
|
+
remix db rollback --dry-run
|
|
436
440
|
remix db seed
|
|
437
|
-
remix db reset
|
|
438
|
-
remix db wipe
|
|
441
|
+
remix db reset --force
|
|
442
|
+
remix db wipe --force
|
|
439
443
|
```
|
|
440
444
|
|
|
441
|
-
`--to
|
|
445
|
+
`rollback` reverts the most recently applied migration by default. Bound it with either `--step <count>` or `--to <migration>`, which reverts through the target migration inclusively. Migration targets accept a bare migration id (`20260301113000`) or the full directory name (`20260301113000_add_user_status`). Use `--dry-run` to report what would be reverted without changing the database.
|
|
442
446
|
|
|
443
447
|
`remix db status` reports applied migrations whose files are no longer present as `missing`. If the journal table does not exist, it reports every migration as pending without creating the table. Forward migration runs stop before executing SQL when an applied journal entry is missing from the current migration set. Rollbacks skip those orphaned journal entries so migrations that are still present can be reverted.
|
|
444
448
|
|
|
@@ -473,6 +477,15 @@ for (let script of plan.sql) console.log(script)
|
|
|
473
477
|
|
|
474
478
|
`to` and `step` are mutually exclusive. Omit `journalTable` to use `data_table_migrations`.
|
|
475
479
|
|
|
480
|
+
Hosts that embed the database CLI can invoke the same rollback behavior through `runRemixDb()`:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { runRemixDb } from 'remix/data-table/cli'
|
|
484
|
+
|
|
485
|
+
await runRemixDb({ command: 'rollback', db, migrations })
|
|
486
|
+
await runRemixDb({ command: 'rollback', db, migrations, step: 2, dryRun: true })
|
|
487
|
+
```
|
|
488
|
+
|
|
476
489
|
Database drivers with migration locking run the complete migration and journal lifecycle through the
|
|
477
490
|
connection that owns the lock. This keeps advisory locks correctly paired when the driver uses a
|
|
478
491
|
connection pool, including pools configured with a single connection.
|
|
@@ -237,6 +237,45 @@ router.map(routes.contact, {
|
|
|
237
237
|
})
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
#### Method Not Allowed Responses
|
|
241
|
+
|
|
242
|
+
When a request URL matches one or more route patterns, but none of those routes are registered for the request method, the router first falls through to any less specific route that can handle the method (including `ANY` routes). If no route can handle the request, the router responds with `405 Method Not Allowed` and an [`Allow` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Allow) listing the methods that are registered for that URL, instead of treating the request as a 404.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { createRouter } from 'remix/router'
|
|
246
|
+
|
|
247
|
+
let router = createRouter()
|
|
248
|
+
|
|
249
|
+
router.get('/users/:id', ({ params }) => new Response(`User ${params.id}`))
|
|
250
|
+
router.put('/users/:id', ({ params }) => new Response(`Updated ${params.id}`))
|
|
251
|
+
|
|
252
|
+
let response = await router.fetch('https://remix.run/users/1', { method: 'DELETE' })
|
|
253
|
+
// 405 Method Not Allowed
|
|
254
|
+
// Allow: GET, HEAD, PUT
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
If you want a handler to receive every request method at a given URL instead, register an `ANY` route for it.
|
|
258
|
+
|
|
259
|
+
#### HEAD Requests
|
|
260
|
+
|
|
261
|
+
Routes registered for `GET` also serve `HEAD` requests. The `GET` handler runs as usual, and the router strips the body from its response, so a `HEAD` request observes the same status and headers as the equivalent `GET` request.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
let router = createRouter()
|
|
265
|
+
|
|
266
|
+
router.get(
|
|
267
|
+
'/report',
|
|
268
|
+
() => new Response('report data', { headers: { 'Content-Type': 'text/csv' } }),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
let response = await router.fetch('https://remix.run/report', { method: 'HEAD' })
|
|
272
|
+
// 200 OK
|
|
273
|
+
// Content-Type: text/csv
|
|
274
|
+
// (empty body)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
If you register an explicit `HEAD` route for the same pattern, it takes precedence over the `GET` route for `HEAD` requests.
|
|
278
|
+
|
|
240
279
|
### Composing Route Groups
|
|
241
280
|
|
|
242
281
|
As applications grow, it is useful to let one file own the routes for a specific area of the app while the top-level router decides where that area is mounted. Use `router.mount()` with a route installer to register a group of routes under a route pattern prefix.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# multiple-import-maps-polyfill
|
|
2
|
+
|
|
3
|
+
Polyfill for dynamic JavaScript imports that depend on import maps added after the document's initial import map. Browsers with native support for multiple import maps continue to use native dynamic imports.
|
|
4
|
+
|
|
5
|
+
The module loading logic in this package was adapted from [ES Module Shims](https://github.com/guybedford/es-module-shims) by Guy Bedford.
|
|
6
|
+
|
|
7
|
+
This package assumes the initial document contains one complete import map before any module scripts. It is designed to load dynamic imports that depend on additional import maps installed after the initial page load.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Detects native support for multiple import maps
|
|
12
|
+
- Loads dynamic imports through every import map in the document when a polyfill is required
|
|
13
|
+
- Preloads dynamic modules through the same polyfill cache
|
|
14
|
+
- Resolves native module types through import maps while leaving their loading semantics to the browser
|
|
15
|
+
- Supports import-map integrity metadata
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm i remix
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Use `importModule` in place of `import()` when a dynamic import may depend on an import map added at runtime:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { importModule } from 'remix/multiple-import-maps-polyfill'
|
|
29
|
+
|
|
30
|
+
let moduleUrl = new URL('./features/search.ts', import.meta.url).href
|
|
31
|
+
let feature = await importModule(moduleUrl)
|
|
32
|
+
feature.openSearch()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`importModule` uses native `import()` when the browser supports multiple import maps. In other browsers, it loads the module through the polyfill using every import map currently installed in the document.
|
|
36
|
+
|
|
37
|
+
Use `detectMultipleImportMapSupport` and `preloadShim` when an integration also manages module preloads. For example, configure Remix UI to load client entries discovered during navigation:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {
|
|
41
|
+
detectMultipleImportMapSupport,
|
|
42
|
+
importModule,
|
|
43
|
+
preloadShim,
|
|
44
|
+
} from 'remix/multiple-import-maps-polyfill'
|
|
45
|
+
import { run } from 'remix/ui'
|
|
46
|
+
|
|
47
|
+
run({
|
|
48
|
+
async loadModule(moduleUrl, exportName) {
|
|
49
|
+
let module = await importModule(moduleUrl)
|
|
50
|
+
let Component = module[exportName]
|
|
51
|
+
if (typeof Component !== 'function') {
|
|
52
|
+
throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
|
|
53
|
+
}
|
|
54
|
+
return Component
|
|
55
|
+
},
|
|
56
|
+
async processClientEntryPreloads(preloads) {
|
|
57
|
+
if (await detectMultipleImportMapSupport()) return preloads
|
|
58
|
+
|
|
59
|
+
preloadShim(preloads)
|
|
60
|
+
return []
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Browsers with multiple import map support retain native module loading and native `<link rel="modulepreload">` elements. Other browsers load late client entries and preloads through the polyfill.
|
|
66
|
+
|
|
67
|
+
`detectMultipleImportMapSupport()` returns a cached promise. When it detects that the polyfill is required, it begins loading the polyfill runtime in the background so the work can overlap with a later `importShim()` or `preloadShim()` call. Browsers with multiple import map support do not load the polyfill runtime.
|
|
68
|
+
|
|
69
|
+
`importShim()` always uses the polyfill to load a dynamic JavaScript import using every import map currently installed in the document. It does not detect native support. `preloadShim()` always uses the polyfill fetch cache rather than native module preload links. Like native module preloads, it fetches only the supplied module specifier or array of specifiers and caches their responses for later imports. Preload failures are ignored. `importShim()` reports the failure if the module is later required.
|
|
70
|
+
|
|
71
|
+
## Content Security Policy
|
|
72
|
+
|
|
73
|
+
Polyfilled module graphs are evaluated from blob URLs, and `es-module-lexer` compiles its parser from Wasm. Content Security Policies must allow `blob:` module scripts and Wasm compilation with `'wasm-unsafe-eval'`.
|
|
74
|
+
|
|
75
|
+
The support detector creates a Trusted Types policy named `remix/multiple-import-maps-polyfill`. Allow this policy when Trusted Types are required for scripts, for example:
|
|
76
|
+
|
|
77
|
+
```http
|
|
78
|
+
Content-Security-Policy: script-src 'self' blob: 'wasm-unsafe-eval'; require-trusted-types-for 'script'; trusted-types remix/multiple-import-maps-polyfill;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## `remix/assets` HMR Support
|
|
82
|
+
|
|
83
|
+
HMR appends mappings for updated modules to the document in additional `<script type="importmap">` elements. Configure the Remix asset server's `hmr.moduleImporter` option to use the polyfilled `importModule` function when these updates must work in browsers without native support for multiple import maps:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { createAssetServer } from 'remix/assets'
|
|
87
|
+
import { createBrowserHmrChannel } from 'remix/node-hmr/runtime'
|
|
88
|
+
|
|
89
|
+
let assets = createAssetServer({
|
|
90
|
+
hmr: {
|
|
91
|
+
channel: createBrowserHmrChannel,
|
|
92
|
+
moduleImporter: 'remix/multiple-import-maps-polyfill',
|
|
93
|
+
},
|
|
94
|
+
watch: true,
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The module importer and its dependencies must be available through the document's initial import map. Importing `remix/multiple-import-maps-polyfill` from the application's main client entry satisfies this requirement.
|
|
99
|
+
|
|
100
|
+
## Related Packages
|
|
101
|
+
|
|
102
|
+
- [`assets`](https://github.com/remix-run/remix/tree/main/packages/assets) - Compiles and serves browser assets
|
|
103
|
+
- [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) - Loads client entries discovered during navigation
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/packages/multiple-import-maps-polyfill/LICENSE)
|
package/src/node-hmr/README.md
CHANGED
|
@@ -120,7 +120,6 @@ let isDevelopment = process.env.NODE_ENV === 'development'
|
|
|
120
120
|
|
|
121
121
|
let assetServer = createAssetServer({
|
|
122
122
|
basePath: '/assets',
|
|
123
|
-
fileMap: { '/app/*path': 'app/*path' },
|
|
124
123
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
125
124
|
denyFiles: ['app/**/*.test.*'],
|
|
126
125
|
hmr:
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# render-middleware
|
|
2
2
|
|
|
3
|
-
Request-scoped
|
|
3
|
+
Request-scoped response rendering for Remix. It provides the conventional Remix UI renderer and a low-level escape hatch for custom renderers.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
7
|
+
- **Remix UI rendering** - Stream nodes to HTML responses with `render()`
|
|
8
|
+
- **Framework-owned frames** - Resolve nested and targeted `<Frame>` requests through the current router
|
|
9
|
+
- **Client entry assets** - Resolve source-based `clientEntry()` modules, import maps, and preloads through an asset server
|
|
10
|
+
- **Typed context** - Preserve renderer input and response option types on `context.render`
|
|
11
|
+
- **Custom renderers** - Install JSON, email, or other response pipelines with `renderWith()`
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
@@ -17,82 +18,96 @@ npm i remix
|
|
|
17
18
|
|
|
18
19
|
## Usage
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
Install `render()` in the router middleware stack. Pass an asset server when components use source-based client entries such as `clientEntry(import.meta.url, Component)`.
|
|
21
22
|
|
|
22
|
-
```
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
```tsx
|
|
24
|
+
import { createAssetServer } from 'remix/assets'
|
|
25
|
+
import { render } from 'remix/middleware/render'
|
|
26
|
+
import { staticFiles } from 'remix/middleware/static'
|
|
27
|
+
import { createRouter } from 'remix/router'
|
|
28
|
+
import { Frame } from 'remix/ui'
|
|
29
|
+
|
|
30
|
+
let assets = createAssetServer({
|
|
31
|
+
basePath: '/assets',
|
|
32
|
+
rootDir: process.cwd(),
|
|
33
|
+
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
34
|
+
allowPackages: ['remix'],
|
|
35
|
+
denyFiles: ['app/**/*.test.*'],
|
|
36
|
+
})
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
let router = createRouter({
|
|
39
|
+
middleware: [staticFiles('./public'), render({ assets })],
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
router.get(
|
|
43
|
+
'/assets/*path',
|
|
44
|
+
async ({ request }) =>
|
|
45
|
+
(await assets.fetch(request)) ?? new Response('Not Found', { status: 404 }),
|
|
31
46
|
)
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
router.get('/', (context) =>
|
|
49
|
+
context.render(
|
|
50
|
+
<html>
|
|
51
|
+
<body>
|
|
52
|
+
<h1>Dashboard</h1>
|
|
53
|
+
<Frame src="/activity" fallback={<p>Loading activity…</p>} />
|
|
54
|
+
</body>
|
|
55
|
+
</html>,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
```
|
|
34
59
|
|
|
35
|
-
|
|
36
|
-
middleware: [render],
|
|
37
|
-
})
|
|
60
|
+
`context.render(node, init)` returns an HTML `Response` and preserves the supplied status and headers:
|
|
38
61
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
```tsx
|
|
63
|
+
router.get('/missing', (context) =>
|
|
64
|
+
context.render(<h1>Not found</h1>, {
|
|
65
|
+
status: 404,
|
|
66
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
42
69
|
```
|
|
43
70
|
|
|
44
|
-
|
|
71
|
+
The middleware forwards request credentials and session headers to internal frame requests, converts them to safe `GET` requests, follows redirects, preserves application error bodies, propagates frame targets and top-frame URLs, and cancels frame rendering when the original request is aborted.
|
|
72
|
+
|
|
73
|
+
### Options
|
|
45
74
|
|
|
46
|
-
|
|
75
|
+
- **`assets`** - An asset server that resolves source-based client entry IDs to browser module URLs, import maps, and preload URLs. Omit it when client entries already use public URLs or the app has no client entries.
|
|
76
|
+
- **`onError`** - A callback for server rendering errors. When omitted, the UI renderer uses its default error reporting.
|
|
77
|
+
|
|
78
|
+
## Custom renderers
|
|
79
|
+
|
|
80
|
+
Use `renderWith()` when the input is not a Remix UI node or the application owns a fully custom response pipeline. The factory runs once per request and may read the current request context.
|
|
47
81
|
|
|
48
82
|
```ts
|
|
49
83
|
import { renderWith } from 'remix/middleware/render'
|
|
84
|
+
import { createRouter } from 'remix/router'
|
|
50
85
|
|
|
51
|
-
|
|
86
|
+
let json = renderWith(
|
|
52
87
|
() =>
|
|
53
88
|
function render(data: unknown, init?: ResponseInit) {
|
|
54
89
|
return Response.json(data, init)
|
|
55
90
|
},
|
|
56
91
|
)
|
|
57
92
|
|
|
58
|
-
router
|
|
59
|
-
return context.render({ ok: true })
|
|
60
|
-
})
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
For Remix UI, create a renderer that owns frame resolution and response creation.
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
import { createHtmlResponse } from 'remix/response/html'
|
|
67
|
-
import { renderWith } from 'remix/middleware/render'
|
|
68
|
-
import type { RemixNode } from 'remix/ui'
|
|
69
|
-
import { renderToStream } from 'remix/ui/server'
|
|
93
|
+
let router = createRouter({ middleware: [json] })
|
|
70
94
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
function render(node: RemixNode, init?: ResponseInit) {
|
|
74
|
-
let stream = renderToStream(node, {
|
|
75
|
-
async resolveFrame(src) {
|
|
76
|
-
let response = await router.fetch(new URL(src, url))
|
|
95
|
+
router.get('/api/status', (context) => context.render({ ok: true }))
|
|
96
|
+
```
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
return `<pre>Frame error: ${response.status}</pre>`
|
|
80
|
-
}
|
|
98
|
+
Custom renderers are also available through `context.get(Renderer)` when direct-property access is not suitable.
|
|
81
99
|
|
|
82
|
-
|
|
83
|
-
},
|
|
84
|
-
})
|
|
100
|
+
## Related Packages
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
102
|
+
- [`assets`](https://github.com/remix-run/remix/tree/main/packages/assets) - Source asset compilation and browser module URLs
|
|
103
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Request routing and typed context
|
|
104
|
+
- [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) - Remix UI components, frames, and server rendering
|
|
105
|
+
- [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - Web `Response` helpers
|
|
90
106
|
|
|
91
|
-
## Related
|
|
107
|
+
## Related Work
|
|
92
108
|
|
|
93
|
-
- [
|
|
94
|
-
- [
|
|
95
|
-
- [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers
|
|
109
|
+
- [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
|
110
|
+
- [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
|
|
96
111
|
|
|
97
112
|
## License
|
|
98
113
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# spa
|
|
2
|
+
|
|
3
|
+
Client-rendered application routing for Remix. It connects a standard fetch router to the browser
|
|
4
|
+
UI runtime without exposing the response carrier used to associate route responses with Remix
|
|
5
|
+
nodes.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Standard fetch routing** - Preserve `Request` to `Response` dispatch, redirects, status codes,
|
|
10
|
+
headers, middleware, and cancellation
|
|
11
|
+
- **Node rendering middleware** - Render `RemixNode` values through `context.render()`
|
|
12
|
+
- **Browser runtime** - Resolve the current URL and future frame navigations through the router
|
|
13
|
+
- **Initial fallback** - Display an interactive Remix node while the first route loads
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm i remix
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Install `render()` before middleware and route handlers that use `context.render()`, then pass the
|
|
24
|
+
router to `run()`.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { createRouter } from 'remix/router'
|
|
28
|
+
import { get, route } from 'remix/routes'
|
|
29
|
+
import { render, run } from 'remix/spa'
|
|
30
|
+
|
|
31
|
+
const routes = route({
|
|
32
|
+
home: get('/'),
|
|
33
|
+
about: get('/about'),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const router = createRouter({
|
|
37
|
+
middleware: [render()],
|
|
38
|
+
defaultHandler({ render }) {
|
|
39
|
+
return render(<h1>Not Found</h1>, { status: 404 })
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
router.map(routes, {
|
|
44
|
+
actions: {
|
|
45
|
+
home({ render }) {
|
|
46
|
+
return render(<h1>Home</h1>)
|
|
47
|
+
},
|
|
48
|
+
about({ render }) {
|
|
49
|
+
return render(<h1>About</h1>)
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const app = run(router, { fallback: <p>Loading…</p> })
|
|
55
|
+
await app.ready()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The router remains an ordinary fetch router, so its normal actions, controllers, middleware, and
|
|
59
|
+
context typing continue to apply.
|
|
60
|
+
|
|
61
|
+
## Wrapping Route Content
|
|
62
|
+
|
|
63
|
+
Pass a request-aware transform to `render()` when every route should share an application shell.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
const router = createRouter({
|
|
67
|
+
middleware: [
|
|
68
|
+
render((content, { url }) => (
|
|
69
|
+
<main data-pathname={url.pathname}>
|
|
70
|
+
<nav>{/* ... */}</nav>
|
|
71
|
+
{content}
|
|
72
|
+
</main>
|
|
73
|
+
)),
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Related Packages
|
|
79
|
+
|
|
80
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Request
|
|
81
|
+
routing, controllers, and middleware context
|
|
82
|
+
- [`render-middleware`](https://github.com/remix-run/remix/tree/main/packages/render-middleware) -
|
|
83
|
+
Request-scoped renderer middleware
|
|
84
|
+
- [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) - Remix components, frames, and
|
|
85
|
+
browser runtime
|
|
86
|
+
|
|
87
|
+
## Related Work
|
|
88
|
+
|
|
89
|
+
- [Fetch standard](https://fetch.spec.whatwg.org/) - The request and response model preserved by SPA
|
|
90
|
+
routers
|
|
91
|
+
- [Navigation API](https://wicg.github.io/navigation-api/) - Browser navigation lifecycle used by
|
|
92
|
+
the Remix UI runtime
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
package/src/spa.ts
ADDED
package/src/test/README.md
CHANGED
|
@@ -58,7 +58,7 @@ trailing commas are allowed:
|
|
|
58
58
|
|
|
59
59
|
```jsonc
|
|
60
60
|
{
|
|
61
|
-
"$schema": "
|
|
61
|
+
"$schema": "./node_modules/remix/schema/remix.json",
|
|
62
62
|
"test": {
|
|
63
63
|
"files": ["**/*.test{,.browser,.e2e}.{ts,tsx}"],
|
|
64
64
|
"browserFiles": ["**/*.test.browser.{ts,tsx}"],
|