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/assets/README.md
CHANGED
|
@@ -6,11 +6,11 @@ Fetch-based server for compiling browser assets on demand.
|
|
|
6
6
|
|
|
7
7
|
- **On-Demand Compilation** - Compile browser scripts and styles on demand
|
|
8
8
|
- **File Serving** - Serve configured file assets like images and fonts with optional transforms
|
|
9
|
-
- **Custom File Mapping** - Define patterns for mapping public URLs to file paths on disk
|
|
10
9
|
- **Access Control** - Control exactly which files and packages can be served
|
|
11
10
|
- **Preloads** - Generate preload URLs for scripts and styles based on imports
|
|
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
|
|
@@ -21,6 +21,12 @@ Fetch-based server for compiling browser assets on demand.
|
|
|
21
21
|
npm i remix
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
The optional image transform examples also use Sharp:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm i sharp
|
|
28
|
+
```
|
|
29
|
+
|
|
24
30
|
## Usage
|
|
25
31
|
|
|
26
32
|
Use `createAssetServer` to serve browser assets from a URL namespace in your app.
|
|
@@ -31,10 +37,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
31
37
|
|
|
32
38
|
let assetServer = createAssetServer({
|
|
33
39
|
basePath: '/assets',
|
|
34
|
-
fileMap: {
|
|
35
|
-
'/app/*path': 'app/*path',
|
|
36
|
-
'/npm/*path': 'node_modules/*path',
|
|
37
|
-
},
|
|
38
40
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
39
41
|
allowPackages: ['remix'],
|
|
40
42
|
files: {
|
|
@@ -51,6 +53,64 @@ router.get('/assets/*', ({ request }) => {
|
|
|
51
53
|
|
|
52
54
|
This example gives you an `/assets/*` endpoint that serves compiled browser source from `public/` directories throughout `app/` and from the `remix` package.
|
|
53
55
|
|
|
56
|
+
## Shared Configuration
|
|
57
|
+
|
|
58
|
+
Keep JSON-compatible asset mapping, access, and file-type settings in `remix.json` so the running
|
|
59
|
+
server and Remix CLI use the same configuration:
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
{
|
|
63
|
+
"$schema": "./node_modules/remix/schema/remix.json",
|
|
64
|
+
"assets": {
|
|
65
|
+
"basePath": "/assets",
|
|
66
|
+
"mounts": {
|
|
67
|
+
"app": "app",
|
|
68
|
+
"npm": "node_modules",
|
|
69
|
+
},
|
|
70
|
+
"allowFiles": ["app/routes.ts", "app/**/public/**"],
|
|
71
|
+
"allowPackages": ["remix"],
|
|
72
|
+
"denyFiles": ["app/**/*.test.*"],
|
|
73
|
+
"files": {
|
|
74
|
+
"extensions": [".svg", ".png", ".jpg", ".woff2"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Load it from application code and add runtime-only behavior there:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { createAssetServer, defineFileTransform } from 'remix/assets'
|
|
84
|
+
import { loadConfig } from 'remix/cli'
|
|
85
|
+
import sharp from 'sharp'
|
|
86
|
+
|
|
87
|
+
let config = await loadConfig(import.meta.dirname)
|
|
88
|
+
if (config.assets === undefined) throw new Error('Missing assets configuration')
|
|
89
|
+
if (config.assets.files === undefined) throw new Error('Missing asset file configuration')
|
|
90
|
+
|
|
91
|
+
let assetServer = createAssetServer({
|
|
92
|
+
...config.assets,
|
|
93
|
+
files: {
|
|
94
|
+
...config.assets.files,
|
|
95
|
+
transforms: {
|
|
96
|
+
webp: defineFileTransform({
|
|
97
|
+
extensions: ['.png', '.jpg'],
|
|
98
|
+
async transform(bytes) {
|
|
99
|
+
return {
|
|
100
|
+
content: await sharp(bytes).webp({ quality: 80 }).toBuffer(),
|
|
101
|
+
extension: '.webp',
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`loadConfig()` accepts either a config file or a directory. When given a directory, it searches
|
|
111
|
+
upward for the nearest `remix.json`. Run `remix assets` to list reachable files, or
|
|
112
|
+
`remix assets inspect <url-or-file>` to inspect one mapping and its access decision.
|
|
113
|
+
|
|
54
114
|
## Root Directory
|
|
55
115
|
|
|
56
116
|
Use `rootDir` to specify the root directory of the asset server, which is used to resolve relative file paths. Defaults to `process.cwd()`.
|
|
@@ -62,10 +122,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
62
122
|
let assetServer = createAssetServer({
|
|
63
123
|
rootDir: path.resolve(import.meta.dirname, '..'),
|
|
64
124
|
basePath: '/assets',
|
|
65
|
-
fileMap: {
|
|
66
|
-
'/app/*path': 'app/*path',
|
|
67
|
-
'/npm/*path': 'node_modules/*path',
|
|
68
|
-
},
|
|
69
125
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
70
126
|
allowPackages: ['remix'],
|
|
71
127
|
})
|
|
@@ -80,10 +136,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
80
136
|
|
|
81
137
|
let assetServer = createAssetServer({
|
|
82
138
|
basePath: '/assets',
|
|
83
|
-
fileMap: {
|
|
84
|
-
'/app/*path': 'app/*path',
|
|
85
|
-
'/npm/*path': 'node_modules/*path',
|
|
86
|
-
},
|
|
87
139
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
88
140
|
allowPackages: ['remix'],
|
|
89
141
|
denyFiles: ['app/**/*.server.*'],
|
|
@@ -92,27 +144,27 @@ let assetServer = createAssetServer({
|
|
|
92
144
|
|
|
93
145
|
Values for `allowFiles` and `denyFiles` are file paths or globs. Relative values are resolved from `rootDir`. Absolute file paths match exactly, and absolute directory paths also match their descendants.
|
|
94
146
|
|
|
95
|
-
Values for `allowPackages` are exact package names. Dependencies and installed optional dependencies of packages in `allowPackages` are also allowed automatically. Peer dependencies must be listed explicitly if they should be browser-reachable. Allowed package files must still be reachable through `
|
|
147
|
+
Values for `allowPackages` are exact package names. Dependencies and installed optional dependencies of packages in `allowPackages` are also allowed automatically. Peer dependencies must be listed explicitly if they should be browser-reachable. Allowed package files must still be reachable through `mounts`.
|
|
96
148
|
|
|
97
|
-
##
|
|
149
|
+
## Mounts
|
|
98
150
|
|
|
99
|
-
|
|
151
|
+
By default, the asset server mounts the `app` directory at `/app` and `node_modules` at `/npm`. Use `mounts` to replace these defaults. Keys are public paths relative to `basePath`, and values are directory paths relative to `rootDir`.
|
|
100
152
|
|
|
101
153
|
```ts
|
|
102
154
|
import { createAssetServer } from 'remix/assets'
|
|
103
155
|
|
|
104
156
|
let assetServer = createAssetServer({
|
|
105
157
|
basePath: '/assets',
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
mounts: {
|
|
159
|
+
source: 'app',
|
|
160
|
+
vendor: 'node_modules',
|
|
109
161
|
},
|
|
110
162
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
111
163
|
allowPackages: ['remix'],
|
|
112
164
|
})
|
|
113
165
|
```
|
|
114
166
|
|
|
115
|
-
|
|
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.
|
|
116
168
|
|
|
117
169
|
### File watching
|
|
118
170
|
|
|
@@ -123,10 +175,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
123
175
|
|
|
124
176
|
let assetServer = createAssetServer({
|
|
125
177
|
basePath: '/assets',
|
|
126
|
-
fileMap: {
|
|
127
|
-
'/app/*path': 'app/*path',
|
|
128
|
-
'/npm/*path': 'node_modules/*path',
|
|
129
|
-
},
|
|
130
178
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
131
179
|
allowPackages: ['remix'],
|
|
132
180
|
})
|
|
@@ -145,10 +193,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
145
193
|
|
|
146
194
|
let assetServer = createAssetServer({
|
|
147
195
|
basePath: '/assets',
|
|
148
|
-
fileMap: {
|
|
149
|
-
'/app/*path': 'app/*path',
|
|
150
|
-
'/npm/*path': 'node_modules/*path',
|
|
151
|
-
},
|
|
152
196
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
153
197
|
allowPackages: ['remix'],
|
|
154
198
|
watch: false,
|
|
@@ -162,10 +206,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
162
206
|
|
|
163
207
|
let assetServer = createAssetServer({
|
|
164
208
|
basePath: '/assets',
|
|
165
|
-
fileMap: {
|
|
166
|
-
'/app/*path': 'app/*path',
|
|
167
|
-
'/npm/*path': 'node_modules/*path',
|
|
168
|
-
},
|
|
169
209
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
170
210
|
allowPackages: ['remix'],
|
|
171
211
|
watch: {
|
|
@@ -174,13 +214,82 @@ let assetServer = createAssetServer({
|
|
|
174
214
|
})
|
|
175
215
|
```
|
|
176
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
|
+
|
|
177
272
|
## Hrefs
|
|
178
273
|
|
|
179
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.
|
|
180
275
|
|
|
181
276
|
```ts
|
|
182
|
-
let src = await assetServer.getHref('app/
|
|
183
|
-
// '/assets/app/
|
|
277
|
+
let src = await assetServer.getHref('app/media/public/logo.svg')
|
|
278
|
+
// '/assets/app/media/public/logo.svg'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Inspection
|
|
282
|
+
|
|
283
|
+
Use `getAssets()` for a sorted list of files that are currently browser-reachable through the
|
|
284
|
+
asset server. Use `getAssetDetails()` with a public URL or file path to inspect its mapping, file
|
|
285
|
+
type, access rules, and reachability status.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
let assets = await assetServer.getAssets()
|
|
289
|
+
// [{ url: '/assets/app/actions/public/entry.ts', filePath: '/project/app/actions/public/entry.ts', ... }]
|
|
290
|
+
|
|
291
|
+
let details = await assetServer.getAssetDetails('/assets/app/actions/public/entry.ts')
|
|
292
|
+
// { status: 'reachable', type: 'script', ... }
|
|
184
293
|
```
|
|
185
294
|
|
|
186
295
|
For configured `files` assets, you can also pass a `transform` pipeline to build a request URL with custom file transforms. Basic transforms are written as strings, while dynamic transforms use `[name, param]` tuples.
|
|
@@ -192,6 +301,16 @@ let src = await assetServer.getHref('app/media/public/image.png', {
|
|
|
192
301
|
// '/assets/app/media/public/image.png?transform=resize%3A100x100&transform=webp'
|
|
193
302
|
```
|
|
194
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
|
+
|
|
195
314
|
## Preloads
|
|
196
315
|
|
|
197
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.
|
|
@@ -214,29 +333,23 @@ let preloads = await assetServer.getPreloads([
|
|
|
214
333
|
|
|
215
334
|
By default, assets are served at stable URLs with ETags and `Cache-Control: no-cache`.
|
|
216
335
|
|
|
217
|
-
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.
|
|
218
337
|
|
|
219
338
|
```ts
|
|
220
339
|
import { createAssetServer } from 'remix/assets'
|
|
221
340
|
|
|
222
341
|
let assetServer = createAssetServer({
|
|
223
342
|
basePath: '/assets',
|
|
224
|
-
fileMap: {
|
|
225
|
-
'/app/*path': 'app/*path',
|
|
226
|
-
'/npm/*path': 'node_modules/*path',
|
|
227
|
-
},
|
|
228
343
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
229
344
|
allowPackages: ['remix'],
|
|
230
345
|
watch: false,
|
|
231
|
-
fingerprint:
|
|
232
|
-
buildId: process.env.GITHUB_SHA,
|
|
233
|
-
},
|
|
346
|
+
fingerprint: true,
|
|
234
347
|
})
|
|
235
348
|
```
|
|
236
349
|
|
|
237
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`.
|
|
238
351
|
|
|
239
|
-
|
|
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`.
|
|
240
353
|
|
|
241
354
|
## Target
|
|
242
355
|
|
|
@@ -247,10 +360,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
247
360
|
|
|
248
361
|
let assetServer = createAssetServer({
|
|
249
362
|
basePath: '/assets',
|
|
250
|
-
fileMap: {
|
|
251
|
-
'/app/*path': 'app/*path',
|
|
252
|
-
'/npm/*path': 'node_modules/*path',
|
|
253
|
-
},
|
|
254
363
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
255
364
|
allowPackages: ['remix'],
|
|
256
365
|
target: {
|
|
@@ -272,10 +381,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
272
381
|
|
|
273
382
|
let assetServer = createAssetServer({
|
|
274
383
|
basePath: '/assets',
|
|
275
|
-
fileMap: {
|
|
276
|
-
'/app/*path': 'app/*path',
|
|
277
|
-
'/npm/*path': 'node_modules/*path',
|
|
278
|
-
},
|
|
279
384
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
280
385
|
allowPackages: ['remix'],
|
|
281
386
|
sourceMaps: 'external',
|
|
@@ -289,10 +394,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
289
394
|
|
|
290
395
|
let assetServer = createAssetServer({
|
|
291
396
|
basePath: '/assets',
|
|
292
|
-
fileMap: {
|
|
293
|
-
'/app/*path': 'app/*path',
|
|
294
|
-
'/npm/*path': 'node_modules/*path',
|
|
295
|
-
},
|
|
296
397
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
297
398
|
allowPackages: ['remix'],
|
|
298
399
|
sourceMaps: 'inline',
|
|
@@ -309,10 +410,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
309
410
|
|
|
310
411
|
let assetServer = createAssetServer({
|
|
311
412
|
basePath: '/assets',
|
|
312
|
-
fileMap: {
|
|
313
|
-
'/app/*path': 'app/*path',
|
|
314
|
-
'/npm/*path': 'node_modules/*path',
|
|
315
|
-
},
|
|
316
413
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
317
414
|
allowPackages: ['remix'],
|
|
318
415
|
minify: true,
|
|
@@ -330,10 +427,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
330
427
|
|
|
331
428
|
let assetServer = createAssetServer({
|
|
332
429
|
basePath: '/assets',
|
|
333
|
-
fileMap: {
|
|
334
|
-
'/app/*path': 'app/*path',
|
|
335
|
-
'/npm/*path': 'node_modules/*path',
|
|
336
|
-
},
|
|
337
430
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
338
431
|
allowPackages: ['remix'],
|
|
339
432
|
scripts: {
|
|
@@ -355,10 +448,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
355
448
|
|
|
356
449
|
let assetServer = createAssetServer({
|
|
357
450
|
basePath: '/assets',
|
|
358
|
-
fileMap: {
|
|
359
|
-
'/app/*path': 'app/*path',
|
|
360
|
-
'/npm/*path': 'node_modules/*path',
|
|
361
|
-
},
|
|
362
451
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
363
452
|
allowPackages: ['remix'],
|
|
364
453
|
scripts: {
|
|
@@ -376,7 +465,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
376
465
|
|
|
377
466
|
let assetServer = createAssetServer({
|
|
378
467
|
basePath: '/assets',
|
|
379
|
-
fileMap: { '/app/*path': 'app/*path' },
|
|
380
468
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
381
469
|
denyFiles: ['app/**/*.test.*'],
|
|
382
470
|
scripts: {
|
|
@@ -404,10 +492,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
404
492
|
|
|
405
493
|
let assetServer = createAssetServer({
|
|
406
494
|
basePath: '/assets',
|
|
407
|
-
fileMap: {
|
|
408
|
-
'/app/*path': 'app/*path',
|
|
409
|
-
'/npm/*path': 'node_modules/*path',
|
|
410
|
-
},
|
|
411
495
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
412
496
|
allowPackages: ['remix'],
|
|
413
497
|
files: {
|
|
@@ -430,10 +514,6 @@ import sharp from 'sharp'
|
|
|
430
514
|
|
|
431
515
|
let assetServer = createAssetServer({
|
|
432
516
|
basePath: '/assets',
|
|
433
|
-
fileMap: {
|
|
434
|
-
'/app/*path': 'app/*path',
|
|
435
|
-
'/npm/*path': 'node_modules/*path',
|
|
436
|
-
},
|
|
437
517
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
438
518
|
allowPackages: ['remix'],
|
|
439
519
|
files: {
|
|
@@ -464,10 +544,6 @@ import { createAssetServer, defineFileTransform } from 'remix/assets'
|
|
|
464
544
|
|
|
465
545
|
let assetServer = createAssetServer({
|
|
466
546
|
basePath: '/assets',
|
|
467
|
-
fileMap: {
|
|
468
|
-
'/app/*path': 'app/*path',
|
|
469
|
-
'/npm/*path': 'node_modules/*path',
|
|
470
|
-
},
|
|
471
547
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
472
548
|
allowPackages: ['remix'],
|
|
473
549
|
files: {
|
|
@@ -512,10 +588,6 @@ import { optimize as optimizeSvg } from 'svgo'
|
|
|
512
588
|
|
|
513
589
|
let assetServer = createAssetServer({
|
|
514
590
|
basePath: '/assets',
|
|
515
|
-
fileMap: {
|
|
516
|
-
'/app/*path': 'app/*path',
|
|
517
|
-
'/npm/*path': 'node_modules/*path',
|
|
518
|
-
},
|
|
519
591
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
520
592
|
allowPackages: ['remix'],
|
|
521
593
|
files: {
|
|
@@ -535,11 +607,9 @@ let assetServer = createAssetServer({
|
|
|
535
607
|
|
|
536
608
|
#### File transform caching
|
|
537
609
|
|
|
538
|
-
Use `files.cache` to store transformed file outputs via a [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) backend.
|
|
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.
|
|
539
611
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
If `fingerprint.buildId` is set, the file cache can be reused across server restarts for the same build.
|
|
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.
|
|
543
613
|
|
|
544
614
|
```ts
|
|
545
615
|
import * as path from 'node:path'
|
|
@@ -548,14 +618,11 @@ import { createFsFileStorage } from 'remix/file-storage/fs'
|
|
|
548
618
|
|
|
549
619
|
let assetServer = createAssetServer({
|
|
550
620
|
basePath: '/assets',
|
|
551
|
-
fileMap: {
|
|
552
|
-
'/app/*path': 'app/*path',
|
|
553
|
-
'/npm/*path': 'node_modules/*path',
|
|
554
|
-
},
|
|
555
621
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
556
622
|
allowPackages: ['remix'],
|
|
557
623
|
files: {
|
|
558
624
|
cache: createFsFileStorage(path.resolve('.tmp/assets-cache')),
|
|
625
|
+
cacheKey: process.env.GIT_COMMIT_SHA,
|
|
559
626
|
extensions: ['.svg', '.png', '.jpg', '.jpeg', '.woff2'],
|
|
560
627
|
transforms: {
|
|
561
628
|
/*...*/
|
|
@@ -573,10 +640,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
573
640
|
|
|
574
641
|
let assetServer = createAssetServer({
|
|
575
642
|
basePath: '/assets',
|
|
576
|
-
fileMap: {
|
|
577
|
-
'/app/*path': 'app/*path',
|
|
578
|
-
'/npm/*path': 'node_modules/*path',
|
|
579
|
-
},
|
|
580
643
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
581
644
|
allowPackages: ['remix'],
|
|
582
645
|
files: {
|
|
@@ -624,10 +687,6 @@ import { createAssetServer } from 'remix/assets'
|
|
|
624
687
|
|
|
625
688
|
let assetServer = createAssetServer({
|
|
626
689
|
basePath: '/assets',
|
|
627
|
-
fileMap: {
|
|
628
|
-
'/app/*path': 'app/*path',
|
|
629
|
-
'/npm/*path': 'node_modules/*path',
|
|
630
|
-
},
|
|
631
690
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
632
691
|
allowPackages: ['remix'],
|
|
633
692
|
onError(error) {
|
|
@@ -651,9 +710,8 @@ import { createAssetServer } from 'remix/assets'
|
|
|
651
710
|
let isDevelopment = process.env.NODE_ENV === 'development'
|
|
652
711
|
let assetServer = createAssetServer({
|
|
653
712
|
basePath: '/assets',
|
|
654
|
-
fileMap: { '/app/*path': 'app/*path' },
|
|
655
713
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
656
|
-
|
|
714
|
+
allowPackages: ['remix'],
|
|
657
715
|
hmr: isDevelopment
|
|
658
716
|
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
|
|
659
717
|
: undefined,
|
|
@@ -661,6 +719,30 @@ let assetServer = createAssetServer({
|
|
|
661
719
|
})
|
|
662
720
|
```
|
|
663
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
|
+
|
|
664
746
|
### `import.meta.hot`
|
|
665
747
|
|
|
666
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.
|
|
@@ -798,7 +880,6 @@ if (import.meta.hot) {
|
|
|
798
880
|
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - A Fetch-based router that pairs naturally with `assets`
|
|
799
881
|
- [`node-hmr`](https://github.com/remix-run/remix/tree/main/packages/node-hmr) - Provides the server-side `import.meta.hot` runtime and browser HMR channel used by `hmr`
|
|
800
882
|
- [`ui-hmr`](https://github.com/remix-run/remix/tree/main/packages/ui-hmr) - Provides a Remix UI component HMR loader for `scripts.loaders`
|
|
801
|
-
- [`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern) - Route-pattern syntax for URL and route file matching
|
|
802
883
|
|
|
803
884
|
## License
|
|
804
885
|
|
package/src/auth/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Composable browser authentication primitives for Remix. Use this package to veri
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Small, composable primitives: `verifyCredentials()`, `startExternalAuth()`, `finishExternalAuth()`, `refreshExternalAuth()`, and `completeAuth()`
|
|
8
|
-
- Built-in provider support for Google, Microsoft, Okta, Auth0, GitHub, Facebook,
|
|
8
|
+
- Built-in provider support for Google, Microsoft, Okta, Auth0, GitHub, Facebook, and X
|
|
9
9
|
- Module-scope provider configuration for boot-time validation and stable callback URLs
|
|
10
10
|
- App-owned session records so you decide what auth data to persist
|
|
11
11
|
- Shared session completion for credentials and external auth flows
|
|
@@ -276,7 +276,7 @@ router.get(routes.app.dashboard, {
|
|
|
276
276
|
|
|
277
277
|
A typical external auth flow looks like this:
|
|
278
278
|
|
|
279
|
-
1. Create the provider once at module scope.
|
|
279
|
+
1. Create the provider once at module scope.
|
|
280
280
|
2. Call `startExternalAuth()` from the login route.
|
|
281
281
|
3. Call `finishExternalAuth()` from the callback route.
|
|
282
282
|
4. Persist any provider tokens you want to reuse later.
|
|
@@ -286,12 +286,11 @@ A typical external auth flow looks like this:
|
|
|
286
286
|
|
|
287
287
|
## Built-in External Auth Providers
|
|
288
288
|
|
|
289
|
-
When one of the built-in providers matches your auth provider, start there. Google, Microsoft, Okta, and Auth0 use the shared OIDC runtime. GitHub, Facebook,
|
|
289
|
+
When one of the built-in providers matches your auth provider, start there. Google, Microsoft, Okta, and Auth0 use the shared OIDC runtime. GitHub, Facebook, and X use built-in custom OAuth flows.
|
|
290
290
|
|
|
291
291
|
```ts
|
|
292
292
|
import {
|
|
293
293
|
createAuth0AuthProvider,
|
|
294
|
-
createAtmosphereAuthProvider,
|
|
295
294
|
createFacebookAuthProvider,
|
|
296
295
|
createGitHubAuthProvider,
|
|
297
296
|
createGoogleAuthProvider,
|
|
@@ -307,12 +306,6 @@ let auth0Provider = createAuth0AuthProvider({
|
|
|
307
306
|
redirectUri: new URL('/auth/auth0/callback', env.APP_ORIGIN),
|
|
308
307
|
})
|
|
309
308
|
|
|
310
|
-
let atmosphereProvider = createAtmosphereAuthProvider({
|
|
311
|
-
clientId: new URL('/oauth/client-metadata.json', env.APP_ORIGIN),
|
|
312
|
-
redirectUri: new URL('/auth/atmosphere/callback', env.APP_ORIGIN),
|
|
313
|
-
sessionSecret: env.SESSION_SECRET,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
309
|
let facebookProvider = createFacebookAuthProvider({
|
|
317
310
|
clientId: env.FACEBOOK_CLIENT_ID,
|
|
318
311
|
clientSecret: env.FACEBOOK_CLIENT_SECRET,
|
|
@@ -361,11 +354,7 @@ Notes:
|
|
|
361
354
|
- `createMicrosoftAuthProvider()` adds the `tenant` option and builds the issuer from it
|
|
362
355
|
- `createOktaAuthProvider()` expects the full Okta issuer URL, usually something like `https://example.okta.com/oauth2/default`
|
|
363
356
|
- `createAuth0AuthProvider()` expects your Auth0 domain and derives the issuer URL for you
|
|
364
|
-
- `
|
|
365
|
-
- Atmosphere callback routes pass the module-scope provider directly to `finishExternalAuth()`; the original handle or DID is stored in the sealed OAuth transaction state
|
|
366
|
-
- `createAtmosphereAuthProvider()` requires `sessionSecret` and seals the in-flight account, authorization server, nonce, and DPoP key state into the existing OAuth transaction stored in your app session, so you do not need a separate file or database store for the redirect step
|
|
367
|
-
- `createAtmosphereAuthProvider()` returns DPoP-bound token material in `result.tokens`, including `accessToken`, `refreshToken`, authorization server refresh details, and `dpop` JWK state for follow-up DPoP-signed requests
|
|
368
|
-
- `refreshExternalAuth()` supports built-in OIDC providers, X, and Atmosphere when the stored token bundle includes a refresh token
|
|
357
|
+
- `refreshExternalAuth()` supports built-in OIDC providers and X when the stored token bundle includes a refresh token
|
|
369
358
|
- Providers only return refresh tokens when configured to request offline access, such as `authorizationParams: { access_type: 'offline' }` for Google or adding `offline.access` to X scopes
|
|
370
359
|
- Use `mapProfile()` with `createOIDCAuthProvider()` when you want `result.profile` to have an app-specific type before it reaches your route code
|
|
371
360
|
|
|
@@ -425,6 +414,53 @@ router.get('/auth/company/callback', async (context) => {
|
|
|
425
414
|
})
|
|
426
415
|
```
|
|
427
416
|
|
|
417
|
+
Provider packages for other OAuth protocols can use `createOAuthProvider()` to implement the authorization, callback, and optional refresh hooks consumed by `startExternalAuth()`, `finishExternalAuth()`, and `refreshExternalAuth()`.
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { createOAuthProvider } from 'remix/auth'
|
|
421
|
+
import type { OAuthTokens } from 'remix/auth'
|
|
422
|
+
|
|
423
|
+
interface AcmeProfile {
|
|
424
|
+
id: string
|
|
425
|
+
email: string
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface AcmeTokens extends OAuthTokens {
|
|
429
|
+
resourceServer: string
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function createAcmeAuthProvider(options: AcmeAuthProviderOptions) {
|
|
433
|
+
return createOAuthProvider<AcmeProfile, 'acme', AcmeTokens>('acme', {
|
|
434
|
+
async createAuthorizationURL(transaction) {
|
|
435
|
+
let metadata = await discoverAuthorizationServer(options)
|
|
436
|
+
|
|
437
|
+
transaction.providerState = await encryptProviderState(metadata)
|
|
438
|
+
return createAuthorizationRequest(metadata, transaction)
|
|
439
|
+
},
|
|
440
|
+
async handleCallback(context, transaction) {
|
|
441
|
+
let metadata = await decryptProviderState(transaction.providerState)
|
|
442
|
+
let tokens = await exchangeAuthorizationCode(metadata, context, transaction)
|
|
443
|
+
let profile = await loadProfile(tokens)
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
provider: 'acme',
|
|
447
|
+
account: {
|
|
448
|
+
provider: 'acme',
|
|
449
|
+
providerAccountId: profile.id,
|
|
450
|
+
},
|
|
451
|
+
profile,
|
|
452
|
+
tokens,
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
async refreshTokens(tokens) {
|
|
456
|
+
return refreshAcmeTokens(tokens)
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The runtime may write a serialized value to `transaction.providerState` during `createAuthorizationURL()`. Remix persists that value with the OAuth transaction and returns it to `handleCallback()`. Treat it as provider-owned opaque data, and encrypt sensitive values because session storage is not guaranteed to be confidential. Extending `OAuthTokens` preserves provider-specific fields through callback and refresh results without requiring those fields to become part of Remix's built-in token model.
|
|
463
|
+
|
|
428
464
|
## Related Packages
|
|
429
465
|
|
|
430
466
|
- [`auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) - Request authentication and route protection helpers
|