npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@remix-run/assets

v0.3.0

Published

Fetch-based server for compiling browser JS/TS and CSS assets on demand

Readme

assets

Fetch-based server for compiling browser JS/TS and CSS assets on demand.

Features

  • On-Demand Compilation - Compile browser scripts and styles on demand
  • Custom File Mapping - Define patterns for mapping public URLs to file paths on disk
  • Access Control - Control exactly which files can be served with allow and deny rules
  • Preloads - Generate preload URLs for scripts and styles based on imports
  • Caching - Conservative caching by default with stable URLs, ETags, and revalidation
  • Optional Fingerprinting - Source-based fingerprinted URLs for long-lived browser caching
  • Source Maps - Serve inline or external sourcemaps

Installation

npm i remix

Usage

Use createAssetServer to serve browser JS/TS and CSS assets from a URL namespace in your app.

import { createRouter } from 'remix/fetch-router'
import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: {
    '/app/*path': 'app/*path',
    '/npm/*path': 'node_modules/*path',
  },
  allow: ['app/assets/**', 'node_modules/**'],
})

let router = createRouter()

router.get('/assets/*', ({ request }) => {
  return assetServer.fetch(request)
})

This example gives you an /assets/* endpoint that serves compiled browser assets from app/assets and node_modules.

Root Directory

Use rootDir to specify the root directory of the asset server, which is used to resolve relative file paths. Defaults to process.cwd().

import * as path from 'node:path'
import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  rootDir: path.resolve(import.meta.dirname, '..'),
  basePath: '/assets',
  fileMap: {
    '/app/*path': 'app/*path',
    '/npm/*path': 'node_modules/*path',
  },
  allow: ['app/assets/**', 'node_modules/**'],
})

Access Control

You must provide an allow list to specify which files are allowed to be served. deny is optional and takes precedence over allow.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  deny: ['app/**/*.server.*'],
})

Rules for allow and deny are file paths or globs. Relative values are resolved from rootDir. Absolute file paths match exactly, and absolute directory paths also match their descendants.

File Map

Use fileMap to map public URLs to file paths on disk. basePath defines the shared public mount point, and the fileMap keys are URL patterns relative to that base path. The values are root-relative file path patterns.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: {
    '/app/*path': 'app/*path',
    '/packages/*path': '../packages/*path',
  },
  allow: ['app/assets/**', '../packages/**'],
})

fileMap entries use route-pattern syntax for both URL and file patterns. Wildcards must be named, and the same params must appear in both patterns so imports can be rewritten back to public URLs. For example, with basePath: '/assets', a fileMap key of '/app/*path' is served at /assets/app/*path.

File watching

The file system is watched by default so source changes are picked up without requiring a server restart.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**', 'app/node_modules/**'],
})

When finished with the asset server, call await assetServer.close() to clean up the file watcher.

await assetServer.close()

You can disable file watching if the files on disk won't change, or if watching is managed at a higher level (e.g. Node's --watch flag).

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**', 'app/node_modules/**'],
  watch: false,
})

You can optionally provide an array of glob patterns to the watch.ignore option:

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**', 'app/node_modules/**'],
  watch: {
    ignore: ['**/node_modules/**'],
  },
})

Hrefs

Use assetServer.getHref() when you need the public URL for a served asset. You can provide a root-relative or absolute file path, or a file:// URL.

let src = await assetServer.getHref('app/assets/entry.tsx')
// '/assets/app/assets/entry.tsx'

Preloads

Use assetServer.getPreloads() when rendering HTML so you can turn the returned URLs into <link rel="modulepreload">, stylesheet preload tags, or Link headers for one or more assets and their dependencies. You can provide root-relative or absolute file paths, or file:// URLs.

let preloads = await assetServer.getPreloads(['app/assets/entry.tsx', 'app/assets/search.tsx'])
// [
//   '/assets/app/assets/entry.tsx',
//   '/assets/app/assets/search.tsx',
//   '/assets/app/assets/utils.ts',
//   '/assets/npm/@remix-run/ui/index.js',
//   ...etc
// ]

Fingerprinting

By default, assets are served at stable URLs with ETags and Cache-Control: no-cache. Responses are cached for the lifetime of the asset server instance.

If you want clients to cache assets aggressively without revalidation, you can opt into source-based fingerprinting.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  watch: false,
  fingerprint: {
    buildId: process.env.GITHUB_SHA,
  },
})

When fingerprinting is enabled, assets use a .@<fingerprint> segment before the file extension and are served with Cache-Control: public, max-age=31536000, immutable.

Source fingerprints are based on the original file contents and the build ID. The build ID must change for each deployment so that fingerprinted assets are invalidated together. This fingerprinting strategy assumes that files on disk won't change, so fingerprinting requires watch: false.

Target

Use target to lower emitted syntax to a specific browser support policy and/or ECMAScript version.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  target: {
    chrome: '109',
    ios: '15.6',
    es: '2020',
  },
})

Supported target options are chrome, firefox, safari, edge, opera, ios, samsung, and es (ECMAScript version).

Source Maps

Enable sourcemaps with either 'external' or 'inline' using sourceMaps:

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  sourceMaps: 'external',
})

By default, sourcemap sources use URLs so they're presented alongside the compiled output in your browser's developer tools. You can also use file system paths instead with sourceMapSourcePaths:

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  sourceMaps: 'inline',
  sourceMapSourcePaths: 'absolute',
})

Minification

Enable minification with minify:

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  minify: true,
})

Script Options

Define

Use scripts.define to replace global identifiers with constant expressions.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**', 'app/node_modules/**'],
  scripts: {
    define: {
      'process.env.NODE_ENV': '"production"',
    },
  },
})

Values are injected exactly as defined, so string literals must include their own quotes, e.g. process.env.NODE_ENV must be "production" rather than production.

External Imports

Use scripts.external to leave specific import specifiers unchanged by providing an array of specifiers.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  scripts: {
    external: ['my-external-import'],
  },
})

CSS Imports

Relative CSS @import rules are rewritten to asset server URLs. External @import URLs are left unchanged automatically. url() references are preserved as authored.

/* Rewritten to asset server URL: */
@import './reset.css';
/* External URL: */
@import 'https://fonts.googleapis.com/css2?family=Inter';

Error Handling

Use onError to report unexpected compilation failures and/or return a custom response.

import { createAssetServer } from 'remix/assets'

let assetServer = createAssetServer({
  basePath: '/assets',
  fileMap: { '/app/*path': 'app/*path' },
  allow: ['app/assets/**'],
  onError(error) {
    console.error('Failed to build client assets', error)
    return new Response('Client asset build failed', { status: 500 })
  },
})

If onError returns nothing, the asset server responds with the default 500 Internal Server Error response.

Related Packages

  • fetch-router - A Fetch-based router that pairs naturally with assets
  • route-pattern - Route-pattern syntax for URL and route file matching

License

See LICENSE