7.6 KiB
Assets and Browser Modules
What This Covers
How to serve browser scripts and styles from source. Read this when the task involves:
- Configuring
createAssetServer(basePath,fileMap,allowFiles,allowPackages,denyFiles, fingerprinting, compiler options) - Choosing between
staticFiles()for already-built files andcreateAssetServer()for source assets that need import rewriting, preloads, or fingerprinted URLs - Generating script URLs or
<link rel="modulepreload">tags for a client entry - Enabling browser HMR for source-served modules
- Keeping files such as tests out of the browser via
denyFilesrules
For routing the URL namespace itself, see routing-and-controllers.md. For client entry hydration and browser update handling, see hydration-frames-navigation.md. For the Node HMR runner and browser HMR channel, see middleware-and-server.md.
When To Reach For It
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.
Use staticFiles() for files that already exist on disk exactly as they should be served. Use createAssetServer() for source scripts or styles that need rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
Default Pattern
import { createAssetServer } from 'remix/assets'
import { createController } from 'remix/router'
import { get, route } from 'remix/routes'
export const routes = route({
assets: get('/assets/*path'),
})
let assetServer = createAssetServer({
basePath: '/assets',
rootDir: process.cwd(),
fileMap: {
'app/*path': 'app/*path',
'node_modules/*path': 'node_modules/*path',
},
allowFiles: ['app/routes.ts', 'app/**/public/**'],
allowPackages: ['remix'],
denyFiles: ['app/**/*.test.*'],
target: { es: '2020', chrome: '109', safari: '16.4' },
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
minify: process.env.NODE_ENV === 'production',
scripts: {
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'),
},
},
})
export default createController(routes, {
actions: {
async assets({ request }) {
return (await assetServer.fetch(request)) ?? new Response('Not Found', { status: 404 })
},
},
})
Rules
- Treat
allowFiles/allowPackagesanddenyFilesas the security boundary for browser-reachable source files. - Put browser-reachable app source in a
public/directory insideapp/, beside its narrowest owner, such asapp/ui/public/orapp/actions/cart/public/. - Every local dependency in a browser module graph must match
allowFiles, so keep the whole graph inside thosepublic/directories.app/routes.tsis allowed separately so browser modules can build type-safe links withroutes.*.href(...). - Deny test modules with
denyFilesso tests can be colocated inside apublic/directory without becoming browser-reachable. - Use
allowFilesanddenyFilesfor file paths and globs. Relative values resolve fromrootDir. - Use
allowPackagesfor exact package names, not globs or subpaths. Packages allowed byallowPackagesalso allow their installeddependenciesandoptionalDependencies; peer dependencies must be listed explicitly if they should be browser-reachable. denyFilestakes precedence over both file and package allow rules.- Set
rootDirexplicitly in monorepos so relative paths resolve from the intended project root. basePathis the public URL namespace handled by the asset server.fileMapkeys are URL patterns relative tobasePath, and values are root-relative file path patterns. They useroute-patternsyntax on both sides.- Keep the same wildcard params on both sides of a
fileMapentry so import rewriting can map source files back to public URLs. - CSS files are compiled and served alongside scripts. Local CSS
@importrules are rewritten and fingerprinted with the same asset server routing rules.
Rendering HTML
Use getHref() when you need the public URL for one module, and getPreloads() when you want <link rel="modulepreload"> tags or Link headers for one or more entrypoints and their dependencies.
let entryHref = await assetServer.getHref('app/actions/public/entry.ts')
let entryPreloads = await assetServer.getPreloads('app/actions/public/entry.ts')
Use this when rendering documents or layouts that boot browser behavior with a known client entry.
When resolving hydrated client entries during server rendering, pass the source entry ID from clientEntry(import.meta.url, ...) to getHref() inside resolveClientEntry. Keep export-name resolution in that render helper, and avoid hard-coding public asset URLs in source-owned component modules.
Development vs Deployment
In development:
- Keep
watchenabled so source changes are picked up without restarting the server - Prefer stable URLs with normal revalidation
- Enable source maps when debugging browser code
- Use
hmronly when the app is running underremix/node-hmr - Use
scripts.loadersfor development-only browser transforms such asuiHmr()
In deployment:
- Set
watch: false - Use
fingerprint: { buildId }for long-lived immutable caching - Make sure
buildIdchanges for each deploy
Fingerprinting assumes files on disk are stable and requires watch: false.
Browser HMR
Use browser HMR when source-served browser modules should update without a full page reload during development. Let remix/node-hmr own the browser HMR channel so browser updates stay coordinated with server restarts.
import { createAssetServer } from 'remix/assets'
import { uiHmr } from 'remix/ui-hmr/assets'
const isDevelopment = process.env.NODE_ENV === 'development'
const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
const assetServer = createAssetServer({
basePath: '/assets',
fileMap: { '/app/*path': 'app/*path' },
allowFiles: ['app/routes.ts', 'app/**/public/**'],
denyFiles: ['app/**/*.test.*'],
watch: isDevelopment,
hmr: isHmr
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
: undefined,
scripts: {
loaders: isHmr ? [uiHmr()] : undefined,
},
})
Rules:
- Guard
remix/node-hmr/runtimeimports withprocess.env.REMIX_NODE_HMR; that runtime API is only available inside the supervised child process. - Keep browser HMR and loaders development-only.
- Add
remix/assets/types/hmrtocompilerOptions.typesonly when browser source modules useimport.meta.hotdirectly. - Write HMR accept calls directly as
import.meta.hot.accept(...)with literal dependency specifiers.
Useful Compiler Options
minifyfor production minification of scripts and stylessourceMapsfor'external'or'inline'source maps for scripts and stylessourceMapSourcePathsfor'url'or'absolute'source map pathstargetas an object for shared browser targets and script-only ECMAScript output, such as{ es: '2020', chrome: '109', safari: '16.4' }scripts.defineto replace globals such asprocess.env.NODE_ENVscripts.externalto leave specific script imports untouchedscripts.loadersto transform browser modules during compilation
Do not nest shared compiler options under scripts. Use top-level minify, sourceMaps, sourceMapSourcePaths, and target so they apply to styles as well as scripts.
Lifecycle
If the asset server is long-lived and watching the file system, call await assetServer.close() when shutting down dev servers or disposing tests.