Skip to content

Add generic file support to assets#11355

Draft
markdalgleish wants to merge 4 commits into
mainfrom
markdalgleish/assets-files
Draft

Add generic file support to assets#11355
markdalgleish wants to merge 4 commits into
mainfrom
markdalgleish/assets-files

Conversation

@markdalgleish
Copy link
Copy Markdown
Member

@markdalgleish markdalgleish commented May 6, 2026

This adds file asset support to @remix-run/assets so createAssetServer() can serve configured leaf assets like images and fonts alongside compiled scripts and styles from the same URL namespace.

As part of that, the asset server can now rewrite relative CSS url() references when they resolve to configured file assets, and getHref() can generate request URLs for named file transform pipelines with the same fingerprinting and watch-mode invalidation behavior as direct file requests.

  • adds a files option to createAssetServer() with explicit extensions, optional globalTransforms, named transforms, maxRequestTransforms, and transformed-output cache
  • exports defineFileTransform() so request transforms can declare whether they accept no param, a required string param, or an optional string param
  • rewrites relative CSS url() references to asset server URLs when they resolve to configured file assets, while leaving external, root-relative, data:, and fragment-only URLs unchanged
  • serializes request transforms as repeated transform=name[:param] search params in URLs, while keeping tuple-based authoring in assetServer.getHref(..., { transform })
  • fingerprints direct file asset URLs, invalidates file and transformed-file caches in watch mode, and serves direct file responses through the asset server alongside the existing script/style pipeline
  • updates the assets README and demo app to show direct file serving, transformed SVG variants, CSS background images, and transformed-output caching

Basic usage:

import { createAssetServer, defineFileTransform } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  files: {
    extensions: ['.svg', '.png', '.jpg', '.jpeg'],
    transforms: {
      recolor: defineFileTransform({
        extensions: ['.svg'],
        param: true,
        async transform(bytes, { param }) {
          if (!/^#?(?:[\da-f]{3,4}|[\da-f]{6}(?:[\da-f]{2})?)$/i.test(param)) {
            throw new TypeError('Expected a hex color, with or without a leading #')
          }

          let svg = new TextDecoder().decode(bytes)
          return svg.replaceAll('currentColor', `${!param.startsWith('#') ? '#' : ''}${param}`)
        },
      }),
    },
  },
})

let logoUrl = await assetServer.getHref('app/assets/logo.svg', {
  transform: [['recolor', '#8B5CF6']],
})
// '/assets/app/assets/logo.svg?transform=recolor%3A%238B5CF6'

You can also use relative paths with transforms in CSS files and these will be resolved at compilation time:

.selector {
  background-image: url('./image.png?transform=resize:100x100&transform=webp');
}

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Preview Build Available

A preview build has been created for this PR. You can install it using:

pnpm install "remix-run/remix#preview/pr-11355&path:packages/remix"

This preview build will be updated automatically as you push new commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant