DEV Community

Theodor Heiselberg
Theodor Heiselberg

Posted on • Edited on

(3)Creating the Pinnacle of Niche Software: Using vite-plugin-elm-watch

About

The problem we are trying to solve here is the usual.

  1. Create a stable development environment
  2. Enable Hot reloading
  3. Simplify the development process and prepare for production

In this article we will go through how to use:

  1. vite-plugin-elm-watch
  2. site-config-loader
  3. 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'],
  },
}));

Enter fullscreen mode Exit fullscreen mode

First let's setup vite to handle building Elm

Here is the steps when starting from scratch

  1. elm init
  2. touch src/Main.elm
  3. npm init
  4. npm install vite-plugin-elm-watch --save-dev OR -D
  5. npm install site-config-loader
  6. touch index.html
  7. ! Emmet Abbreviation (To fill index.html)
  8. touch src/main.js
  9. Add to index.html
<body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode
  1. Add to src/index.js
import Main from './Main.elm';
let app = Main.init({
  node: document.getElementById('app')
})
Enter fullscreen mode Exit fullscreen mode
  1. 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"],
  },
}));
Enter fullscreen mode Exit fullscreen mode
  1. npm install vite -D
  2. npx 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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>'
      );
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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)
  ]
}));
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Adding tailwind

  1. npm install -D @tailwindcss/cli @tailwindcss/vite tailwindcss
  2. touch src/site.css
  3. Add content to site.css
@import "tailwindcss";

body {
  @apply bg-pink-50 text-red-900 font-sans;
}
Enter fullscreen mode Exit fullscreen mode
  1. Add usage of tailwind to vite.config.mjs
import tailwindcss from '@tailwindcss/vite';
...
  plugins: [
    elm(),
    devMetaTagPlugin(command),
    tailwindcss()
  ],
Enter fullscreen mode Exit fullscreen mode
  1. Add the css to main.js
import './site.css';
Enter fullscreen mode Exit fullscreen mode
  1. 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.

  1. 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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
adamdicarlo profile image
Adam DiCarlo

I'm a bit confused - devMetaTagPlugin adds 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-loader thing 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 and import.meta.env?

Collapse
 
sukkergris profile image
Theodor Heiselberg

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. :)