In particular, I'm using Blazor (server hosted) with ASP.NET Core Preview 8. I tried adding it using LibMan, but that seems to be more about downloading files from a CDN. I'd like to introduce Tailwind to my build process.
Is this a case where I should use something like Webpack? If so, how do I make Webpack part of my build process?
The original answer is still below...
Time has marched on since I originally asked this question. For example, I'm now targeting ES6 on modern browsers without Babel (the main reason why I was using Webpack).
So I thought I might document my current solution (Tailwind cli installed with npm without Webpack)
Assuming you have npm
installed (part of node.js). In the root of your project:
npm init -y
This will create a package.json
file. This is what my file looks like:
{
"name": "holly",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
NB - I made the name
lowercase (the folder/project name is "Holly") - I was looking at the file using VS Code and there was a squiggly line!
Next, I install Tailwind:
npm install -D tailwindcss cross-env
I've also added cross-env
- this is so I can run the same command on my dev machine (Windows) and in my GitHub Action.
After that, generate the Tailwind config file:
npx tailwindcss init
Which should create a file similar to this:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [],
theme: {
extend: {},
},
plugins: [],
}
I'm using ASP.NET Core/Blazor Server. So I set the content
property to the following:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{razor,cshtml,html}"],
theme: {
extend: {},
},
plugins: [],
}
Now that Tailwind is configured, we need an input file for generating the css. In my project, I've created a Styles
folder and a file called app.css
:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
The next step is to create some handy npm scripts. This is what my package.json
file looks like now:
{
"name": "holly",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"cross-env": "^7.0.3",
"tailwindcss": "^3.2.4"
},
"scripts": {
"build": "cross-env NODE_ENV=development ./node_modules/tailwindcss/lib/cli.js -i ./Styles/app.css -o ./wwwroot/css/app.css",
"watch": "cross-env NODE_ENV=development ./node_modules/tailwindcss/lib/cli.js -i ./Styles/app.css -o ./wwwroot/css/app.css --watch",
"release": "cross-env NODE_ENV=production ./node_modules/tailwindcss/lib/cli.js -i ./Styles/app.css -o ./wwwroot/css/app.css --minify"
},
"keywords": [],
"author": "",
"license": "ISC"
}
build
will be used by Visual Studio in DEBUG
modewatch
is just handy for on-the-fly updates in dev mode.release
is for productionFinally, add the following to your project file (Holly.csproj
in my case):
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NpmLastInstall>node_modules/.last-install</NpmLastInstall>
</PropertyGroup>
<!-- Items removed for brevity -->
<Target Name="CheckForNpm" BeforeTargets="NpmInstall">
<Exec Command="npm -v" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="You must install NPM to build this project" />
</Target>
<Target Name="NpmInstall" BeforeTargets="BuildCSS" Inputs="package.json" Outputs="$(NpmLastInstall)">
<Exec Command="npm install" />
<Touch Files="$(NpmLastInstall)" AlwaysCreate="true" />
</Target>
<Target Name="BuildCSS" BeforeTargets="Compile">
<Exec Command="npm run build" Condition=" '$(Configuration)' == 'Debug' " />
<Exec Command="npm run release" Condition=" '$(Configuration)' == 'Release' " />
</Target>
</Project>
This is based on a tutorial I found written by Chris Sainty (which I can't find at the moment). I did, however, find this rather excellent post by Chris on Adding Tailwind CSS v3 to a Blazor app. Though I think this version avoids the integration with Visual Studio.
Make sure you copy and paste the NpmLastInstall
element and the Target
elements to their appropriate locations in your .csproj
file.
If you're in DEBUG
mode, Visual Studio will execute npm run build
. If you're in RELEASE
mode, it'll execute npm run release
. If you've just pulled the repo from the server, Visual Studio should be smart enough to automatically execute npm install
. The only thing is, I don't think it'll run npm install
when the package.json
file has been updated - you'll need to remember to do that manually.
Targeting npm
in the .csproj
file means that Tailwind will build when the ASP.NET Core project builds. Things will stay consistent - if you hit F5
you know the css is up-to-date! The Tailwind JIT is on by default - so Tailwind build times are negligible and do not add much to the total build time.
One final thing. Because Tailwind css is updated by the Visual Studio project - it means the same thing happens in the cloud. Here's a cut down version of my GitHub Action:
name: production-deployment
on:
push:
branches: [ master ]
env:
AZURE_WEBAPP_NAME: holly
AZURE_WEBAPP_PACKAGE_PATH: './Holly'
DOTNET_VERSION: '6.0.x'
NODE_VERSION: '12.x'
jobs:
build-and-deploy-holly:
runs-on: ubuntu-latest
steps:
# Checkout the repo
- uses: actions/checkout@master
# Setup .NET Core 6 SDK
- name: Setup .NET Core ${{ env.DOTNET_VERSION }}
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
# We need Node for npm!
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
# Run dotnet build and publish for holly
- name: Dotnet build and publish for holly
env:
NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }}
NUGET_PASSWORD: ${{ secrets.NUGET_PASSWORD }}
run: |
cd '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}'
dotnet build --configuration Release /warnaserror
dotnet publish -c Release -o 'app'
# Deploy holly to Azure Web apps
- name: 'Run Azure webapp deploy action for holly using publish profile credentials'
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} # Define secret variable in repository settings as per action documentation
package: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}/app'
The Setup Node.js
step ensures that npm
is available before the dotnet build
command is run. The dotnet build
command will trigger the Tailwind build process. Thanks to cross-env
the same command will work on a Windows, macOS or Linux machine.
I think that's everything!
After reviewing the information in this SO post. Here's a quick rundown of what I ended up implementing. It's not perfect and it needs some work. But it's a good starting point (without making things too complicated).
Created npm Package
I ran npm init
in the root of the solution - this created a package.json
file. Based on advice I read, this shouldn't be created underneath a project/sub-folder.
Installed/Configured Webpack
Based on the webpack installation guide, I did the following:
npm install webpack webpack-cli --save-dev
In preparation for my Tailwind setup, I also installed the following (see the webpack.config.js
file below for more details):
npm install css-loader postcss-loader mini-css-extract-plugin --save-dev
npm install tailwindcss postcss-import
And here's my webpack.config.js
file. Note that it's mainly geared towards processing css with Tailwind:
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const bundleFileName = 'holly';
const dirName = 'Holly/wwwroot/dist';
module.exports = (env, argv) => {
return {
mode: argv.mode === "production" ? "production" : "development",
entry: ['./Holly/wwwroot/js/app.js', './Holly/wwwroot/css/styles.css'],
output: {
filename: bundleFileName + '.js',
path: path.resolve(__dirname, dirName)
},
module: {
rules: [{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}]
},
plugins: [
new MiniCssExtractPlugin({
filename: bundleFileName + '.css'
})
]
};
};
In the case of css, this will take a single entry point styles.css
(which is underneath a sub-folder/project called "Holly") and process it with PostCSS/Tailwind CSS. CSS is broken into separate files, but handled by postcss-import
(more on that below). All CSS is compiled into a single file called holly.css
.
Managing Multiple CSS Files
I also have a postcss.config.js
file in the root of my solution:
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('autoprefixer'),
]
}
This configures PostCSS for Tailwind, but also includes postcss-import
. In the Webpack config styles.css
is the entry point for processing:
@import "tailwindcss/base";
@import "./holly-base.css";
@import "tailwindcss/components";
@import "./holly-components.css";
@import "tailwindcss/utilities";
As per the Tailwind documentation postcss-import
module pre-processes the @import
statements before applying Tailwind CSS.
Making it Work
Once everything was configured, I added the following scripts to the npm
package:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --progress --profile",
"watch": "webpack --progress --profile --watch",
"production": "webpack --progress --profile --mode production"
},
To apply Tailwind to the styles.css
file, I ran the following command:
npm run build
It would be nice if I could get Visual Studio to run the above command anytime a file is altered (with a guarantee that it will wait for said compilation when debugging the app) and have Visual Studio show me the errors. But that's another kettle of fish/much more difficult. So I settled on the following workflow.
When I'm debugging on my machine, I run this command in an open terminal:
npm run watch
Whenever a .css file changes, a new holly.css
file is generated. Which works fine while the app is running - I just have to refresh the page after I've made a change.
The production server runs inside a Docker container. So I ended up calling npm run production
in the Dockerfile
:
# Latest .NET Core from https://hub.docker.com/_/microsoft-dotnet-core-sdk/ (not the nightly one)
FROM mcr.microsoft.com/dotnet/core/sdk:3.0.100-preview9-disco AS build-env
# Setup npm!
RUN apt-get -y update && apt-get install npm -y && apt-get clean
WORKDIR /app
COPY . ./
# To run Tailwind via Webpack/Postcss
RUN npm install
RUN npm run production
RUN dotnet restore "./Holly/Holly.csproj"
RUN dotnet publish "./Holly/Holly.csproj" -c Release -o out
As you can see, the build process isn't as simple as hitting the "Start" button in Visual Studio. But the workflow is simple enough for others members of the team to learn. If the above workflow becomes problematic, I'll look at what my options are at that point.
The next thing I'll probably focus on is removing unused Tailwind CSS
If there's anything that doesn't make sense or could be done better, please let me know!