About
The problem we are trying to solve here is the usual.
- Create a stable development environment
- Enable Hot reloading
- Simplify the development process and prepare for production
In this article we will go through how to use:
- vite-plugin-elm-watch
- site-config-loader
- Add tailwind css to the project
Key takaways
A working example of this setup running in a devcontaner can be found here - Link! in the branch feature/add-tailwind
host: '0.0.0.0' or host: true is needed in order to run vite from a devcontainer - Documentation - Link!
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import elm from 'vite-plugin-elm-watch';
import devMetaTagPlugin from './vite-plugin-dev-meta.mjs';
export default defineConfig(({ command }) => ({
publicDir: 'public',
build: {
outDir: 'wwwroot',
emptyOutDir: true,
},
plugins: [elm(), tailwindcss(), devMetaTagPlugin(command)],
server: {
open: true,
port: 3456,
host: '0.0.0.0', // Listen on all network interfaces to allow access from the host machine
allowedHosts: ['host.docker.internal', 'localhost'],
},
}));
First let's setup vite to handle building Elm
Here is the steps when starting from scratch
elm inittouch src/Main.elmnpm init-
npm install vite-plugin-elm-watch --save-devOR -D npm install site-config-loadertouch index.html- ! Emmet Abbreviation (To fill index.html)
touch src/main.js- Add to index.html
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
- Add to src/index.js
import Main from './Main.elm';
let app = Main.init({
node: document.getElementById('app')
})
-
touch vite.config.mjs
import { defineConfig } from "vite";
import elm from 'vite-plugin-elm-watch';
export default defineConfig(({ command }) => ({
publicDir: "public",
build: {
outDir: "wwwroot",
emptyOutDir: true,
},
plugins: [elm()],
server: {
open: true,
port: 3456,
host: "0.0.0.0", // Listen on all network interfaces to allow access from the host machine
allowedHosts: ["host.docker.internal", "localhost"],
},
}));
npm install vite -Dnpx vite
Environment Management with site-config-loader
Managing environment variables in a frontend application can be tricky. site-config-loader simplifies this by fetching configuration files based on the environment your site is running in.
To determine the environment, the loader looks for a tag. During development, we can use a custom Vite plugin to inject this tag dynamically.
1. Create Configuration Files
First, we need to store our environment-specific data. We’ll create a default config and a local override.
Bash
mkdir -p public/config
touch public/config/environmentVariables.json
public/config/environmentVariables.local.json
Add your variables to these files. For example:
public/config/environmentVariables.json
{
"API_URL": "https://api.production.com",
"FEATURE_FLAG": false
}
2. Inject the Environment Meta Tag
Since we want to control which configuration the loader picks up during development, we use Vite’s transformIndexHtml hook. By default, site-config-loader might fall back to local based on the URL, but using a meta tag gives us explicit control.
Create a small plugin file: vite-plugin-dev-meta.mjs
export default function devMetaTagPlugin(command) {
// Only apply this transformation during local development ('serve')
if (command !== 'serve') return null;
return {
name: 'html-transform-dev-only',
transformIndexHtml(html) {
return html.replace(
'</head>',
'<meta name="environment-name" content="local"></head>'
);
}
};
}
3. Register the Plugin in Vite
Now, integrate the plugin into your vite.config.mjs to ensure the tag is injected when you run the dev server.
import { defineConfig } from 'vite';
import elm from 'vite-plugin-elm-watch';
import devMetaTagPlugin from './vite-plugin-dev-meta.mjs';
export default defineConfig(({ command }) => ({
// ... other config
plugins: [
elm(),
devMetaTagPlugin(command)
]
}));
4. Verify the Setup
Run your development server:
npm run dev
Open the browser console. You should see site-config-loader successfully detecting the environment and loading the merged configuration from your JSON files.
Adding tailwind
npm install -D @tailwindcss/cli @tailwindcss/vite tailwindcsstouch src/site.css- Add content to site.css
@import "tailwindcss";
body {
@apply bg-pink-50 text-red-900 font-sans;
}
- Add usage of tailwind to
vite.config.mjs
import tailwindcss from '@tailwindcss/vite';
...
plugins: [
elm(),
devMetaTagPlugin(command),
tailwindcss()
],
- Add the css to
main.js
import './site.css';
- Restart the vite server
Using Environment Variables in Elm
Since site-config-loader fetches your configuration asynchronously, we need to wait for the data to be ready before initializing the Elm application. We then pass the configuration into Elm using Flags.
- Update src/main.js Modify your entry point to load the config first, then start Elm:
import '@fortawesome/fontawesome-free/css/all.min.css';
import { loadEnvironmentVariables } from 'site-config-loader';
import './kort-til-kort.css';
import Main from '/src/Main.elm';
const config = await loadEnvironmentVariables('clientsettings');
let app = Main.init({
node: document.getElementById('app'),
flags: {
isDevelopment: config.isDevelopment,
turnstileSiteKey: config.turnstile.sitekey,
googleConfig: {
mapsApiKey: config.googleConfigurations.mapsApiKey,
mapId: config.googleConfigurations.mapId,
},
pixelId: config.facebookPixelId,
},
});
Conclusion
There are quit a few moving parts need to get this up and going.
A Complete example can be found here: Link! in the branch feature/add-tailwind
Next up we will abandon using localhost entirely and use a custom domain name for development. This will also require adding quite a few moving parts, like a reverse proxy to our .devcontainer/docker-compose.yml.
I wrote about it previously here: Link!
Top comments (2)
I'm a bit confused -
devMetaTagPluginadds a production meta tag, not a dev one. (It also seems to imply using Vite to serve in production, which should not be done.)The
site-loaderthing is also unclear to me. How about adding some examples of using the environment variables configured via site-loader, even if just in JS? (There isn't a way that I know of to use them in Elm just from what's here.) JS-wise, how is the site-loader method different from using Vite's built-in environment variable support via .env files andimport.meta.env?Thanks for the feedback I'll get back to clearing things up! I definitely don't use the vite server for production; I only use it to build FOR production.
I'll add an example of exactly how I use the site-config-loader with Elm too. I can see it got at bit too meta for others follow. :)