Tailwind not working with Aurelia

Hi, I am really struggling to get tailwind working with Aurelia 1 and would really appreciate some guidance.

Versions
Node 14.15.0
npm 6.14.8
aurelia cli 2.0.3
webpack 5.65.0

I have created a new project using “au new” and selected the following options:
-Custom Project
-App
-Webpack
-Web
-Babel
-No markup processing
-Standard CSS with no pre-processor
-PostCSS Processing with autoprefixer plus cssnano
-Jest
-no e2e

-Minimum aurelia app

I then installed tailwind “npm install tailwindcss -D”

Then “./node_modules/.bin/tailwind init”

I created a styles.css in the src folder with the following contents:

@tailwind base;
@tailwind components;
@tailwind utilities;

I then referenced it in my app.html “”

My app.html is very simple

<template>
  <require from="./styles.css"></require>
  <div class="bg-red-900">
  <h1 class="text-cyan-800">Hello World</h1>
  </div>
</template>


Everything above seems straight forward. The webpack.config.js is where the confusion is. I have seen many different ways to add tailwind and none of them seem to work.

I added the following

const cssRules = [
  {
    loader: "css-loader",
  },
  {
    loader: "postcss-loader",
    options: {
      postcssOptions: {
        plugins: [
          require("tailwindcss")("./tailwind.config.js"),
          "autoprefixer",
          "cssnano"
        ]
      }
    }
  }
];

and the rules are

