@remix-run/cli 0.6.0 → 0.7.0
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/package.json +8 -8
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +17 -10
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +36 -0
- package/template/.agents/skills/remix/references/middleware-and-server.md +1 -1
- package/template/app/actions/document.tsx +7 -4
- package/template/app/actions/public/entry.ts +15 -29
- package/template/app/assets.ts +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Command-line interface for Remix",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"jsonc-parser": "^3.3.1",
|
|
35
35
|
"semver": "^7.7.4",
|
|
36
|
-
"@remix-run/
|
|
37
|
-
"@remix-run/data-table
|
|
38
|
-
"@remix-run/data-table-
|
|
39
|
-
"@remix-run/data-table-
|
|
40
|
-
"@remix-run/
|
|
41
|
-
"@remix-run/
|
|
42
|
-
"@remix-run/
|
|
36
|
+
"@remix-run/assets": "^0.7.0",
|
|
37
|
+
"@remix-run/data-table": "^0.5.1",
|
|
38
|
+
"@remix-run/data-table-postgres": "^0.5.2",
|
|
39
|
+
"@remix-run/data-table-mysql": "^0.5.2",
|
|
40
|
+
"@remix-run/data-table-sqlite": "^0.6.2",
|
|
41
|
+
"@remix-run/terminal": "^0.1.1",
|
|
42
|
+
"@remix-run/test": "^0.6.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^24.6.0",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
How to serve browser scripts and styles from source. Read this when the task involves:
|
|
6
6
|
|
|
7
7
|
- Configuring `createAssetServer` (`basePath`, `mounts`, `allowFiles`, `allowPackages`, `denyFiles`, fingerprinting, compiler options)
|
|
8
|
-
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need
|
|
8
|
+
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need dependency resolution, preloads, or fingerprinted URLs
|
|
9
9
|
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry
|
|
10
10
|
- Enabling browser HMR for source-served modules
|
|
11
11
|
- Keeping files such as tests out of the browser via `denyFiles` rules
|
|
@@ -16,7 +16,7 @@ For routing the URL namespace itself, see `routing-and-controllers.md`. For clie
|
|
|
16
16
|
|
|
17
17
|
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles, and monorepo code that should be compiled and served under a public URL namespace.
|
|
18
18
|
|
|
19
|
-
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need
|
|
19
|
+
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need compilation, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
|
|
20
20
|
|
|
21
21
|
## Default Pattern
|
|
22
22
|
|
|
@@ -71,16 +71,17 @@ export default createController(routes, {
|
|
|
71
71
|
|
|
72
72
|
## Rendering HTML
|
|
73
73
|
|
|
74
|
-
Use `
|
|
74
|
+
Use `getScriptEntry()` when rendering a browser script entry. Scripts keep JavaScript imports as authored, so a rendered script entry needs its public URL, modulepreload hints, and an import map.
|
|
75
75
|
|
|
76
76
|
```typescript
|
|
77
|
-
let
|
|
78
|
-
let entryPreloads = await assets.getPreloads('app/actions/public/entry.ts')
|
|
77
|
+
let { href, importMap, preloads } = await assets.getScriptEntry('app/actions/public/entry.ts')
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
Render `importMap` with `ImportMap` from `remix/ui/server` before the modulepreload links and module script. This combines its mappings with import maps from blocking client entries.
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
Use `getHref()` directly when you need the public URL for a non-script asset, and `getPreloads()` when you need lower-level preload control for one or more entrypoints.
|
|
83
|
+
|
|
84
|
+
For normal Remix applications, pass the asset server to `render({ assets })` from `remix/middleware/render`. The middleware resolves source entry IDs from `clientEntry(import.meta.url, ...)` with `getScriptEntry()`, includes their import maps, and applies the UI renderer's explicit-hash or named-component export rules. Use a custom `resolveClientEntry` callback only when building a custom rendering pipeline.
|
|
84
85
|
|
|
85
86
|
## Development vs Deployment
|
|
86
87
|
|
|
@@ -91,12 +92,15 @@ In development:
|
|
|
91
92
|
- Enable source maps when debugging browser code
|
|
92
93
|
- Use `hmr` only when the app is running under `remix/node-hmr`
|
|
93
94
|
- Use `scripts.loaders` for development-only browser transforms such as `uiHmr()`
|
|
95
|
+
- Configure `hmr.moduleImporter` with `remix/multiple-import-maps-polyfill` when HMR must support browsers without native support for multiple import maps. HMR appends updated mappings in additional `<script type="importmap">` elements
|
|
94
96
|
|
|
95
97
|
In deployment:
|
|
96
98
|
|
|
97
99
|
- Set `watch: false`
|
|
98
|
-
- Use `fingerprint:
|
|
99
|
-
-
|
|
100
|
+
- Use `fingerprint: true` for content-based fingerprints and long-lived immutable caching
|
|
101
|
+
- Render the script import map before modulepreload links and module scripts
|
|
102
|
+
- Keep bare-import resolution uniform for files in the same directory; different directories may use
|
|
103
|
+
different resolutions through more-specific import map scopes
|
|
100
104
|
|
|
101
105
|
Fingerprinting assumes files on disk are stable and requires `watch: false`.
|
|
102
106
|
|
|
@@ -117,7 +121,10 @@ const assetServer = createAssetServer({
|
|
|
117
121
|
denyFiles: ['app/**/*.test.*'],
|
|
118
122
|
watch: isDevelopment,
|
|
119
123
|
hmr: isHmr
|
|
120
|
-
?
|
|
124
|
+
? {
|
|
125
|
+
channel: async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel(),
|
|
126
|
+
moduleImporter: 'remix/multiple-import-maps-polyfill',
|
|
127
|
+
}
|
|
121
128
|
: undefined,
|
|
122
129
|
scripts: {
|
|
123
130
|
loaders: isHmr ? [uiHmr()] : undefined,
|
|
@@ -88,6 +88,42 @@ The resolved `preloads` array contains browser module hrefs. During server rende
|
|
|
88
88
|
|
|
89
89
|
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
|
|
90
90
|
|
|
91
|
+
Client entries introduced by later frame responses may depend on import map entries that were not in the initial document. Browsers without native support for multiple import maps cannot resolve those modules with `import()`. Apps that use asset server import maps and target these browsers can opt into `remix/multiple-import-maps-polyfill`:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import {
|
|
95
|
+
detectMultipleImportMapSupport,
|
|
96
|
+
importModule,
|
|
97
|
+
preloadShim,
|
|
98
|
+
} from 'remix/multiple-import-maps-polyfill'
|
|
99
|
+
import { run } from 'remix/ui'
|
|
100
|
+
|
|
101
|
+
const app = run({
|
|
102
|
+
async loadModule(moduleUrl, exportName) {
|
|
103
|
+
let mod = await importModule(moduleUrl)
|
|
104
|
+
let Component = mod[exportName]
|
|
105
|
+
if (typeof Component !== 'function') {
|
|
106
|
+
throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
|
|
107
|
+
}
|
|
108
|
+
return Component
|
|
109
|
+
},
|
|
110
|
+
async processClientEntryPreloads(preloads) {
|
|
111
|
+
if (await detectMultipleImportMapSupport()) return preloads
|
|
112
|
+
|
|
113
|
+
preloadShim(preloads)
|
|
114
|
+
return []
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
app.addEventListener('error', (event) => {
|
|
119
|
+
console.error('Component error:', event.error)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await app.ready()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The support check keeps native imports and modulepreload links in browsers that support multiple import maps. Other browsers use the polyfill for late client entries and their preloads. Apps that do not need this compatibility can use `import()` directly in `loadModule` and omit `processClientEntryPreloads`.
|
|
126
|
+
|
|
91
127
|
```tsx
|
|
92
128
|
import { run } from 'remix/ui'
|
|
93
129
|
|
|
@@ -69,7 +69,7 @@ let router = createRouter({ middleware })
|
|
|
69
69
|
### Static files vs browser modules
|
|
70
70
|
|
|
71
71
|
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in the root `public/` directory
|
|
72
|
-
- Use `remix/assets` when browser modules should be compiled and served from source files with
|
|
72
|
+
- Use `remix/assets` when browser modules should be compiled and served from source files with dependency resolution, preloads, or fingerprinted URLs
|
|
73
73
|
- `public/` directories inside `app/` hold browser-reachable source for the asset server
|
|
74
74
|
|
|
75
75
|
### Ordering notes
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Handle, RemixNode } from 'remix/ui'
|
|
2
2
|
import { css } from 'remix/ui'
|
|
3
|
+
import { ImportMap } from 'remix/ui/server'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { scriptEntry } from '../assets.ts'
|
|
5
6
|
|
|
6
7
|
export interface DocumentProps {
|
|
7
8
|
children?: RemixNode
|
|
@@ -14,6 +15,7 @@ const DEFAULT_TITLE = readAppDisplayName('%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%
|
|
|
14
15
|
export function Document(handle: Handle<DocumentProps>) {
|
|
15
16
|
return () => {
|
|
16
17
|
let { children, head, title = DEFAULT_TITLE } = handle.props
|
|
18
|
+
let { href, importMap, preloads } = scriptEntry
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
21
|
<html lang="en">
|
|
@@ -24,10 +26,11 @@ export function Document(handle: Handle<DocumentProps>) {
|
|
|
24
26
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
25
27
|
<title>{title}</title>
|
|
26
28
|
{head}
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
+
<ImportMap value={importMap} />
|
|
30
|
+
{preloads.map((preloadHref) => (
|
|
31
|
+
<link key={preloadHref} rel="modulepreload" href={preloadHref} />
|
|
29
32
|
))}
|
|
30
|
-
<script type="module" src={
|
|
33
|
+
<script type="module" src={href}></script>
|
|
31
34
|
</head>
|
|
32
35
|
<body mix={css({ margin: 0 })}>{children}</body>
|
|
33
36
|
</html>
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectMultipleImportMapSupport,
|
|
3
|
+
importModule,
|
|
4
|
+
preloadShim,
|
|
5
|
+
} from 'remix/multiple-import-maps-polyfill'
|
|
1
6
|
import { run } from 'remix/ui'
|
|
2
7
|
|
|
3
8
|
const app = run({
|
|
4
9
|
async loadModule(moduleUrl, exportName) {
|
|
5
|
-
let mod = await
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let response = await fetch(src, {
|
|
10
|
-
headers: { Accept: 'text/html' },
|
|
11
|
-
method: options?.method,
|
|
12
|
-
body: getRequestBody(options?.formData, options?.method, options?.encType),
|
|
13
|
-
signal: options?.signal,
|
|
14
|
-
})
|
|
15
|
-
if (!response.ok) {
|
|
16
|
-
return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`
|
|
10
|
+
let mod = await importModule(moduleUrl)
|
|
11
|
+
let Component = mod[exportName]
|
|
12
|
+
if (typeof Component !== 'function') {
|
|
13
|
+
throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
|
|
17
14
|
}
|
|
15
|
+
return Component
|
|
16
|
+
},
|
|
17
|
+
async processClientEntryPreloads(preloads) {
|
|
18
|
+
if (await detectMultipleImportMapSupport()) return preloads
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
return
|
|
20
|
+
preloadShim(preloads)
|
|
21
|
+
return []
|
|
21
22
|
},
|
|
22
23
|
})
|
|
23
24
|
|
|
@@ -31,18 +32,3 @@ if (import.meta.hot) {
|
|
|
31
32
|
}
|
|
32
33
|
})
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
function getRequestBody(
|
|
36
|
-
formData?: FormData,
|
|
37
|
-
method?: string,
|
|
38
|
-
encType?: string,
|
|
39
|
-
): BodyInit | undefined {
|
|
40
|
-
if (!formData || method?.toLowerCase() === 'get') return
|
|
41
|
-
if (encType !== 'application/x-www-form-urlencoded') return formData
|
|
42
|
-
|
|
43
|
-
let body = new URLSearchParams()
|
|
44
|
-
for (let [name, value] of formData) {
|
|
45
|
-
body.append(name, typeof value === 'string' ? value : value.name)
|
|
46
|
-
}
|
|
47
|
-
return body
|
|
48
|
-
}
|
package/template/app/assets.ts
CHANGED
|
@@ -17,12 +17,14 @@ export const assets = createAssetServer({
|
|
|
17
17
|
minify: !isDevelopment,
|
|
18
18
|
watch: isDevelopment,
|
|
19
19
|
hmr: isHmr
|
|
20
|
-
?
|
|
20
|
+
? {
|
|
21
|
+
channel: async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel(),
|
|
22
|
+
moduleImporter: 'remix/multiple-import-maps-polyfill',
|
|
23
|
+
}
|
|
21
24
|
: undefined,
|
|
22
25
|
scripts: { loaders: isHmr ? [uiHmr()] : undefined },
|
|
23
26
|
})
|
|
24
27
|
|
|
25
28
|
const entry = 'app/actions/public/entry.ts'
|
|
26
29
|
|
|
27
|
-
export const
|
|
28
|
-
export const entryPreloads = await assets.getPreloads(entry)
|
|
30
|
+
export const scriptEntry = await assets.getScriptEntry(entry)
|