remix 3.0.0-rc.1 → 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/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/package.json +39 -34
- package/src/assets/README.md +101 -13
- 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/render-middleware/README.md +2 -2
- package/src/ui/README.md +56 -8
- package/src/ui/server/README.md +25 -5
- package/src/ui-hmr/README.md +6 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multiple-import-maps-polyfill.d.ts","sourceRoot":"","sources":["../src/multiple-import-maps-polyfill.ts"],"names":[],"mappings":"AACA,cAAc,0CAA0C,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remix",
|
|
3
|
-
"version": "3.0.0-rc.
|
|
3
|
+
"version": "3.0.0-rc.2",
|
|
4
4
|
"description": "The Remix web framework",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -261,6 +261,10 @@
|
|
|
261
261
|
"types": "./dist/multipart-parser/node.d.ts",
|
|
262
262
|
"default": "./dist/multipart-parser/node.js"
|
|
263
263
|
},
|
|
264
|
+
"./multiple-import-maps-polyfill": {
|
|
265
|
+
"types": "./dist/multiple-import-maps-polyfill.d.ts",
|
|
266
|
+
"default": "./dist/multiple-import-maps-polyfill.js"
|
|
267
|
+
},
|
|
264
268
|
"./node-fetch-server": {
|
|
265
269
|
"types": "./dist/node-fetch-server.d.ts",
|
|
266
270
|
"default": "./dist/node-fetch-server.js"
|
|
@@ -512,53 +516,54 @@
|
|
|
512
516
|
"typescript": "^7.0.2"
|
|
513
517
|
},
|
|
514
518
|
"dependencies": {
|
|
515
|
-
"@remix-run/
|
|
516
|
-
"@remix-run/auth": "^0.
|
|
517
|
-
"@remix-run/
|
|
518
|
-
"@remix-run/
|
|
519
|
-
"@remix-run/
|
|
520
|
-
"@remix-run/
|
|
521
|
-
"@remix-run/
|
|
522
|
-
"@remix-run/
|
|
523
|
-
"@remix-run/
|
|
519
|
+
"@remix-run/auth": "^0.3.1",
|
|
520
|
+
"@remix-run/auth-middleware": "^0.2.6",
|
|
521
|
+
"@remix-run/ui": "^0.9.0",
|
|
522
|
+
"@remix-run/compression-middleware": "^0.1.14",
|
|
523
|
+
"@remix-run/assets": "^0.7.0",
|
|
524
|
+
"@remix-run/async-context-middleware": "^0.3.6",
|
|
525
|
+
"@remix-run/cors-middleware": "^0.1.9",
|
|
526
|
+
"@remix-run/csrf-middleware": "^0.1.9",
|
|
527
|
+
"@remix-run/data-table": "^0.5.1",
|
|
524
528
|
"@remix-run/cookie": "^0.6.0",
|
|
525
|
-
"@remix-run/data-table-postgres": "^0.5.
|
|
526
|
-
"@remix-run/
|
|
527
|
-
"@remix-run/data-table": "^0.
|
|
528
|
-
"@remix-run/data-table-mysql": "^0.5.1",
|
|
529
|
+
"@remix-run/data-table-postgres": "^0.5.2",
|
|
530
|
+
"@remix-run/cop-middleware": "^0.1.9",
|
|
531
|
+
"@remix-run/data-table-sqlite": "^0.6.2",
|
|
529
532
|
"@remix-run/fetch-proxy": "^0.8.5",
|
|
533
|
+
"@remix-run/data-schema": "^0.3.0",
|
|
534
|
+
"@remix-run/fetch-router": "^0.22.0",
|
|
530
535
|
"@remix-run/file-storage": "^0.13.7",
|
|
536
|
+
"@remix-run/data-table-mysql": "^0.5.2",
|
|
531
537
|
"@remix-run/file-storage-s3": "^0.1.4",
|
|
532
|
-
"@remix-run/form-data-middleware": "^0.3.
|
|
533
|
-
"@remix-run/form-data-parser": "^0.17.5",
|
|
534
|
-
"@remix-run/fetch-router": "^0.21.0",
|
|
535
|
-
"@remix-run/data-table-sqlite": "^0.6.1",
|
|
538
|
+
"@remix-run/form-data-middleware": "^0.3.6",
|
|
536
539
|
"@remix-run/fs": "^0.4.6",
|
|
537
|
-
"@remix-run/headers": "^0.21.1",
|
|
538
540
|
"@remix-run/lazy-file": "^5.0.6",
|
|
539
|
-
"@remix-run/logger-middleware": "^0.3.
|
|
541
|
+
"@remix-run/logger-middleware": "^0.3.6",
|
|
542
|
+
"@remix-run/form-data-parser": "^0.17.5",
|
|
543
|
+
"@remix-run/method-override-middleware": "^0.1.14",
|
|
544
|
+
"@remix-run/headers": "^0.21.1",
|
|
540
545
|
"@remix-run/html-template": "^0.3.1",
|
|
541
|
-
"@remix-run/method-override-middleware": "^0.1.13",
|
|
542
546
|
"@remix-run/mime": "^0.4.2",
|
|
543
|
-
"@remix-run/multipart-parser": "^0.16.4",
|
|
544
|
-
"@remix-run/node-fetch-server": "^0.14.1",
|
|
545
547
|
"@remix-run/node-tsx": "^0.1.1",
|
|
548
|
+
"@remix-run/multipart-parser": "^0.16.4",
|
|
546
549
|
"@remix-run/response": "^0.3.8",
|
|
547
|
-
"@remix-run/
|
|
550
|
+
"@remix-run/node-fetch-server": "^0.14.1",
|
|
551
|
+
"@remix-run/route-pattern": "^0.24.0",
|
|
548
552
|
"@remix-run/session": "^0.4.2",
|
|
549
|
-
"@remix-run/
|
|
553
|
+
"@remix-run/session-middleware": "^0.4.1",
|
|
554
|
+
"@remix-run/session-storage-memcache": "^0.1.2",
|
|
550
555
|
"@remix-run/assert": "^0.3.0",
|
|
551
|
-
"@remix-run/cli": "^0.6.0",
|
|
552
556
|
"@remix-run/session-storage-redis": "^0.1.1",
|
|
553
|
-
"@remix-run/
|
|
554
|
-
"@remix-run/
|
|
555
|
-
"@remix-run/
|
|
556
|
-
"@remix-run/
|
|
557
|
-
"@remix-run/
|
|
558
|
-
"@remix-run/
|
|
557
|
+
"@remix-run/test": "^0.6.0",
|
|
558
|
+
"@remix-run/cli": "^0.7.0",
|
|
559
|
+
"@remix-run/tar-parser": "^0.7.1",
|
|
560
|
+
"@remix-run/spa": "^0.1.1",
|
|
561
|
+
"@remix-run/static-middleware": "^0.4.15",
|
|
562
|
+
"@remix-run/render-middleware": "^0.3.0",
|
|
559
563
|
"@remix-run/terminal": "^0.1.1",
|
|
560
|
-
"@remix-run/
|
|
561
|
-
"@remix-run/
|
|
564
|
+
"@remix-run/multiple-import-maps-polyfill": "^0.1.0",
|
|
565
|
+
"@remix-run/ui-hmr": "^0.1.0",
|
|
566
|
+
"@remix-run/node-hmr": "^0.2.0"
|
|
562
567
|
},
|
|
563
568
|
"bin": {
|
|
564
569
|
"remix": "./dist/cli-entry.js"
|
package/src/assets/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Fetch-based server for compiling browser assets on demand.
|
|
|
10
10
|
- **Preloads** - Generate preload URLs for scripts and styles based on imports
|
|
11
11
|
- **Inspection** - List browser-reachable assets and explain URL-to-file mappings
|
|
12
12
|
- **Caching** - Conservative caching by default with stable URLs, ETags, and revalidation
|
|
13
|
-
- **Optional Fingerprinting** -
|
|
13
|
+
- **Optional Fingerprinting** - Content-based fingerprinted URLs for long-lived browser caching
|
|
14
14
|
- **Source Maps** - Serve inline or external sourcemaps
|
|
15
15
|
- **Hot Module Reloading** - Handle live code updates in development
|
|
16
16
|
- **Script Loaders** - Post-process compiled JavaScript with Node-compatible loaders
|
|
@@ -164,6 +164,8 @@ let assetServer = createAssetServer({
|
|
|
164
164
|
})
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
Package managers that install outside `rootDir`, such as pnpm's global virtual store, are handled without configuration: the asset server reads the store location from the nearest `node_modules/.modules.yaml` and mounts it internally so those package files still resolve to public URLs. A store that a configured mount already covers, such as pnpm's default `node_modules/.pnpm`, is left alone.
|
|
168
|
+
|
|
167
169
|
### File watching
|
|
168
170
|
|
|
169
171
|
The file system is watched by default so source changes are picked up without requiring a server restart.
|
|
@@ -212,13 +214,68 @@ let assetServer = createAssetServer({
|
|
|
212
214
|
})
|
|
213
215
|
```
|
|
214
216
|
|
|
217
|
+
## Script Entries
|
|
218
|
+
|
|
219
|
+
Use `assetServer.getScriptEntry()` to get everything needed to load a script and its dependencies.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
let { href, importMap, preloads } = await assetServer.getScriptEntry('app/assets/entry.tsx')
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This can be used when rendering a document shell:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import type { Handle, RemixNode } from 'remix/ui'
|
|
229
|
+
import { ImportMap } from 'remix/ui/server'
|
|
230
|
+
import { assetServer } from './assets.ts'
|
|
231
|
+
|
|
232
|
+
let { href, importMap, preloads } = await assetServer.getScriptEntry('app/assets/entry.tsx')
|
|
233
|
+
|
|
234
|
+
export function Document(handle: Handle<{ children: RemixNode }>) {
|
|
235
|
+
return () => (
|
|
236
|
+
<html>
|
|
237
|
+
<head>
|
|
238
|
+
{/* ... */}
|
|
239
|
+
<ImportMap value={importMap} />
|
|
240
|
+
{preloads.map((preload) => (
|
|
241
|
+
<link rel="modulepreload" href={preload} />
|
|
242
|
+
))}
|
|
243
|
+
<script type="module" src={href} />
|
|
244
|
+
</head>
|
|
245
|
+
<body>{handle.props.children}</body>
|
|
246
|
+
</html>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
This can also be used for resolved client entries in [`remix/ui`](https://github.com/remix-run/remix/tree/main/packages/ui) when using `import.meta.url` as the client entry ID:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { renderToStream } from 'remix/ui/server'
|
|
255
|
+
import { assetServer } from './assets.ts'
|
|
256
|
+
|
|
257
|
+
let stream = renderToStream(<App />, {
|
|
258
|
+
async resolveClientEntry(entryId, component) {
|
|
259
|
+
let { href, importMap, preloads } = await assetServer.getScriptEntry(entryId)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
href,
|
|
263
|
+
importMap,
|
|
264
|
+
preloads,
|
|
265
|
+
exportName: entryId.split('#')[1] || component.name,
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
// ...
|
|
269
|
+
})
|
|
270
|
+
```
|
|
271
|
+
|
|
215
272
|
## Hrefs
|
|
216
273
|
|
|
217
274
|
Use `assetServer.getHref()` when you need the public URL for a served asset. You can provide a root-relative or absolute file path, or a `file://` URL.
|
|
218
275
|
|
|
219
276
|
```ts
|
|
220
|
-
let src = await assetServer.getHref('app/
|
|
221
|
-
// '/assets/app/
|
|
277
|
+
let src = await assetServer.getHref('app/media/public/logo.svg')
|
|
278
|
+
// '/assets/app/media/public/logo.svg'
|
|
222
279
|
```
|
|
223
280
|
|
|
224
281
|
## Inspection
|
|
@@ -244,6 +301,16 @@ let src = await assetServer.getHref('app/media/public/image.png', {
|
|
|
244
301
|
// '/assets/app/media/public/image.png?transform=resize%3A100x100&transform=webp'
|
|
245
302
|
```
|
|
246
303
|
|
|
304
|
+
## Import Maps
|
|
305
|
+
|
|
306
|
+
Scripts retain their imports as authored and rely on import maps for resolution in the browser. `assetServer.getScriptEntry()` returns the import map for a single rendered script entry. Use `assetServer.getImportMap()` directly when you need to generate a combined import map for multiple script roots or other custom graph-level behavior.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
let importMap = await assetServer.getImportMap(['app/assets/entry.tsx', 'app/assets/search.tsx'])
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Without fingerprinting, import maps resolve authored specifiers to stable asset URLs. With fingerprinting enabled, the same import maps resolve stable asset URLs to content-fingerprinted asset URLs.
|
|
313
|
+
|
|
247
314
|
## Preloads
|
|
248
315
|
|
|
249
316
|
Use `assetServer.getPreloads()` when rendering HTML so you can turn the returned URLs into `<link rel="modulepreload">`, stylesheet preload tags, or `Link` headers for one or more assets and their dependencies. You can provide root-relative or absolute file paths, or `file://` URLs.
|
|
@@ -266,7 +333,7 @@ let preloads = await assetServer.getPreloads([
|
|
|
266
333
|
|
|
267
334
|
By default, assets are served at stable URLs with ETags and `Cache-Control: no-cache`.
|
|
268
335
|
|
|
269
|
-
If you want clients to cache assets aggressively without revalidation, you can opt into
|
|
336
|
+
If you want clients to cache assets aggressively without revalidation, you can opt into content-based fingerprinting.
|
|
270
337
|
|
|
271
338
|
```ts
|
|
272
339
|
import { createAssetServer } from 'remix/assets'
|
|
@@ -276,15 +343,13 @@ let assetServer = createAssetServer({
|
|
|
276
343
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
277
344
|
allowPackages: ['remix'],
|
|
278
345
|
watch: false,
|
|
279
|
-
fingerprint:
|
|
280
|
-
buildId: process.env.GITHUB_SHA,
|
|
281
|
-
},
|
|
346
|
+
fingerprint: true,
|
|
282
347
|
})
|
|
283
348
|
```
|
|
284
349
|
|
|
285
350
|
When fingerprinting is enabled, assets use a `.@<fingerprint>` segment before the file extension and are served with `Cache-Control: public, max-age=31536000, immutable`.
|
|
286
351
|
|
|
287
|
-
|
|
352
|
+
Fingerprints are based on emitted asset contents. This allows unchanged assets to keep the same URL across deployments, but it assumes that files on disk won't change after a URL is generated, so fingerprinting requires `watch: false`.
|
|
288
353
|
|
|
289
354
|
## Target
|
|
290
355
|
|
|
@@ -542,11 +607,9 @@ let assetServer = createAssetServer({
|
|
|
542
607
|
|
|
543
608
|
#### File transform caching
|
|
544
609
|
|
|
545
|
-
Use `files.cache` to store transformed file outputs via a [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) backend.
|
|
546
|
-
|
|
547
|
-
Without `files.cache`, transformed file outputs are recomputed per request.
|
|
610
|
+
Use `files.cache` to store transformed file outputs via a [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) backend. Without `files.cache`, transformed file outputs are recomputed per request.
|
|
548
611
|
|
|
549
|
-
|
|
612
|
+
`files.cacheKey` scopes transformed file cache entries. Use a stable identifier, such as a commit SHA, when you want unchanged transformed files to be reused across server restarts for the same build.
|
|
550
613
|
|
|
551
614
|
```ts
|
|
552
615
|
import * as path from 'node:path'
|
|
@@ -559,6 +622,7 @@ let assetServer = createAssetServer({
|
|
|
559
622
|
allowPackages: ['remix'],
|
|
560
623
|
files: {
|
|
561
624
|
cache: createFsFileStorage(path.resolve('.tmp/assets-cache')),
|
|
625
|
+
cacheKey: process.env.GIT_COMMIT_SHA,
|
|
562
626
|
extensions: ['.svg', '.png', '.jpg', '.jpeg', '.woff2'],
|
|
563
627
|
transforms: {
|
|
564
628
|
/*...*/
|
|
@@ -647,7 +711,7 @@ let isDevelopment = process.env.NODE_ENV === 'development'
|
|
|
647
711
|
let assetServer = createAssetServer({
|
|
648
712
|
basePath: '/assets',
|
|
649
713
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
650
|
-
|
|
714
|
+
allowPackages: ['remix'],
|
|
651
715
|
hmr: isDevelopment
|
|
652
716
|
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
|
|
653
717
|
: undefined,
|
|
@@ -655,6 +719,30 @@ let assetServer = createAssetServer({
|
|
|
655
719
|
})
|
|
656
720
|
```
|
|
657
721
|
|
|
722
|
+
Use `moduleImporter` to customize how HMR dynamically imports updated browser modules. It is resolved relative to the asset server's root directory and must point to a browser module exporting:
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
export function importModule(specifier: string, parentUrl: string): Promise<Record<string, unknown>>
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
HMR appends mappings for updated modules to the document in additional `<script type="importmap">` elements. Use `remix/multiple-import-maps-polyfill` when these updates must work in browsers without native support for multiple import maps:
|
|
729
|
+
|
|
730
|
+
```ts
|
|
731
|
+
import { createAssetServer } from 'remix/assets'
|
|
732
|
+
import { createBrowserHmrChannel } from 'remix/node-hmr/runtime'
|
|
733
|
+
|
|
734
|
+
let assetServer = createAssetServer({
|
|
735
|
+
basePath: '/assets',
|
|
736
|
+
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
737
|
+
allowPackages: ['remix'],
|
|
738
|
+
hmr: {
|
|
739
|
+
channel: createBrowserHmrChannel,
|
|
740
|
+
moduleImporter: 'remix/multiple-import-maps-polyfill',
|
|
741
|
+
},
|
|
742
|
+
watch: true,
|
|
743
|
+
})
|
|
744
|
+
```
|
|
745
|
+
|
|
658
746
|
### `import.meta.hot`
|
|
659
747
|
|
|
660
748
|
The `import.meta.hot` API provided by `assets` is a small runtime contract for modules that can handle updates without reloading the page. It is primarily intended for browser modules compiled by `assets`, but it can also be used directly.
|
|
@@ -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)
|
|
@@ -6,7 +6,7 @@ Request-scoped response rendering for Remix. It provides the conventional Remix
|
|
|
6
6
|
|
|
7
7
|
- **Remix UI rendering** - Stream nodes to HTML responses with `render()`
|
|
8
8
|
- **Framework-owned frames** - Resolve nested and targeted `<Frame>` requests through the current router
|
|
9
|
-
- **Client entry assets** - Resolve source-based `clientEntry()` modules and
|
|
9
|
+
- **Client entry assets** - Resolve source-based `clientEntry()` modules, import maps, and preloads through an asset server
|
|
10
10
|
- **Typed context** - Preserve renderer input and response option types on `context.render`
|
|
11
11
|
- **Custom renderers** - Install JSON, email, or other response pipelines with `renderWith()`
|
|
12
12
|
|
|
@@ -72,7 +72,7 @@ The middleware forwards request credentials and session headers to internal fram
|
|
|
72
72
|
|
|
73
73
|
### Options
|
|
74
74
|
|
|
75
|
-
- **`assets`** - An asset server that resolves source-based client entry IDs to browser module URLs and preload URLs. Omit it when client entries already use public URLs or the app has no client entries.
|
|
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
76
|
- **`onError`** - A callback for server rendering errors. When omitted, the UI renderer uses its default error reporting.
|
|
77
77
|
|
|
78
78
|
## Custom renderers
|
package/src/ui/README.md
CHANGED
|
@@ -79,10 +79,9 @@ function Actions() {
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Client Entry Loading
|
|
83
83
|
|
|
84
|
-
`run()`
|
|
85
|
-
fetches the frame source:
|
|
84
|
+
`run()` hydrates client entries by calling `loadModule` for each component module:
|
|
86
85
|
|
|
87
86
|
```tsx
|
|
88
87
|
import { run } from 'remix/ui'
|
|
@@ -97,6 +96,47 @@ let app = run({
|
|
|
97
96
|
await app.ready()
|
|
98
97
|
```
|
|
99
98
|
|
|
99
|
+
Client entries introduced after the initial document may depend on import maps added at runtime.
|
|
100
|
+
When targeting browsers without native support for multiple import maps, use
|
|
101
|
+
`remix/multiple-import-maps-polyfill` to load these modules and process their preloads:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import {
|
|
105
|
+
detectMultipleImportMapSupport,
|
|
106
|
+
importModule,
|
|
107
|
+
preloadShim,
|
|
108
|
+
} from 'remix/multiple-import-maps-polyfill'
|
|
109
|
+
|
|
110
|
+
let app = run({
|
|
111
|
+
async loadModule(moduleUrl, exportName) {
|
|
112
|
+
let module = await importModule(moduleUrl)
|
|
113
|
+
let Component = module[exportName]
|
|
114
|
+
if (typeof Component !== 'function') {
|
|
115
|
+
throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
|
|
116
|
+
}
|
|
117
|
+
return Component
|
|
118
|
+
},
|
|
119
|
+
async processClientEntryPreloads(preloads) {
|
|
120
|
+
if (await detectMultipleImportMapSupport()) return preloads
|
|
121
|
+
|
|
122
|
+
preloadShim(preloads)
|
|
123
|
+
return []
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Frame Navigation
|
|
129
|
+
|
|
130
|
+
The same runtime represents the current document as `app.frames.top` and intercepts eligible
|
|
131
|
+
same-origin links and forms through the browser's Navigation API. Those navigations fetch HTML with
|
|
132
|
+
the frame resolver and update the existing document in place instead of loading a new document.
|
|
133
|
+
This soft-navigation behavior applies even when the page only uses `clientEntry()` and does not
|
|
134
|
+
render an explicit `<Frame>`.
|
|
135
|
+
|
|
136
|
+
Frame navigation requires both `window.navigation` and `NavigateEvent.sourceElement`. Browsers
|
|
137
|
+
missing either capability use document navigation for links, forms, and `navigate()`. Hydration and
|
|
138
|
+
explicit frame reloads still work.
|
|
139
|
+
|
|
100
140
|
The default resolver is equivalent to:
|
|
101
141
|
|
|
102
142
|
```js
|
|
@@ -108,7 +148,8 @@ async function resolveFrame(src, options) {
|
|
|
108
148
|
signal: options?.signal,
|
|
109
149
|
})
|
|
110
150
|
|
|
111
|
-
|
|
151
|
+
let isHtml = response.headers.get('Content-Type')?.toLowerCase().includes('text/html')
|
|
152
|
+
if (response.status >= 500 || (response.status >= 300 && !isHtml)) {
|
|
112
153
|
throw new Error(`Failed to resolve frame: ${response.status} ${response.statusText}`.trimEnd())
|
|
113
154
|
}
|
|
114
155
|
|
|
@@ -149,11 +190,18 @@ CRLF-delimited text, and `multipart/form-data` submissions use `FormData`. Pass
|
|
|
149
190
|
`resolveFrame` when the server requires additional headers, another body encoding, or a different
|
|
150
191
|
response policy.
|
|
151
192
|
|
|
152
|
-
Add `data-rmx-document` to a link or form to leave
|
|
193
|
+
Add `data-rmx-document` to a link or form to leave that navigation to the browser. To keep all links
|
|
194
|
+
and forms as document navigations while still hydrating client entries and using explicit frames,
|
|
195
|
+
register a listener before calling `run()`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
window.navigation?.addEventListener('navigate', (e) => e.stopImmediatePropagation())
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This prevents Remix from intercepting Navigation API events. Explicit frame reloads such as
|
|
202
|
+
`handle.frame.reload()` continue to use the frame resolver.
|
|
153
203
|
|
|
154
|
-
The default resolver
|
|
155
|
-
A custom `resolveFrame` may return a `Response` with any status when it wants Remix UI to render the
|
|
156
|
-
response body.
|
|
204
|
+
The default resolver accepts `2xx` responses and `3xx` or `4xx` responses whose `Content-Type` includes `text/html`, ignoring case. It rejects other `3xx` or `4xx` responses and all `5xx` responses with an error containing their status and status text. A custom `resolveFrame` may return a `Response` with any status when it wants Remix UI to render the response body.
|
|
157
205
|
|
|
158
206
|
Forms remain ordinary HTML forms before the runtime starts. Add `data-rmx-target` to reload a named frame, or `data-rmx-document` to require a full-document submission:
|
|
159
207
|
|
package/src/ui/server/README.md
CHANGED
|
@@ -33,12 +33,10 @@ let stream = renderToStream(<App />, {
|
|
|
33
33
|
return fetchHtml(frameUrl)
|
|
34
34
|
},
|
|
35
35
|
async resolveClientEntry(entryId, component) {
|
|
36
|
-
let
|
|
37
|
-
assetServer.getHref(entryId),
|
|
38
|
-
assetServer.getPreloads(entryId),
|
|
39
|
-
])
|
|
36
|
+
let { href, importMap, preloads } = await assetServer.getScriptEntry(entryId)
|
|
40
37
|
return {
|
|
41
38
|
href,
|
|
39
|
+
importMap,
|
|
42
40
|
exportName: entryId.split('#')[1] || component.name,
|
|
43
41
|
preloads,
|
|
44
42
|
}
|
|
@@ -59,7 +57,7 @@ return new Response(stream, {
|
|
|
59
57
|
- **`topFrameSrc`** - Overrides the root frame URL used for `handle.frames.top.src`. This is mainly useful when calling `renderToStream()` from inside `resolveFrame()` for a nested frame render.
|
|
60
58
|
- **`signal`** - Cancels pending server rendering work. Pass `request.signal` so client disconnects can stop unresolved frame work without invoking `onError` for the disconnect itself.
|
|
61
59
|
- **`resolveFrame(src, target, context)`** - Called when a `<Frame>` needs its content. Return a string of HTML, a `ReadableStream<Uint8Array>`, or a promise of either. `context.currentFrameSrc` is the URL for the frame that contains the `<Frame>`, and `context.topFrameSrc` is the outer document URL. Required if your component tree contains `<Frame>` elements.
|
|
62
|
-
- **`resolveClientEntry(entryId, component)`** - Resolves the public module URL, export name, and optional module preload hrefs for a hydrated client entry.
|
|
60
|
+
- **`resolveClientEntry(entryId, component)`** - Resolves the public module URL, export name, optional import map, and optional module preload hrefs for a hydrated client entry.
|
|
63
61
|
- **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.
|
|
64
62
|
|
|
65
63
|
When you render nested frame responses with `renderToStream()` inside `resolveFrame()`, pass `frameSrc` for the frame being rendered and carry `topFrameSrc` forward from the parent context. That preserves `handle.frames.top.src` across the whole SSR frame tree.
|
|
@@ -97,6 +95,28 @@ function ProductPage() {
|
|
|
97
95
|
|
|
98
96
|
Components using the `css(...)` mixin through `mix` have their styles collected during rendering and emitted as a single `<style>` tag in the `<head>`. No client-side style injection is needed for server-rendered content.
|
|
99
97
|
|
|
98
|
+
### Import maps
|
|
99
|
+
|
|
100
|
+
Use `<ImportMap>` when the document needs one initial import map containing both authored mappings and mappings from blocking client entries. The server merges these mappings into the component before sending the initial HTML.
|
|
101
|
+
|
|
102
|
+
Regular `<script type="importmap">` elements remain supported. The server leaves them unchanged and emits any additional client entry mappings separately, omitting entries already present in authored maps.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import type { Handle, RemixNode } from 'remix/ui'
|
|
106
|
+
import { ImportMap } from 'remix/ui/server'
|
|
107
|
+
|
|
108
|
+
function Document(handle: Handle<{ children: RemixNode }>) {
|
|
109
|
+
return () => (
|
|
110
|
+
<html>
|
|
111
|
+
<head>
|
|
112
|
+
<ImportMap value={scriptEntry.importMap} />
|
|
113
|
+
</head>
|
|
114
|
+
<body>{handle.props.children}</body>
|
|
115
|
+
</html>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
100
120
|
## See Also
|
|
101
121
|
|
|
102
122
|
- [Hydration](https://github.com/remix-run/remix/blob/main/packages/ui/docs/hydration.md) - Making server-rendered components interactive on the client
|
package/src/ui-hmr/README.md
CHANGED
|
@@ -39,7 +39,10 @@ let assetServer = createAssetServer({
|
|
|
39
39
|
allowPackages: ['remix'],
|
|
40
40
|
denyFiles: ['app/**/*.test.*'],
|
|
41
41
|
hmr: isDevelopment
|
|
42
|
-
?
|
|
42
|
+
? {
|
|
43
|
+
channel: async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel(),
|
|
44
|
+
moduleImporter: 'remix/multiple-import-maps-polyfill',
|
|
45
|
+
}
|
|
43
46
|
: undefined,
|
|
44
47
|
scripts: {
|
|
45
48
|
loaders: isDevelopment ? [uiHmr()] : undefined,
|
|
@@ -48,6 +51,8 @@ let assetServer = createAssetServer({
|
|
|
48
51
|
})
|
|
49
52
|
```
|
|
50
53
|
|
|
54
|
+
HMR appends mappings for updated modules to the document in additional `<script type="importmap">` elements. The module importer allows browsers without native support for multiple import maps to load these updates.
|
|
55
|
+
|
|
51
56
|
## Direct Transforms
|
|
52
57
|
|
|
53
58
|
Use the direct transform APIs when you are writing your own loader, Node module hooks, or build integration.
|