I’ve recently been doing a lot of work with TypeScript and Vite, and one of the issues I’ve run into is my vite.config.ts file can become a mess almost too quickly. Unfortunately, Vite’s strength of configurability is also its most significant barrier: so many options exist!

In this post, we’ll see how you can set up your Vite asset management to predictably place assets where you want them while taking advantage of Vite’s build pipeline to hash and chunk files. I’ll also introduce you to a Vite plugin that I think should be part of Vite out of the box.

Let’s get started!

A Vite Project and Goals

When starting a Vite-powered project, you’ll likely pick your frontend framework, possibly set up TypeScript, install external dependencies, and add assets like images, videos, and other static files. However, it’s important to note that Vite operates in what I refer to as a split-mind approach. As a result, what happens in development mode differs from what you expect during the build process. Your development folder can have one organizational strategy, while your distribution folder takes an entirely different direction.

Understanding and configuring where your final build artifacts end up is an optional feature of Vite, but taking the time to think through the process can set you up for success when deploying your app.

For example, having images in a dedicated folder means you can more easily manage your static assets in a hosting environment. Additionally, some files, like RSS feeds, might not benefit from the hash and be counterproductive. Finally, having folders allows for CDN and syncing strategies at a higher level than a single folder or file.

So, how do you manage your Vite assets? Let me show you.

Vite Assets Configuration

Vite options have a lot of methods and values you can override to create the particular behavior you seek. In short, you need to override the following Vite option values:

  • build.rollupOptions.output.entryFileNames
  • build.rollupOptions.output.assetFileNames
  • build.rollupOptions.output.chunkFileNames

While you could certainly set these values inline, you’ll find your vite.config.ts file quickly becomes unruly. So what I did was create a file at config/assets.ts. Let’s take a look at it.

import {PreRenderedAsset} from "rollup";

type AssetOutputEntry = {
    output: string,
    regex: RegExp
}

export const assetDir = "assets";
export const entryFileNames = `${assetDir}/js/[name]-[hash].js`;
export const chunkFileNames = `${assetDir}/js/[name]-[hash]-chunk.js`
const assets: AssetOutputEntry[] = [
    {
        output: `${assetDir}/img/[name]-[hash][extname]`,
        regex: /\.(png|jpe?g|gif|svg|webp|avif)$/
    },
    {
        regex: /\.css$/,
        output: `${assetDir}/css/[name]-[hash][extname]`
    },
    {
        output: `${assetDir}/js/[name]-[hash][extname]`,
        regex: /\.js$/
    },
    {
        output: `[name][extname]`,
        regex: /\.xml$/
    }
];

export function processAssetFileNames(info: PreRenderedAsset): string {
    if (info && info.name) {
        const name = info.name as string;
        const result = assets.find(a => a.regex.test(name));
        if (result) {
            return result.output;
        }
    }
    // default since we don't have an entry
    return `${assetDir}/[name]-[hash][extname]`
}

The file allows anyone to manage assets by setting a new AssetOutputEntry item with a corresponding output and regex value. As Vite finds new assets to process, the method processAssetFileNames will be passed the information, at which point you can route the file to its final destination. Let’s see how you use this new file in your vite.config.ts file.

import {defineConfig} from "vite";
import {processAssetFileNames, entryFileNames, chunkFileNames, assetDir} from "./config/assets";
import {resolve} from "path";
import viteHtmlResolveAlias from 'vite-plugin-html-resolve-alias'

export default defineConfig({

    resolve: {
        alias: {
            '@img': resolve(__dirname, 'src/img/'),
            '@rss': resolve(__dirname, 'src/rss.xml')
        }
    },

    plugins: [
      viteHtmlResolveAlias()
    ],

    build: {
        minify: true,
        assetsDir: assetDir,
        // don't inline anything for demo
        assetsInlineLimit: 0,
        emptyOutDir: true,
        rollupOptions: {
            output: {
                entryFileNames: entryFileNames,
                assetFileNames: processAssetFileNames,
                chunkFileNames: chunkFileNames
            }
        }
    }
});

Now you can manage all your assets from your assets.ts file using regular expressions. Neat!

Now let’s look at how we can use the assets in our HTML files.

HTML Resolve Alias Vite Assets

You will want to use some assets in HTML and JavaScript files. For example, I’m using an image of vite.svg in the main.ts file and index.html in this sample.

Note: The following package says it’s incompatible with the latest Vite version, but it’s not. So you may need to perform an npm install --force to get the package installed.

Using the package vite-plugin-html-resolve-alias, I can create resolution aliases that the plugin will process in my HTML files. But first, let’s see what setting up aliases looks like. You may have noticed the resolve section in my previous configuration file.

resolve: {
    alias: {
        '@img': resolve(__dirname, 'src/img/'),
        '@rss': resolve(__dirname, 'src/rss.xml')
    }
},

These are aliases to files found in my src directory that I want to run through the Vite asset pipeline and use in TypeScript and HTML. To use these files in HTML, we can use our new aliases.

<link rel="icon" type="image/svg+xml" href="@img/vite.svg"/>
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="@rss"/>

When we call to build, these assets are processed similarly to any other asset imported in our TypeScript. Neat! Here’s a snippet from the final build output.

<link rel="icon" type="image/svg+xml" href="/assets/img/vite-4a748afd.svg">
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/rss.xml">

Depending on the use case, I’m no longer hampered by duplicate assets in public or src. I can use the same asset in both static HTML and dynamic JavaScript. That’s a win-win!

Conclusion

Vite’s asset pipeline is robust, especially for folks building web applications. With some care and thought, you can decide how assets are processed and where they end up in the final distribution folder.

If you’d like to see a working sample of this blog post, I’ve uploaded it to my GitHub repository, where you can see the build in action.

I hope you found this post helpful, and let me know what you think. As always, thanks for reading and sharing my posts. Cheers.