Integrating Tailwind CSS with dune
Despite not being a great fan, Tailwind is an extremely popular choice for styling, and if you're building web applications with Melange or any other dune-based system, integrating it is surprisingly easy.
This tutorial shows you how to set it up so dune build handles both JavaScript compilation and CSS generation in one go. A single watch process carried by dune.
#Prerequisites
Before we start, you'll need:
-
A Melange project with dune. If you're starting from scratch, the easiest way is to use create-melange-app:
npx create-melange-app my-app cd my-app -
Tailwind CSS installed via npm
npm install -D @tailwindcss/cli -
A CSS entrypoint file (
styles.css) with your Tailwind directives@import "tailwindcss"; @theme { --font-display: "Satoshi", "sans-serif"; }Check the Tailwind documentation for the complete customization.
#Setting up the dune rules
The trick to making Tailwind work seamlessly with Melange is to use dune's install and rule stanzas. The rule lets you define custom build steps that integrate with dune's dependency tracking and caching.
Here's what you need to add to your dune file:
(install
; This tells `dune` where to install the tailwind executable
; "sections" are predefined installation locations within a package (like `bin` for executables, `lib` for libraries, `share` for data files and `docs`)
(section bin)
; Replace this with your package name
(package my-app)
; Make the tailwind CLI available as an executable called "tailwind"
; by copying it from node_modules to the install directory
(files
("../node_modules/@tailwindcss/cli/dist/index.mjs" as tailwind)))
(rule
; The output file that will be generated
(target output.css)
; Dependencies that this rule needs to run
(deps
; :input is your source CSS file with Tailwind directives
(:input ./styles.css)
; Watch the source_tree so Tailwind runs on each change on your source files
(source_tree .))
; The command to execute: runs the tailwind CLI
(action
(run tailwind -i %{input} -o %{target})))For more details about rules, check the dune documentation on rules.
Now you can generate your CSS with:
dune build output.cssOr if you want to rebuild on changes:
dune build --watch output.cssThe generated output.css will be in _build/default/output.css (relative to your dune file's location) and will contain only the Tailwind utilities you're actually using in your project.
Dune will only rebuild output.css when one of its dependencies changes, making your builds fast and efficient.
dune build would also run the rule.
#Specifying dependencies
A crucial part of dune rules is explicitly declaring what your rule depends on. When any of these dependencies change, dune will automatically re-run the rule.
In our Tailwind setup, we use (source_tree .) to watch the entire source tree. This is important because Tailwind scans your source files for class names to determine which CSS to generate. When you add a new class to your code, Tailwind needs to regenerate the CSS.
Dune supports many types of dependencies beyond source_tree:
(file <filename>)- depend on a specific file(glob_files *.ml)- depend on all files matching a pattern(alias <name>)- depend on an alias being built(universe)- depend on everything (use when you can't specify exact dependencies)
For the complete list of dependency types, see the dune dependency specification documentation.
If you're unsure what dependencies to declare, you can use (universe) as a catch-all, though this prevents dune from caching the result effectively. It's better to be specific when possible.
#Using the generated CSS
With the rule above, your output.css will be in _build/default/output.css. You can reference it in your HTML:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="_build/default/output.css">
</head>
<body> <!-- ... --> </body>
</html>This works, but knwoing the file structure under _build can feel a bit awkward. That's where promotion comes in.
#Promotion
By default, dune generates files in the _build directory by replicating your folder structure and store build metadata.
In this case, you want the generated output.css to appear in your source tree (so you can reference it directly in your HTML). That's "promotion" (promote the file to your source code). Enabled with (mode promote) into to a rule:
(rule
(target output.css)
(deps
(:input ./styles.css)
(source_tree .))
(action
(run tailwind -i %{input} -o %{target}))
; Promote the generated file to the source tree
(mode promote))Now when you run dune build, the output.css will be copied to your source directory alongside your dune file. This makes it easy to reference in your HTML:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="output.css">
</head>
<body> <!-- ... --> </body>
</html>The promoted output.css file will be regenerated whenever your Tailwind config or input styles change, keeping your styling in sync with your code.
#Production builds
For production, you'll want the --minify flag to reduce file size. You can use dune aliases to have separate development and production builds:
(rule
(target output.min.css)
(alias prod)
(deps
(:input ./styles.css)
(source_tree .))
(action
(run tailwind -i %{input} -o %{target} --minify))
(mode promote))During development, run dune build --watch. For production, run dune build @prod which generates a minified output.min.css.
Aliases are just one way to handle this. You could also use enabled_if to conditionally enable rules based on environment variables or other conditions.
That's the setup: dune build now handles your Melange compilation and Tailwind CSS generation together, with proper dependency tracking and caching.
Happy hacking with Melange!
Thanks for reading! If something's unclear or you think I'm wrong, tell me. Feedback is appreciated.
@davesnx