Files
dotfiles/dot_agents/skills/remix/references/assets-and-browser-modules.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

5.0 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, allow, deny, fingerprinting, compiler options)
  • Choosing between staticFiles() for already-built files and createAssetServer() for source assets that need import rewriting, preloads, or fingerprinted URLs
  • Generating script URLs or <link rel="modulepreload"> tags for a client entry
  • Keeping server-only files out of the browser via deny rules

For routing the URL namespace itself, see routing-and-controllers.md. For client entry hydration, see hydration-frames-navigation.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 under app/assets/, 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',
  },
  allow: ['app/assets/**', 'node_modules/**'],
  deny: ['app/**/*.server.*'],
  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 allow and deny as the security boundary for browser-reachable source files.
  • Add a deny list for server-only modules such as *.server.*, private config, or other files that should never be exposed.
  • Set rootDir explicitly in monorepos so relative paths resolve from the intended project root.
  • basePath is the public URL namespace handled by the asset server.
  • fileMap keys are URL patterns relative to basePath, and values are root-relative file path patterns. They use route-pattern syntax on both sides.
  • Keep the same wildcard params on both sides of a fileMap entry so import rewriting can map source files back to public URLs.
  • CSS files are compiled and served alongside scripts. Local CSS @import rules 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/assets/entry.ts')
let preloads = await assetServer.getPreloads(['app/assets/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 watch enabled so source changes are picked up without restarting the server
  • Prefer stable URLs with normal revalidation
  • Enable source maps when debugging browser code

In deployment:

  • Set watch: false
  • Use fingerprint: { buildId } for long-lived immutable caching
  • Make sure buildId changes for each deploy

Fingerprinting assumes files on disk are stable and requires watch: false.

Useful Compiler Options

  • minify for production minification of scripts and styles
  • sourceMaps for 'external' or 'inline' source maps for scripts and styles
  • sourceMapSourcePaths for 'url' or 'absolute' source map paths
  • target as an object for shared browser targets and script-only ECMAScript output, such as { es: '2020', chrome: '109', safari: '16.4' }
  • scripts.define to replace globals such as process.env.NODE_ENV
  • scripts.external to leave specific script imports untouched

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.