module: {
    rules: [
      // CSS required in JS/TS files should use the style-loader that auto-injects it into the website
      // only when the issuer is a .js/.ts file, so the loaders are not applied inside html templates
      {
        test: /\.css$/i,
        issuer: { not: [ /\.html$/i ] },
        use: [ { loader: MiniCssExtractPlugin.loader }, ...cssRules ]
      },
      {
        test: /\.css$/i,
        issuer: /\.html$/i,
        // CSS required in templates cannot be extracted safely
        // because Aurelia would try to require it again in runtime
        use: cssRules
      },
      // Skip minimize in production build to avoid complain on unescaped < such as
      // <span>${ c < 5 ? c : 'many' }</span>
      { test: /\.html$/i, loader: 'html-loader', options: { minimize: false } },
      { test: /\.js$/i, loader: 'babel-loader', exclude: nodeModulesDir },
      // embed small images and fonts as Data Urls and larger ones as files:
      { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset' },
      { test: /\.(woff|woff2|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/i,  type: 'asset' },
      { test: /environment\.json$/i, use: [
        {loader: "app-settings-loader", options: {env: production ? 'production' : 'development' }},
      ]}
    ]
  },

Commenting out the following line in the webpack.config.js does change the “Hello World” text size but when it is not commented out the expected tailwind styles are not included.

require(“tailwindcss”)("./tailwind.config.js"),

Please can someone give me an idea as to where I am going wrong?

I just searched for Aurelia tailwind here on discourse and got this link:

Did you see that discussion?

Hi, Thanks for your reply. It was the link at the bottom of that post that I was following:

[https://medium.com/@Hamed__Fathi/how-to-configure-an-aurelia-cli-1-x-application-with-tailwind-css-and-purgecss-6e4864095edc](How to configure an Aurelia CLI 1.x application with Tailwind CSS and PurgeCSS? | by HamedFathi | Medium)

Since that was written I think webpack has changed. If I use the exact cssrules I get the following error:

ValidationError: Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema.

  • options has an unknown property ‘plugins’. These properties are valid:
    object { postcssOptions?, execute?, sourceMap?, implementation? }

so I have added the postcssOptions object to the options as per the webpack docs.

I have included my webpack config at the end og this post.

I also found this article but it is for aurelia 2: https://hamedfathi.me/aurelia-2-with-tailwindcss-and-webpack/

This is working if I update the webpack config to:

const postcssLoader = {
  loader: "postcss-loader",
  options: {
    postcssOptions: {
      plugins: [
        require("tailwindcss")("tailwind.config.js"),
        require("autoprefixer")
      ],
    },
  },
};

Interestingly this is using style-loader in the rule.

{ test: /.css$/i, use: [ ‘style-loader’, cssLoader, postcssLoader ] },

If I add ‘style-loader’ into my aurelia 1webpack config I get the error:

vendors-node_modules_aurelia-templating-binding_dist_native-modules_aurelia-templating-bindin-3cec1e.d5992cf9e7897e05a725.bundle.js:2617 Uncaught (in promise) Error: Failed loading required CSS file: styles.css
    at fixupCSSUrls (vendors-node_modules_aurelia-templating-binding_dist_native-modules_aurelia-templating-bindin-3cec1e.d5992cf9e7897e05a725.bundle.js:2617)
    at vendors-node_modules_aurelia-templating-binding_dist_native-modules_aurelia-templating-bindin-3cec1e.d5992cf9e7897e05a725.bundle.js:2651
    at async Promise.all (:8080/index 0)
    at async Promise.all (:8080/index 0)

Really appreciate any help with this!

This is my weback config:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');
const project = require('./aurelia_project/aurelia.json');
const { AureliaPlugin, ModuleDependenciesPlugin } = require('aurelia-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

// config helpers:
const ensureArray = (config) => config && (Array.isArray(config) ? config : [config]) || [];
const when = (condition, config, negativeConfig) =>
  condition ? ensureArray(config) : ensureArray(negativeConfig);

// primary config:
const outDir = path.resolve(__dirname, project.platform.output);
const srcDir = path.resolve(__dirname, 'src');
const nodeModulesDir = path.resolve(__dirname, 'node_modules');
const baseUrl = '';

const cssRules = [
  {
    loader: "css-loader",
  },
  {
    loader: "postcss-loader",
    options: {
      postcssOptions: {
        plugins: [
          require("tailwindcss")("tailwind.config.js"),
          "autoprefixer",
          "cssnano",
        ],
      },
    },
  },
];


module.exports = ({ production }, { analyze, hmr, port, host }) => ({
  resolve: {
    extensions: [".js"],
    modules: [srcDir, "node_modules"],

    alias: {
      // https://github.com/aurelia/dialog/issues/387
      // Uncomment next line if you had trouble to run aurelia-dialog on IE11
      // 'aurelia-dialog': path.resolve(__dirname, 'node_modules/aurelia-dialog/dist/umd/aurelia-dialog.js'),

      // https://github.com/aurelia/binding/issues/702
      // Enforce single aurelia-binding, to avoid v1/v2 duplication due to
      // out-of-date dependencies on 3rd party aurelia plugins
      "aurelia-binding": path.resolve(
        __dirname,
        "node_modules/aurelia-binding"
      ),
    },
  },
  entry: {
    app: [
      // Uncomment next line if you need to support IE11
      // 'promise-polyfill/src/polyfill',
      "aurelia-bootstrapper",
    ],
  },
  mode: production ? "production" : "development",
  output: {
    path: outDir,
    publicPath: baseUrl,
    filename: production
      ? "[name].[chunkhash].bundle.js"
      : "[name].[fullhash].bundle.js",
    chunkFilename: production
      ? "[name].[chunkhash].chunk.js"
      : "[name].[fullhash].chunk.js",
  },
  optimization: {
    runtimeChunk: true, // separates the runtime chunk, required for long term cacheability
    // moduleIds is the replacement for HashedModuleIdsPlugin and NamedModulesPlugin deprecated in https://github.com/webpack/webpack/releases/tag/v4.16.0
    // changes module id's to use hashes be based on the relative path of the module, required for long term cacheability
    moduleIds: "deterministic",
    // Use splitChunks to breakdown the App/Aurelia bundle down into smaller chunks
    // https://webpack.js.org/plugins/split-chunks-plugin/
    splitChunks: {
      hidePathInfo: true, // prevents the path from being used in the filename when using maxSize
      chunks: "initial",
      // sizes are compared against source before minification

      // This is the HTTP/1.1 optimized maxSize.
      maxSize: 200000, // splits chunks if bigger than 200k, adjust as required (maxSize added in webpack v4.15)
      /* This is the HTTP/2 optimized options.
      maxInitialRequests: Infinity, // Default is 3, make this unlimited if using HTTP/2
      maxAsyncRequests: Infinity, // Default is 5, make this unlimited if using HTTP/2
      minSize: 10000, // chunk is only created if it would be bigger than minSize, adjust as required
      maxSize: 40000, // splits chunks if bigger than 40k, adjust as required (maxSize added in webpack v4.15)
      */

      cacheGroups: {
        default: false, // Disable the built-in groups default & vendors (vendors is redefined below)
        // You can insert additional cacheGroup entries here if you want to split out specific modules
        // This is required in order to split out vendor css from the app css
        // For example to separate font-awesome and bootstrap:
        // fontawesome: { // separates font-awesome css from the app css (font-awesome is only css/fonts)
        //   name: 'vendor.font-awesome',
        //   test:  /[\\/]node_modules[\\/]font-awesome[\\/]/,
        //   priority: 100,
        //   enforce: true
        // },
        // bootstrap: { // separates bootstrap js from vendors and also bootstrap css from app css
        //   name: 'vendor.bootstrap',
        //   test:  /[\\/]node_modules[\\/]bootstrap[\\/]/,
        //   priority: 90,
        //   enforce: true
        // },

        // This is the HTTP/1.1 optimized cacheGroup configuration.
        vendors: {
          // picks up everything from node_modules as long as the sum of node modules is larger than minSize
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          priority: 19,
          enforce: true, // causes maxInitialRequests to be ignored, minSize still respected if specified in cacheGroup
          minSize: 30000, // use the default minSize
        },
        vendorsAsync: {
          // vendors async chunk, remaining asynchronously used node modules as single chunk file
          test: /[\\/]node_modules[\\/]/,
          name: "vendors.async",
          chunks: "async",
          priority: 9,
          reuseExistingChunk: true,
          minSize: 10000, // use smaller minSize to avoid too much potential bundle bloat due to module duplication.
        },
        commonsAsync: {
          // commons async chunk, remaining asynchronously used modules as single chunk file
          name: "commons.async",
          minChunks: 2, // Minimum number of chunks that must share a module before splitting
          chunks: "async",
          priority: 0,
          reuseExistingChunk: true,
          minSize: 10000, // use smaller minSize to avoid too much potential bundle bloat due to module duplication.
        },

        /* This is the HTTP/2 optimized cacheGroup configuration.
        // generic 'initial/sync' vendor node module splits: separates out larger modules
        vendorSplit: { // each node module as separate chunk file if module is bigger than minSize
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // Extract the name of the package from the path segment after node_modules
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
            return `vendor.${packageName.replace('@', '')}`;
          },
          priority: 20
        },
        vendors: { // picks up everything else being used from node_modules that is less than minSize
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          priority: 19,
          enforce: true // create chunk regardless of the size of the chunk
        },
        // generic 'async' vendor node module splits: separates out larger modules
        vendorAsyncSplit: { // vendor async chunks, create each asynchronously used node module as separate chunk file if module is bigger than minSize
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // Extract the name of the package from the path segment after node_modules
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
            return `vendor.async.${packageName.replace('@', '')}`;
          },
          chunks: 'async',
          priority: 10,
          reuseExistingChunk: true,
          minSize: 5000 // only create if 5k or larger
        },
        vendorsAsync: { // vendors async chunk, remaining asynchronously used node modules as single chunk file
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors.async',
          chunks: 'async',
          priority: 9,
          reuseExistingChunk: true,
          enforce: true // create chunk regardless of the size of the chunk
        },
        // generic 'async' common module splits: separates out larger modules
        commonAsync: { // common async chunks, each asynchronously used module a separate chunk file if module is bigger than minSize
          name(module) {
            // Extract the name of the module from last path component. 'src/modulename/' results in 'modulename'
            const moduleName = module.context.match(/[^\\/]+(?=\/$|$)/)[0];
            return `common.async.${moduleName.replace('@', '')}`;
          },
          minChunks: 2, // Minimum number of chunks that must share a module before splitting
          chunks: 'async',
          priority: 1,
          reuseExistingChunk: true,
          minSize: 5000 // only create if 5k or larger
        },
        commonsAsync: { // commons async chunk, remaining asynchronously used modules as single chunk file
          name: 'commons.async',
          minChunks: 2, // Minimum number of chunks that must share a module before splitting
          chunks: 'async',
          priority: 0,
          reuseExistingChunk: true,
          enforce: true // create chunk regardless of the size of the chunk
        }
        */
      },
    },
  },
  performance: { hints: false },
  devServer: {
    // serve index.html for all 404 (required for push-state)
    historyApiFallback: true,
    open: project.platform.open,
    hot: hmr || project.platform.hmr,
    port: port || project.platform.port,
    host: host,
  },
  devtool: production ? undefined : "cheap-module-source-map",
  module: {
    rules: [
      // CSS required in JS/TS files should use the style-loader that auto-injects it into the website
      // only when the issuer is a .js/.ts file, so the loaders are not applied inside html templates
      {
        test: /\.css$/i,
        issuer: { not: [ /\.html$/i ] },
        use: [ { loader: MiniCssExtractPlugin.loader }, ...cssRules ]
      },
      {
        test: /\.css$/i,
        issuer: /\.html$/i,
        // CSS required in templates cannot be extracted safely
        // because Aurelia would try to require it again in runtime
        use: cssRules
      },
      // Skip minimize in production build to avoid complain on unescaped < such as
      // <span>${ c < 5 ? c : 'many' }</span>
      { test: /\.html$/i, loader: "html-loader", options: { minimize: false } },
      { test: /\.js$/i, loader: "babel-loader", exclude: nodeModulesDir },
      // embed small images and fonts as Data Urls and larger ones as files:
      { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: "asset" },
      {
        test: /\.(woff|woff2|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/i,
        type: "asset",
      },
      {
        test: /environment\.json$/i,
        use: [
          {
            loader: "app-settings-loader",
            options: { env: production ? "production" : "development" },
          },
        ],
      },
    ],
  },
  plugins: [
    new DuplicatePackageCheckerPlugin(),
    new AureliaPlugin(),
    new ModuleDependenciesPlugin({
      "aurelia-testing": ["./compile-spy", "./view-spy"],
    }),
    new HtmlWebpackPlugin({
      template: "index.ejs",
      metadata: {
        // available in index.ejs //
        baseUrl,
      },
    }),
    // ref: https://webpack.js.org/plugins/mini-css-extract-plugin/
    new MiniCssExtractPlugin({
      // updated to match the naming conventions for the js files
      filename: production
        ? "[name].[contenthash].bundle.css"
        : "[name].[fullhash].bundle.css",
      chunkFilename: production
        ? "[name].[contenthash].chunk.css"
        : "[name].[fullhash].chunk.css",
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: "static", to: outDir, globOptions: { ignore: [".*"] } },
      ],
    }), // ignore dot (hidden) files
    ...when(analyze, new BundleAnalyzerPlugin()),
    /**
     * Note that the usage of following plugin cleans the webpack output directory before build.
     * In case you want to generate any file in the output path as a part of pre-build step, this plugin will likely
     * remove those before the webpack build. In that case consider disabling the plugin, and instead use something like
     * `del` (https://www.npmjs.com/package/del), or `rimraf` (https://www.npmjs.com/package/rimraf).
     */
    new CleanWebpackPlugin(),
  ],
});

@HamedFathi can you perhaps help out with this question?

2 Likes