After some work, I came up with a solution that works for our team.
Just to reiterate/clarify the goals here, I was trying to do the following:
- Integrate the Tailwind library into our hubspot website with Webpack
- Be able to use Webpack’s --watch command to recompile our CSS on save so we can use CSS nesting and Tailwind’s @apply rule (among other PostCSS goodies)
- Be able to use the Hubspot CLI watch command and a Hubspot sandbox environment to semi-hot-reload our work, on save
- Integrate our project with git versioning but on Bitbucket, rather than Github
Our primary issue was the Tailwind library uncompressed and unpurged was way too large to upload to the Hubspot sandbox since there is an upload file limit of 1.5 MiB and the full Tailwind library is ~3 MiB, minified. To get around this issue, we used Tailwind’s built in purgeCSS which brought our file size down to below 20 KiB—easily handled by Hubspot’s watch.
The problem at this point was that Webpack was not recompiling the CSS when we added net-new Tailwind classes to our elements. In order to get those classes, we had to restart the Webpack --watch or rebuild, which was just annoying, especially when trying to iterate classes quickly.
To solve this, we added a the extra-watch-webpack-plugin and dropped the Tailwind purgeCSS in favor of the purgecss-webpack-plugin which can live outside postCSS—where we were doing our other CSS optimizations. This method (while a bit slower than we were hoping for due to the purging) recomiled our CSS and added new Tailwind classes on save, then successfully uploaded those files through the CMS CLI to our development sandbox.
Our Webpack setup, for those who are curious:
webpack.common.js
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const entry = require('webpack-glob-entry')
const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'templates')
}
module.exports = {
entry: entry('./assets/js/main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
new ExtraWatchWebpackPlugin({
dirs: ['./templates'],
}),
new PurgeCSSPlugin({
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
paths: glob.sync(`${PATHS.src}/*.html`, { nodir: true }),
}),
new MiniCssExtractPlugin({
filename: "[name].min.css"
})
]
}
webpack.dev.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(common, {
mode: 'development',
devtool: 'source-map',
watch: true,
module: {
rules: [
{
test: /\.css$/i,
use: [
'style-loader',
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: true
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
map: true,
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('postcss-nested')
],
},
sourceMap: true
}
},
],
},
],
}
})
webpack.prod.js
const path = require('path')
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(common, {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
'style-loader',
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: true
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
map: false,
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('postcss-nested'), // or require('postcss-nesting')
require('autoprefixer'),
require('cssnano')
],
},
}
},
],
},
{
test: /\.(mjs|js|jsx)$/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
{
'plugins': [
'@babel/plugin-proposal-class-properties'
]
}
]
}
}
],
exclude: path.resolve(__dirname, "node_modules")
},
]
}
})
package.json scripts
"scripts": {
"watch": "webpack --mode=development --config=webpack.dev.js --progress",
"build": "rm -rf dist && webpack --mode=production --config=webpack.prod.js"
},
And for those who are curious about our Bitbucket Pipelines integration:
bitbucket-pipelines.yml
image: node:12.16.1
pipelines:
branches:
master:
- step:
name: Generate hubspot.config.yml and deploy
caches:
- node
script:
- npm install
- npm run build
- ./generate-config.js
- ./deploy.sh
deploy.sh
#!/bin/bash
npx hs upload ../build your-site-name
And, finally, our Tailwind config:
tailwind.config.js
const colors = require('tailwindcss/colors')
module.exports = {
purge: ['./**/*.html'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
gray: colors.blueGray
}
},
},
variants: {
backgroundColor: ['responsive', 'hover', 'focus', 'active'],
color: ['responsive', 'hover', 'focus', 'active'],
borderColor: ['responsive', 'hover', 'focus', 'active'],
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')
],
}