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/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": "https://remix.run/schemas/remix.json",
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. When no global `--config` is provided, database commands find the
166
- nearest `remix.json` by walking up from the working directory.
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)
@@ -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": "https://remix.run/schemas/remix.json",
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` accepts a bare migration id (`20260301113000`) or the full directory name (`20260301113000_add_user_status`).
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)
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/multiple-import-maps-polyfill'
@@ -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 renderer middleware for Remix. It stores a renderer in `fetch-router` request context so route actions can render responses without passing request-specific rendering details through every action.
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
- - **Generic renderers** - Render any input type to a `Response`
8
- - **Request-scoped setup** - Create renderers from the current `RequestContext`
9
- - **Typed context** - Typed `context.render` (or `context.get(Renderer)`)
10
- - **Small runtime** - The package only stores a renderer in request context
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
- Use `renderWith()` to add a renderer to `context.render` and `context.get(Renderer)`.
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
- ```ts
23
- import { createRouter, type MiddlewareContext } from 'remix/router'
24
- import { renderWith } from 'remix/middleware/render'
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
- const render = renderWith(
27
- (context) =>
28
- function render(value: string, init?: ResponseInit) {
29
- return new Response(`${context.url.pathname}: ${value}`, init)
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
- type AppContext = MiddlewareContext<[typeof render]>
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
- const router = createRouter<AppContext>({
36
- middleware: [render],
37
- })
60
+ `context.render(node, init)` returns an HTML `Response` and preserves the supplied status and headers:
38
61
 
39
- router.get('/hello', (context) => {
40
- return context.render('Hello')
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
- Use `context.render(...)` (or `context.get(Renderer)(...)`).
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
- Renderers may render any value type, not just UI nodes.
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
- const json = renderWith(
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.get('/api', (context) => {
59
- return context.render({ ok: true })
60
- })
61
- ```
62
-
63
- For Remix UI, create a renderer that owns frame resolution and response creation.
64
-
65
- ```tsx
66
- import { createHtmlResponse } from 'remix/response/html'
67
- import { renderWith } from 'remix/middleware/render'
68
- import type { RemixNode } from 'remix/ui'
69
- import { renderToStream } from 'remix/ui/server'
93
+ let router = createRouter({ middleware: [json] })
70
94
 
71
- const render = renderWith(
72
- ({ router, url }) =>
73
- function render(node: RemixNode, init?: ResponseInit) {
74
- let stream = renderToStream(node, {
75
- async resolveFrame(src) {
76
- let response = await router.fetch(new URL(src, url))
95
+ router.get('/api/status', (context) => context.render({ ok: true }))
96
+ ```
77
97
 
78
- if (!response.ok) {
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
- return response.body ?? response.text()
83
- },
84
- })
100
+ ## Related Packages
85
101
 
86
- return createHtmlResponse(stream, init)
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 Packages
107
+ ## Related Work
92
108
 
93
- - [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Request routing and context
94
- - [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) - Remix UI rendering primitives
95
- - [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers
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
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/spa'
@@ -1,4 +1,4 @@
1
- # static-files-middleware
1
+ # static-middleware
2
2
 
3
3
  Static file serving middleware for Remix. Serves static files from a directory with support for ETags, range requests, and conditional requests.
4
4
 
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/static-middleware'
@@ -58,7 +58,7 @@ trailing commas are allowed:
58
58
 
59
59
  ```jsonc
60
60
  {
61
- "$schema": "https://remix.run/schemas/remix.json",
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}"],