Minor Changes
-
BREAKING CHANGE: Fingerprinted assets now use hashes of their final emitted bytes instead of per-build source hashes. Replace
createAssetServer({ fingerprint: { buildId } })withcreateAssetServer({ fingerprint: true })(see #11706).If you enabled fingerprinting in
app/assets.ts, update the option:import { createAssetServer } from 'remix/assets' export const assets = createAssetServer({ basePath: '/assets', - fingerprint: { buildId: process.env.GIT_COMMIT_SHA }, + fingerprint: true, // ... })If you use
files.cacheand previously relied onfingerprint.buildIdto namespace persisted transformed file cache entries, move that value tofiles.cacheKey:export const assets = createAssetServer({ files: { cache, + cacheKey: process.env.GIT_COMMIT_SHA, }, fingerprint: true, // ... })Production servers can then reuse transformed file outputs across restarts for the same build. When
files.cacheKeyis omitted, transformed file caches use a random per-process namespace. -
BREAKING CHANGE: Custom browser HMR update events now carry JSON data in a
datarecord instead of top-leveltimestampandupdatesfields (see #11706).Apps using the standard asset server and
createBrowserHmrChannel()integration need no changes to their event handling. Give each tool a separate key in the record, for example{ type: 'update', data: { 'my-tool@1': { version: 1 } } }.type BrowserHmrEvent = { type: 'update' - timestamp: number - updates: HmrBrowserUpdate[] + data: Record<string, BrowserHmrData> } -
BREAKING CHANGE: Browser scripts now use import maps to resolve imports instead of rewriting import specifiers to asset URLs (see #11706). Apps must render each script entry's import map before its modulepreload links and module script.
To migrate an app created from the Remix app template:
In
app/assets.ts, replace the separate entry href and preload calls withgetScriptEntry():const entry = 'app/actions/public/entry.ts' -export const entryHref = await assets.getHref(entry) -export const entryPreloads = await assets.getPreloads(entry) +export const scriptEntry = await assets.getScriptEntry(entry)
HMR appends mappings for updated modules to the document in additional
<script type="importmap">elements. When HMR must support browsers without native support for multiple import maps, configureremix/multiple-import-maps-polyfillas its module importer:export const assets = createAssetServer({ // ... hmr: isHmr - ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel() + ? { + channel: async () => + (await import('remix/node-hmr/runtime')).createBrowserHmrChannel(), + moduleImporter: 'remix/multiple-import-maps-polyfill', + } : undefined, scripts: { loaders: isHmr ? [uiHmr()] : undefined }, })In
app/actions/document.tsx, render the managed import map before the entry's preloads and module script:import type { Handle, RemixNode } from 'remix/ui' import { css } from 'remix/ui' +import { ImportMap } from 'remix/ui/server' -import { entryHref, entryPreloads } from '../assets.ts' +import { scriptEntry } from '../assets.ts' export function Document(handle: Handle<DocumentProps>) { return () => { let { children, head, title = DEFAULT_TITLE } = handle.props + let { href, importMap, preloads } = scriptEntry return ( <html lang="en"> <head> {/* ... */} - {entryPreloads.map((href) => ( - <link key={href} rel="modulepreload" href={href} /> + <ImportMap value={importMap} /> + {preloads.map((preloadHref) => ( + <link key={preloadHref} rel="modulepreload" href={preloadHref} /> ))} - <script type="module" src={entryHref}></script> + <script type="module" src={href}></script> </head> {/* ... */}<ImportMap>combines the entry map with mappings from blocking client entries so the initial document contains a single complete import map. Regular<script type="importmap">elements remain supported when this behavior is not needed.The standard
render({ assets })middleware resolves client entries withgetScriptEntry()and includes their import maps in rendered documents and frame responses. Custom rendering pipelines must include the returnedimportMapin theirresolveClientEntry()metadata.In
app/actions/public/entry.ts, useimportModule()to load client entries andprocessClientEntryPreloadsto preload them in browsers that need the polyfill:import { detectMultipleImportMapSupport, importModule, preloadShim, } from 'remix/multiple-import-maps-polyfill' import { run } from 'remix/ui' run({ async loadModule(moduleUrl, exportName) { let module = await importModule(moduleUrl) let Component = module[exportName] if (typeof Component !== 'function') { throw new Error(`Unknown component: ${moduleUrl}#${exportName}`) } return Component }, async processClientEntryPreloads(preloads) { if (await detectMultipleImportMapSupport()) return preloads preloadShim(preloads) return [] }, })
Modules referenced by dynamic
import()expressions are now fetched when the import runs, instead of being preloaded with the entry script. Imports with static specifiers still have entries in the import map.The HMR module importer must be included in the initial import map. Importing it from the main client entry, as shown above, meets this requirement. If your app sets a Content Security Policy, follow the polyfill CSP requirements.
If you call
renderToStream()directly, resolve client entries withgetScriptEntry()and include theirimportMapin the object returned fromresolveClientEntry():let stream = renderToStream(node, { async resolveClientEntry(entryId, component) { - let [href, preloads] = await Promise.all([ - assets.getHref(entryId), - assets.getPreloads(entryId), - ]) + let { href, importMap, preloads } = await assets.getScriptEntry(entryId) return { href, + importMap, exportName: component.name, preloads, } }, })assets.getImportMap()combines import maps for multiple script entries. The public types areScriptEntryandScriptImportMap.In development, HMR installs new import map entries before loading an update. If an existing mapping would change, it reloads the page. Custom HMR importers can use
hmr.moduleImporter, a module specifier resolved relative to the asset server root. That module must exportimportModule(specifier, parentUrl).
Patch Changes
- Fixed
IMPORT_OUTSIDE_MOUNTSerrors when serving dependencies installed in pnpm's global virtual store outsiderootDir. The asset server now finds the store automatically, without requiring an extra mount in your configuration (see #11814).