/assets/vitejs.png

How to replace files on build time with ViteJS

Motivation

For more complex theming, as a prime example, it is necessary to replace files depending on an environment variable on build time. You can do so with several tools like Webpack or RollUp. You can create your own as well. Today you will see how to do this basically using ViteJS.

If you just want to see the more fleshed out example, here you go: https://github.com/Myrmod/vitejs-theming

Writing the config

To replace files we have to access the build process. To access the build process we have to use a hook and to use a hook we need a plugin. So we will write a plugin.

Let’s start by imagining how we use it. Here we want to call our plugin replace and give it an array of string objects, that represent files in their file hierarchy of the “core” repository, this is where the vite.config.js is located, and the theme with all the replacing files.

Because we need only RollUpJS functionality, we add it to a plugin directory with rollup in its name.

js
// vite.config.js
import { defineConfig } from 'vite'
import replace from './build-plugins/rollup/replace-files'
/**
* https://vitejs.dev/config/
*/
export default defineConfig({
build: {
outDir: 'build',
},
plugins: [
replace([
{
find: "App.tsx",
replacement: "App.tsx",
},
]),
],
})

Writing the plugin

So we start writing our plugin. We expect an array of replacements, if we don’t get some, then we don’t want to slow down our build pipeline and return null. A plugin, or the object that’s returned by it, has a specific structure. At first we need to give it a name. Because we “only” need RollUp functionality we go by the name: “rollup-plugin-replace-files”.

Taking a step back we need to remind ourselves, that we want to replace files on build time. So no other plugin should be allowed to access the files beforehand. That’s why we need enforce: ‘pre’ in the returned object. If we don’t have this, we might not be able to “see” our files in the hook we will be using.

For the hook we will use resolveId. Using this hook in combination with its this.resolve counterpart will enable us to identify the path of the file we’re currently resolving.

As soon as we have the path of our current file, we need to check if we need to replace this path, with the path to our replacement. A simple find function will do:

js
const foundReplace = replacements.find((replacement) => replacement.find === resolved.id);

If we found a replacement for the file, we’re currently resolving we can return an object with our files path as id property. The load hook will automatically adjust accordingly, so your imports in the new files will work seemlessly as well.

js
// build-plugins/rollup/replace-files.js
/**
* @function replaceFiles
* @param {({find: string, replacement: string})[]} replacements
* @return {({name: "rollup-plugin-replace-files", enforce: "pre", Promise<resolveId>})}
*/
export default function replaceFiles(replacements) {
if (!replacements?.length) {
return null;
}
return {
name: 'rollup-plugin-replace-files',
enforce: 'pre',
async resolveId(source, importer) {
const resolved = await this.resolve(source, importer, { skipSelf: true })
const foundReplace = replacements.find((replacement) => replacement.find === resolved.id);
if (foundReplace) {
console.info(`replace "${foundReplace.find}" with "${foundReplace.replacement}"`);
try {
// return new file content
return {
id: foundReplace.replacement,
};
} catch (err) {
console.error(err);
return null;
}
}
return null;
},
}
}

Wrapping Up

That’s everything already. As mentioned this concept can be adapted to multiple build tools. I’ve done so myself using Webpack. ViteJS feels better though. If you have questions feel free to ask or if you want to see a more polished example you can take a look at GitHub.