Repository: maizzle/framework
Pull request #903 (2023-02-16 11:41:22)

Upgrade posthtml-safe-class-names: 2.0.0 → 3.0.0 (major)





Here is everything you need to know about this upgrade. Please take a good look at what changed and the test results before merging this pull request.

### What changed?

#### ✳️ posthtml-safe-class-names (2.0.0 → 3.0.0) · [Repo](https://github.com/posthtml/posthtml-safe-class-names)


<details>
<summary>Release Notes</summary>
<h4><a href="https://github.com/posthtml/posthtml-safe-class-names/releases/tag/v3.0.0">3.0.0</a></h4>

<blockquote><ul dir="auto">
<li>[BREAKING] plugin now requires Node 14 or newer  <a href="https://bounce.depfu.com/github.com/posthtml/posthtml-safe-class-names/commit/8ac08c1181a19403db679f7ccf58275923fcf4cf"><tt>8ac08c1</tt></a>
</li>
</ul>
<p dir="auto"><a href="https://bounce.depfu.com/github.com/posthtml/posthtml-safe-class-names/compare/v2.0.0...v3.0.0"><tt>v2.0.0...v3.0.0</tt></a></p></blockquote>
<p><em>Does any of this look wrong? <a href="https://depfu.com/packages/npm/posthtml-safe-class-names/feedback">Please let us know.</a></em></p>
</details>

<details>
<summary>Commits</summary>
<p><a href="https://github.com/posthtml/posthtml-safe-class-names/compare/a45838a4217e7880c36a178709d4115b983249fc...c01e190c768041e7e1f3cc634c518be439f6f771">See the full diff on Github</a>. The new version differs by 27 commits:</p>
<ul>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/c01e190c768041e7e1f3cc634c518be439f6f771"><code>3.0.0</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/8ac08c1181a19403db679f7ccf58275923fcf4cf"><code>build: update required node version</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/75ec4835788f0952e27231f9b055aac08262e02f"><code>chore: update readme.md</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/f89d58264265381339da9b185dda64491a1398b6"><code>chore: update readme.md</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/add8f945e141cff78116f7ce3c328589f0a2e567"><code>ci: update node versions</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/a8e6ce602fe884e90c3e51f00422fe30bf491651"><code>Merge pull request #95 from posthtml/dependabot/npm_and_yarn/ava-5.1.1</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/d5541764374c9d304d2b992f846f5864dd0f1234"><code>Merge pull request #96 from posthtml/dependabot/npm_and_yarn/http-cache-semantics-4.1.1</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/d19c7a2a58176bfaa299d92dea8d75ce2a743d71"><code>Merge pull request #94 from posthtml/dependabot/npm_and_yarn/postcss-8.4.21</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/073d63da057b9bdec8a091fba1cf0c7bb3c345ee"><code>Merge pull request #92 from posthtml/dependabot/npm_and_yarn/json5-and-json5-and-tsconfig-paths-2.2.3</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/021eb926cd7a1efc95f0177656572dc32fd7223e"><code>Merge pull request #90 from posthtml/dependabot/npm_and_yarn/np-7.6.3</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/693656a2178f8dd652457b77a6928b494ef25169"><code>build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/034441c83c96173cadb8657f90b4aeda18849990"><code>build(deps-dev): bump ava from 4.3.3 to 5.1.1</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/fac167c2622cbb01e319284b0273cb654b3484ff"><code>build(deps): bump postcss from 8.4.19 to 8.4.21</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/53be3d9f7f7df80a86be6406574354820ec5fdc6"><code>build(deps): bump json5 and tsconfig-paths</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/9ddd847d17a3e3c4ad795f5e31665cda50ec0c3c"><code>build(deps-dev): bump np from 7.6.2 to 7.6.3</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/1333494323cc848be6f509aa982388115dc04dc3"><code>Merge pull request #82 from posthtml/dependabot/npm_and_yarn/np-7.6.2</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/3d91dc7ad7488687a5be5229cf5c763863c3ae02"><code>build(deps-dev): bump np from 7.6.0 to 7.6.2</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/dca7ce267ea58175afaccb3e32b3b879e4c87284"><code>Merge pull request #89 from posthtml/dependabot/npm_and_yarn/minimatch-3.1.2</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/51fef996c29781432931db3864a4b69630054da5"><code>Merge pull request #88 from posthtml/dependabot/npm_and_yarn/decode-uri-component-0.2.2</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/973616fe1c96361743dfad3f9786468b2e5e71cf"><code>Merge pull request #85 from posthtml/dependabot/npm_and_yarn/postcss-selector-parser-6.0.11</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/d772f94dc71ad45c29360c82090c7456d86531ad"><code>build(deps): bump minimatch from 3.0.4 to 3.1.2</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/bc8b415587f25c0386f1823243966ed7e7509ab9"><code>build(deps): bump decode-uri-component from 0.2.0 to 0.2.2</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/1ebb078d05e8ff6f67d83656eafce3a5abb85b09"><code>Merge pull request #86 from posthtml/dependabot/npm_and_yarn/postcss-8.4.19</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/ecf9bbebc2b2308e336f3fbddd31bed5360281b0"><code>build(deps): bump postcss from 8.4.18 to 8.4.19</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/ca2323e4b12173ae39e64ac0f0159708bbfdc5bd"><code>build(deps): bump postcss-selector-parser from 6.0.10 to 6.0.11</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/a9a4932fc5e4f93e93337effb7456c70cf208968"><code>Merge pull request #84 from posthtml/dependabot/npm_and_yarn/posthtml-0.16.6</code></a></li>
<li><a href="https://github.com/posthtml/posthtml-safe-class-names/commit/98c450aad22dd60037890fb81f45cba6d66a000f"><code>build(deps): bump posthtml from 0.16.5 to 0.16.6</code></a></li>
</ul>
</details>






---
![Depfu Status](https://depfu.com/badges/a7db947cd7dd393f30af028d64118ed6/stats.svg)

[Depfu](https://depfu.com) will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with `@depfu rebase`.

<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@​depfu rebase</dt><dd>Rebases against your default branch and redoes this update</dd>
<dt>@​depfu recreate</dt><dd>Recreates this PR, overwriting any edits that you've made to it</dd>
<dt>@​depfu merge</dt><dd>Merges this PR once your tests are passing and conflicts are resolved</dd>
<dt>@​depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@​depfu reopen</dt><dd>Restores the branch and reopens this PR (if it's closed)</dd>
<dt>@​depfu pause</dt><dd>Ignores all future updates for this dependency and closes this PR</dd>
<dt>@​depfu pause [minor|major]</dt><dd>Ignores all future minor/major updates for this dependency and closes this PR</dd>
<dt>@​depfu resume</dt><dd>Future versions of this dependency will create PRs again (leaves this PR as is)</dd>
</dl></blockquote>
</details>




Patch:
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 91cacff..f4f06ae 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -1,7 +1,7 @@
 # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
 
-name: build
+name: Node.js CI
 
 on:
   push:
diff --git a/README.md b/README.md
index 8fc2dff..1b28097 100644
--- a/README.md
+++ b/README.md
@@ -34,9 +34,9 @@ The Maizzle framework is open-sourced software licensed under the [MIT license](
 
 [npm]: https://www.npmjs.com/package/@maizzle/framework
 [npm-stats]: https://npm-stat.com/charts.html?package=%40maizzle%2Fframework&from=2019-03-27
-[npm-version-shield]: https://img.shields.io/npm/v/@maizzle/framework.svg
-[npm-stats-shield]: https://img.shields.io/npm/dt/@maizzle/framework.svg?color=4f46e5
+[npm-version-shield]: https://img.shields.io/npm/v/@maizzle/framework.svg?style=flat-square
+[npm-stats-shield]: https://img.shields.io/npm/dt/@maizzle/framework.svg?style=flat-square&color=4f46e5
 [github-ci]: https://github.com/maizzle/framework/actions
-[github-ci-shield]: https://github.com/maizzle/framework/actions/workflows/nodejs.yml/badge.svg
+[github-ci-shield]: https://img.shields.io/github/workflow/status/maizzle/framework/Node.js%20CI?style=flat-square
 [license]: ./LICENSE
-[license-shield]: https://img.shields.io/npm/l/@maizzle/framework.svg?color=0e9f6e
+[license-shield]: https://img.shields.io/npm/l/@maizzle/framework.svg?style=flat-square&color=0e9f6e
diff --git a/package-lock.json b/package-lock.json
index 95b629a..2c4bfea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
 {
   "name": "@maizzle/framework",
-  "version": "4.4.0-beta.10",
+  "version": "4.3.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@maizzle/framework",
-      "version": "4.4.0-beta.10",
+      "version": "4.3.1",
       "license": "MIT",
       "dependencies": {
         "@maizzle/cli": "^1.5.1",
         "autoprefixer": "^10.4.13",
-        "browser-sync": "^2.27.11",
+        "browser-sync": "^2.26.13",
         "color-shorthand-hex-to-six-digit": "^3.0.2",
         "email-comb": "^5.2.0",
         "front-matter": "^4.0.0",
@@ -22,34 +22,35 @@
         "juice": "^8.0.0",
         "lodash": "^4.17.20",
         "ora": "^5.1.0",
-        "postcss": "^8.4.21",
+        "postcss": "^8.4.19",
         "postcss-import": "^15.0.0",
         "postcss-merge-longhand": "^5.1.7",
         "posthtml": "^0.16.6",
         "posthtml-attrs-parser": "^0.1.1",
         "posthtml-base-url": "^1.0.1",
-        "posthtml-component": "^1.0.0-beta.16",
         "posthtml-content": "^0.1.0",
+        "posthtml-expressions": "^1.8.1",
         "posthtml-extend": "^0.6.0",
         "posthtml-extra-attributes": "^1.0.0",
         "posthtml-fetch": "^2.2.0",
-        "posthtml-markdownit": "^1.3.1",
+        "posthtml-markdownit": "^1.3.0",
         "posthtml-match-helper": "^1.0.3",
+        "posthtml-modules": "^0.9.0",
         "posthtml-mso": "^1.0.4",
         "posthtml-postcss-merge-longhand": "^1.0.2",
-        "posthtml-safe-class-names": "^2.0.0",
+        "posthtml-safe-class-names": "^3.0.0",
         "posthtml-url-parameters": "^1.0.4",
         "pretty": "^2.0.0",
-        "query-string": "^7.1.3",
+        "query-string": "^7.1.0",
         "string-remove-widows": "^2.1.0",
         "string-strip-html": "^8.2.0",
-        "tailwindcss": "^3.2.6"
+        "tailwindcss": "^3.2.4"
       },
       "bin": {
         "maizzle": "bin/maizzle"
       },
       "devDependencies": {
-        "ava": "^5.2.0",
+        "ava": "^5.1.0",
         "c8": "^7.11.0",
         "np": "*",
         "xo": "0.39.1"
@@ -1776,9 +1777,9 @@
       }
     },
     "node_modules/ava": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/ava/-/ava-5.2.0.tgz",
-      "integrity": "sha512-W8yxFXJr/P68JP55eMpQIa6AiXhCX3VeuajM8nolyWNExcMDD6rnIWKTjw0B/+GkFHBIaN6Jd0LtcMThcoqVfg==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ava/-/ava-5.1.0.tgz",
+      "integrity": "sha512-e5VFrSQ0WBPyZJWRXVrO7RFOizFeNM0t2PORwrPvWtApgkORI6cvGnY3GX1G+lzpd0HjqNx5Jus22AhxVnUMNA==",
       "dev": true,
       "dependencies": {
         "acorn": "^8.8.1",
@@ -1788,10 +1789,10 @@
         "arrify": "^3.0.0",
         "callsites": "^4.0.0",
         "cbor": "^8.1.0",
-        "chalk": "^5.2.0",
+        "chalk": "^5.1.2",
         "chokidar": "^3.5.3",
         "chunkd": "^2.0.1",
-        "ci-info": "^3.7.1",
+        "ci-info": "^3.6.1",
         "ci-parallel-vars": "^1.0.1",
         "clean-yaml-object": "^0.1.0",
         "cli-truncate": "^3.1.0",
@@ -1803,7 +1804,7 @@
         "del": "^7.0.0",
         "emittery": "^1.0.1",
         "figures": "^5.0.0",
-        "globby": "^13.1.3",
+        "globby": "^13.1.2",
         "ignore-by-default": "^2.1.0",
         "indent-string": "^5.0.0",
         "is-error": "^2.2.2",
@@ -1843,9 +1844,9 @@
       }
     },
     "node_modules/ava/node_modules/chalk": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
-      "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz",
+      "integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==",
       "dev": true,
       "engines": {
         "node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -2125,12 +2126,12 @@
       }
     },
     "node_modules/browser-sync": {
-      "version": "2.27.11",
-      "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.11.tgz",
-      "integrity": "sha512-U5f9u97OYJH66T0MGWWzG9rOQTW6ZmDMj97vsmtqwNS03JAwdLVES8eel2lD3rvAqQCNAFqaJ74NMacBI57vJg==",
+      "version": "2.27.10",
+      "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
+      "integrity": "sha512-xKm+6KJmJu6RuMWWbFkKwOCSqQOxYe3nOrFkKI5Tr/ZzjPxyU3pFShKK3tWnazBo/3lYQzN7fzjixG8fwJh1Xw==",
       "dependencies": {
-        "browser-sync-client": "^2.27.11",
-        "browser-sync-ui": "^2.27.11",
+        "browser-sync-client": "^2.27.10",
+        "browser-sync-ui": "^2.27.10",
         "bs-recipes": "1.3.4",
         "bs-snippet-injector": "^2.0.1",
         "chokidar": "^3.5.1",
@@ -2148,7 +2149,7 @@
         "micromatch": "^4.0.2",
         "opn": "5.3.0",
         "portscanner": "2.2.0",
-        "qs": "^6.11.0",
+        "qs": "6.2.3",
         "raw-body": "^2.3.2",
         "resp-modifier": "6.0.2",
         "rx": "4.1.0",
@@ -2168,9 +2169,9 @@
       }
     },
     "node_modules/browser-sync-client": {
-      "version": "2.27.11",
-      "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.11.tgz",
-      "integrity": "sha512-okMNfD2NasL/XD1/BclP3onXjhahisk3e/kTQ5HPDT/lLqdBqNDd6QFcjI5I1ak7na2hxKQSLjryql+7fp5gKQ==",
+      "version": "2.27.10",
+      "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.10.tgz",
+      "integrity": "sha512-KCFKA1YDj6cNul0VsA28apohtBsdk5Wv8T82ClOZPZMZWxPj4Ny5AUbrj9UlAb/k6pdxE5HABrWDhP9+cjt4HQ==",
       "dependencies": {
         "etag": "1.8.1",
         "fresh": "0.5.2",
@@ -2183,9 +2184,9 @@
       }
     },
     "node_modules/browser-sync-ui": {
-      "version": "2.27.11",
-      "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.11.tgz",
-      "integrity": "sha512-1T/Y8Pp1R68aUL7zVSFq0nxtr258xWd/nTasCAHX2M6EsGaswVOFtXsw3bKqsr35z+J+LfVfOdz1HFLYKxdgrA==",
+      "version": "2.27.10",
+      "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.10.tgz",
+      "integrity": "sha512-elbJILq4Uo6OQv6gsvS3Y9vRAJlWu+h8j0JDkF0X/ua+3S6SVbbiWnZc8sNOFlG7yvVGIwBED3eaYQ0iBo1Dtw==",
       "dependencies": {
         "async-each-series": "0.1.1",
         "connect-history-api-fallback": "^1",
@@ -2482,6 +2483,7 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
       "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "dev": true,
       "dependencies": {
         "function-bind": "^1.1.1",
         "get-intrinsic": "^1.0.2"
@@ -2690,16 +2692,10 @@
       "dev": true
     },
     "node_modules/ci-info": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz",
-      "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==",
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz",
+      "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==",
       "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/sibiraj-s"
-        }
-      ],
       "engines": {
         "node": ">=8"
       }
@@ -3450,9 +3446,9 @@
       }
     },
     "node_modules/decode-uri-component": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
-      "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
       "engines": {
         "node": ">=0.10"
       }
@@ -3969,9 +3965,9 @@
       }
     },
     "node_modules/engine.io-client": {
-      "version": "6.2.3",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.3.tgz",
-      "integrity": "sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==",
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
+      "integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1",
@@ -5664,6 +5660,7 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
       "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+      "dev": true,
       "dependencies": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
@@ -5832,9 +5829,9 @@
       }
     },
     "node_modules/globby": {
-      "version": "13.1.3",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz",
-      "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==",
+      "version": "13.1.2",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz",
+      "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==",
       "dev": true,
       "dependencies": {
         "dir-glob": "^3.0.1",
@@ -6016,6 +6013,7 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -6169,9 +6167,9 @@
       }
     },
     "node_modules/http-cache-semantics": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
-      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
     },
     "node_modules/http-errors": {
       "version": "2.0.0",
@@ -6489,11 +6487,6 @@
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
       "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="
     },
-    "node_modules/inline-style-parser": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
-      "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
-    },
     "node_modules/inquirer": {
       "version": "8.2.4",
       "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz",
@@ -7694,9 +7687,9 @@
       "dev": true
     },
     "node_modules/json5": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+      "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
       "dev": true,
       "bin": {
         "json5": "lib/cli.js"
@@ -9248,9 +9241,9 @@
       }
     },
     "node_modules/np": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/np/-/np-7.6.3.tgz",
-      "integrity": "sha512-GTFNvIhu/cZqzUhil/AlISCiipYeAUVx0JtyjtmumFJvWogSewUSAshFi5MSMc6BOj9C98s8NAFZiqlHb2wQPQ==",
+      "version": "7.6.2",
+      "resolved": "https://registry.npmjs.org/np/-/np-7.6.2.tgz",
+      "integrity": "sha512-gExmKGKixh7ITc4Q+Lv7nfCby0CVKvzri9zN8970oKD8976T4L5dw8QWUtMcXcIjhFF6h5lbvztao/NurDbmxQ==",
       "dependencies": {
         "@samverschueren/stream-to-observable": "^0.3.1",
         "any-observable": "^0.5.1",
@@ -9650,6 +9643,7 @@
       "version": "1.12.2",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
       "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+      "dev": true,
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -10624,9 +10618,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.21",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
-      "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
+      "version": "8.4.19",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
+      "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
       "funding": [
         {
           "type": "opencollective",
@@ -10757,9 +10751,9 @@
       }
     },
     "node_modules/postcss-selector-parser": {
-      "version": "6.0.11",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
-      "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
+      "version": "6.0.10",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+      "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -10807,22 +10801,6 @@
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
       }
     },
-    "node_modules/posthtml-component": {
-      "version": "1.0.0-beta.16",
-      "resolved": "https://registry.npmjs.org/posthtml-component/-/posthtml-component-1.0.0-beta.16.tgz",
-      "integrity": "sha512-I/4uxR0mRf5HfkBv9vr6P7kWCMPyt3r0wpXK4G6eTn8H9eUbMh2rxnK5f8EWldpUZ3FYP0qO8HyP5Dxbe+WijQ==",
-      "dependencies": {
-        "lodash": "^4.17.21",
-        "posthtml-attrs-parser": "^0.1.1",
-        "posthtml-expressions": "^1.9.0",
-        "posthtml-parser": "^0.11.0",
-        "posthtml-render": "^3.0.0",
-        "style-to-object": "^0.3.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/posthtml-content": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/posthtml-content/-/posthtml-content-0.1.0.tgz",
@@ -10998,9 +10976,9 @@
       }
     },
     "node_modules/posthtml-markdownit": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/posthtml-markdownit/-/posthtml-markdownit-1.3.1.tgz",
-      "integrity": "sha512-ohva3TR6zD+k+yRirsvC5tYvSNs6FyZ4rD5WdS2rXA7ZP8z5XXispGJPuGKFt4L7pS4eBmB5oROoggG/pwatuA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/posthtml-markdownit/-/posthtml-markdownit-1.3.0.tgz",
+      "integrity": "sha512-/8X/5ZhOpjdfu+uArun3qmF1s9YEX3XEv9zkMtwXv9n6ZqQYqLuQ/w9doIQhAwmCf6lcBtjw9GKLj1OpYw+SAw==",
       "dependencies": {
         "markdown-it": "^12.0.2",
         "min-indent": "^1.0.0",
@@ -11094,6 +11072,21 @@
         "posthtml": ">=0.5.0"
       }
     },
+    "node_modules/posthtml-modules": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/posthtml-modules/-/posthtml-modules-0.9.0.tgz",
+      "integrity": "sha512-ER3OvbQ8qwzPUfTKDGKGUF9UXPyTO+z+4iC7FpKYjidh3oY+rTD71zFyQ7gRhtzBqojzK7OLK3+wPHuLXFE5Og==",
+      "dependencies": {
+        "is-json": "^2.0.1",
+        "lodash": "^4.17.21",
+        "posthtml-expressions": "^1.9.0",
+        "posthtml-match-helper": "^1.0.3",
+        "posthtml-render": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/posthtml-mso": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/posthtml-mso/-/posthtml-mso-1.0.4.tgz",
@@ -11325,9 +11318,9 @@
       }
     },
     "node_modules/posthtml-safe-class-names": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/posthtml-safe-class-names/-/posthtml-safe-class-names-2.0.0.tgz",
-      "integrity": "sha512-wyqfteOT1zgP4y4gqFhgpM5kfD2itaz1oE0sOMrQzXipn5kObpJ6OqASndRFqAfTjm7suoGX8H6doPYbya6gdg==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/posthtml-safe-class-names/-/posthtml-safe-class-names-3.0.0.tgz",
+      "integrity": "sha512-3UepR+RKVp6ZDYmWp/XcdTxDjcdY5DNFQoTvprBYOVLKCUbPZNSIGvaYlPm1TnRvnxb9PVRIhYi6FF0DOtE+JA==",
       "dependencies": {
         "css.escape": "^1.5.1",
         "postcss": "^8.1.10",
@@ -11336,7 +11329,7 @@
         "posthtml": "^0.16.4"
       },
       "engines": {
-        "node": ">=12.22.0"
+        "node": ">=14.0.0"
       }
     },
     "node_modules/posthtml-url-parameters": {
@@ -11491,25 +11484,19 @@
       }
     },
     "node_modules/qs": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
-      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
-      "dependencies": {
-        "side-channel": "^1.0.4"
-      },
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
+      "integrity": "sha512-AY4g8t3LMboim0t6XWFdz6J5OuJ1ZNYu54SXihS/OMpgyCqYmcAJnWqkNSOjSjWmq3xxy+GF9uWQI2lI/7tKIA==",
       "engines": {
         "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/query-string": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
-      "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
+      "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
       "dependencies": {
-        "decode-uri-component": "^0.2.2",
+        "decode-uri-component": "^0.2.0",
         "filter-obj": "^1.1.0",
         "split-on-first": "^1.0.0",
         "strict-uri-encode": "^2.0.0"
@@ -12525,6 +12512,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
       "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "dev": true,
       "dependencies": {
         "call-bind": "^1.0.0",
         "get-intrinsic": "^1.0.2",
@@ -12774,14 +12762,14 @@
       "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
     },
     "node_modules/socket.io-client": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz",
-      "integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.2.tgz",
+      "integrity": "sha512-naqYfFu7CLDiQ1B7AlLhRXKX3gdeaIMfgigwavDzgJoIUYulc1qHH5+2XflTsXTPY7BlPH5rppJyUjhjrKQKLg==",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.2",
-        "engine.io-client": "~6.2.3",
-        "socket.io-parser": "~4.2.1"
+        "engine.io-client": "~6.2.1",
+        "socket.io-parser": "~4.2.0"
       },
       "engines": {
         "node": ">=10.0.0"
@@ -13313,14 +13301,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/style-to-object": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
-      "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
-      "dependencies": {
-        "inline-style-parser": "0.1.1"
-      }
-    },
     "node_modules/stylehacks": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -13526,9 +13506,9 @@
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.6.tgz",
-      "integrity": "sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==",
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
+      "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==",
       "dependencies": {
         "arg": "^5.0.2",
         "chokidar": "^3.5.3",
@@ -13544,12 +13524,12 @@
         "normalize-path": "^3.0.0",
         "object-hash": "^3.0.0",
         "picocolors": "^1.0.0",
-        "postcss": "^8.0.9",
+        "postcss": "^8.4.18",
         "postcss-import": "^14.1.0",
         "postcss-js": "^4.0.0",
         "postcss-load-config": "^3.1.4",
         "postcss-nested": "6.0.0",
-        "postcss-selector-parser": "^6.0.11",
+        "postcss-selector-parser": "^6.0.10",
         "postcss-value-parser": "^4.2.0",
         "quick-lru": "^5.1.1",
         "resolve": "^1.22.1"
@@ -13979,9 +13959,9 @@
       }
     },
     "node_modules/tsconfig-paths/node_modules/json5": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
-      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+      "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
       "dev": true,
       "dependencies": {
         "minimist": "^1.2.0"
@@ -16645,9 +16625,9 @@
       }
     },
     "ava": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/ava/-/ava-5.2.0.tgz",
-      "integrity": "sha512-W8yxFXJr/P68JP55eMpQIa6AiXhCX3VeuajM8nolyWNExcMDD6rnIWKTjw0B/+GkFHBIaN6Jd0LtcMThcoqVfg==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ava/-/ava-5.1.0.tgz",
+      "integrity": "sha512-e5VFrSQ0WBPyZJWRXVrO7RFOizFeNM0t2PORwrPvWtApgkORI6cvGnY3GX1G+lzpd0HjqNx5Jus22AhxVnUMNA==",
       "dev": true,
       "requires": {
         "acorn": "^8.8.1",
@@ -16657,10 +16637,10 @@
         "arrify": "^3.0.0",
         "callsites": "^4.0.0",
         "cbor": "^8.1.0",
-        "chalk": "^5.2.0",
+        "chalk": "^5.1.2",
         "chokidar": "^3.5.3",
         "chunkd": "^2.0.1",
-        "ci-info": "^3.7.1",
+        "ci-info": "^3.6.1",
         "ci-parallel-vars": "^1.0.1",
         "clean-yaml-object": "^0.1.0",
         "cli-truncate": "^3.1.0",
@@ -16672,7 +16652,7 @@
         "del": "^7.0.0",
         "emittery": "^1.0.1",
         "figures": "^5.0.0",
-        "globby": "^13.1.3",
+        "globby": "^13.1.2",
         "ignore-by-default": "^2.1.0",
         "indent-string": "^5.0.0",
         "is-error": "^2.2.2",
@@ -16698,9 +16678,9 @@
       },
       "dependencies": {
         "chalk": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
-          "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz",
+          "integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==",
           "dev": true
         },
         "del": {
@@ -16899,12 +16879,12 @@
       }
     },
     "browser-sync": {
-      "version": "2.27.11",
-      "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.11.tgz",
-      "integrity": "sha512-U5f9u97OYJH66T0MGWWzG9rOQTW6ZmDMj97vsmtqwNS03JAwdLVES8eel2lD3rvAqQCNAFqaJ74NMacBI57vJg==",
+      "version": "2.27.10",
+      "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
+      "integrity": "sha512-xKm+6KJmJu6RuMWWbFkKwOCSqQOxYe3nOrFkKI5Tr/ZzjPxyU3pFShKK3tWnazBo/3lYQzN7fzjixG8fwJh1Xw==",
       "requires": {
-        "browser-sync-client": "^2.27.11",
-        "browser-sync-ui": "^2.27.11",
+        "browser-sync-client": "^2.27.10",
+        "browser-sync-ui": "^2.27.10",
         "bs-recipes": "1.3.4",
         "bs-snippet-injector": "^2.0.1",
         "chokidar": "^3.5.1",
@@ -16922,7 +16902,7 @@
         "micromatch": "^4.0.2",
         "opn": "5.3.0",
         "portscanner": "2.2.0",
-        "qs": "^6.11.0",
+        "qs": "6.2.3",
         "raw-body": "^2.3.2",
         "resp-modifier": "6.0.2",
         "rx": "4.1.0",
@@ -16961,9 +16941,9 @@
       }
     },
     "browser-sync-client": {
-      "version": "2.27.11",
-      "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.11.tgz",
-      "integrity": "sha512-okMNfD2NasL/XD1/BclP3onXjhahisk3e/kTQ5HPDT/lLqdBqNDd6QFcjI5I1ak7na2hxKQSLjryql+7fp5gKQ==",
+      "version": "2.27.10",
+      "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.10.tgz",
+      "integrity": "sha512-KCFKA1YDj6cNul0VsA28apohtBsdk5Wv8T82ClOZPZMZWxPj4Ny5AUbrj9UlAb/k6pdxE5HABrWDhP9+cjt4HQ==",
       "requires": {
         "etag": "1.8.1",
         "fresh": "0.5.2",
@@ -16973,9 +16953,9 @@
       }
     },
     "browser-sync-ui": {
-      "version": "2.27.11",
-      "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.11.tgz",
-      "integrity": "sha512-1T/Y8Pp1R68aUL7zVSFq0nxtr258xWd/nTasCAHX2M6EsGaswVOFtXsw3bKqsr35z+J+LfVfOdz1HFLYKxdgrA==",
+      "version": "2.27.10",
+      "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.10.tgz",
+      "integrity": "sha512-elbJILq4Uo6OQv6gsvS3Y9vRAJlWu+h8j0JDkF0X/ua+3S6SVbbiWnZc8sNOFlG7yvVGIwBED3eaYQ0iBo1Dtw==",
       "requires": {
         "async-each-series": "0.1.1",
         "connect-history-api-fallback": "^1",
@@ -17178,6 +17158,7 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
       "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "dev": true,
       "requires": {
         "function-bind": "^1.1.1",
         "get-intrinsic": "^1.0.2"
@@ -17316,9 +17297,9 @@
       "dev": true
     },
     "ci-info": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz",
-      "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==",
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz",
+      "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==",
       "dev": true
     },
     "ci-parallel-vars": {
@@ -17908,9 +17889,9 @@
       }
     },
     "decode-uri-component": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
-      "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
     },
     "decompress-response": {
       "version": "5.0.0",
@@ -18292,9 +18273,9 @@
       }
     },
     "engine.io-client": {
-      "version": "6.2.3",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.3.tgz",
-      "integrity": "sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==",
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
+      "integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
       "requires": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1",
@@ -19556,6 +19537,7 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
       "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+      "dev": true,
       "requires": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
@@ -19662,9 +19644,9 @@
       }
     },
     "globby": {
-      "version": "13.1.3",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz",
-      "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==",
+      "version": "13.1.2",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz",
+      "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==",
       "dev": true,
       "requires": {
         "dir-glob": "^3.0.1",
@@ -19791,7 +19773,8 @@
     "has-symbols": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+      "dev": true
     },
     "has-tostringtag": {
       "version": "1.0.0",
@@ -19909,9 +19892,9 @@
       }
     },
     "http-cache-semantics": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
-      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
     },
     "http-errors": {
       "version": "2.0.0",
@@ -20130,11 +20113,6 @@
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
       "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="
     },
-    "inline-style-parser": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
-      "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
-    },
     "inquirer": {
       "version": "8.2.4",
       "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz",
@@ -21020,9 +20998,9 @@
       "dev": true
     },
     "json5": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+      "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
       "dev": true
     },
     "jsonfile": {
@@ -22185,9 +22163,9 @@
       "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
     },
     "np": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/np/-/np-7.6.3.tgz",
-      "integrity": "sha512-GTFNvIhu/cZqzUhil/AlISCiipYeAUVx0JtyjtmumFJvWogSewUSAshFi5MSMc6BOj9C98s8NAFZiqlHb2wQPQ==",
+      "version": "7.6.2",
+      "resolved": "https://registry.npmjs.org/np/-/np-7.6.2.tgz",
+      "integrity": "sha512-gExmKGKixh7ITc4Q+Lv7nfCby0CVKvzri9zN8970oKD8976T4L5dw8QWUtMcXcIjhFF6h5lbvztao/NurDbmxQ==",
       "requires": {
         "@samverschueren/stream-to-observable": "^0.3.1",
         "any-observable": "^0.5.1",
@@ -22488,7 +22466,8 @@
     "object-inspect": {
       "version": "1.12.2",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
-      "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ=="
+      "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+      "dev": true
     },
     "object-keys": {
       "version": "1.1.1",
@@ -23164,9 +23143,9 @@
       "dev": true
     },
     "postcss": {
-      "version": "8.4.21",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
-      "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
+      "version": "8.4.19",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
+      "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
       "requires": {
         "nanoid": "^3.3.4",
         "picocolors": "^1.0.0",
@@ -23224,9 +23203,9 @@
       "requires": {}
     },
     "postcss-selector-parser": {
-      "version": "6.0.11",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
-      "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
+      "version": "6.0.10",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+      "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
       "requires": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -23265,19 +23244,6 @@
         "srcset": "^4.0.0"
       }
     },
-    "posthtml-component": {
-      "version": "1.0.0-beta.16",
-      "resolved": "https://registry.npmjs.org/posthtml-component/-/posthtml-component-1.0.0-beta.16.tgz",
-      "integrity": "sha512-I/4uxR0mRf5HfkBv9vr6P7kWCMPyt3r0wpXK4G6eTn8H9eUbMh2rxnK5f8EWldpUZ3FYP0qO8HyP5Dxbe+WijQ==",
-      "requires": {
-        "lodash": "^4.17.21",
-        "posthtml-attrs-parser": "^0.1.1",
-        "posthtml-expressions": "^1.9.0",
-        "posthtml-parser": "^0.11.0",
-        "posthtml-render": "^3.0.0",
-        "style-to-object": "^0.3.0"
-      }
-    },
     "posthtml-content": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/posthtml-content/-/posthtml-content-0.1.0.tgz",
@@ -23399,9 +23365,9 @@
       }
     },
     "posthtml-markdownit": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/posthtml-markdownit/-/posthtml-markdownit-1.3.1.tgz",
-      "integrity": "sha512-ohva3TR6zD+k+yRirsvC5tYvSNs6FyZ4rD5WdS2rXA7ZP8z5XXispGJPuGKFt4L7pS4eBmB5oROoggG/pwatuA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/posthtml-markdownit/-/posthtml-markdownit-1.3.0.tgz",
+      "integrity": "sha512-/8X/5ZhOpjdfu+uArun3qmF1s9YEX3XEv9zkMtwXv9n6ZqQYqLuQ/w9doIQhAwmCf6lcBtjw9GKLj1OpYw+SAw==",
       "requires": {
         "markdown-it": "^12.0.2",
         "min-indent": "^1.0.0",
@@ -23471,6 +23437,18 @@
       "integrity": "sha512-aeRAPvok2Fs6uzSm85665jdAk5UOd8US2QCkWtGU6yLPlKSwzWTSgZZuABc3UeNy3K1lVk/HV9bRkWJYN05Ymw==",
       "requires": {}
     },
+    "posthtml-modules": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/posthtml-modules/-/posthtml-modules-0.9.0.tgz",
+      "integrity": "sha512-ER3OvbQ8qwzPUfTKDGKGUF9UXPyTO+z+4iC7FpKYjidh3oY+rTD71zFyQ7gRhtzBqojzK7OLK3+wPHuLXFE5Og==",
+      "requires": {
+        "is-json": "^2.0.1",
+        "lodash": "^4.17.21",
+        "posthtml-expressions": "^1.9.0",
+        "posthtml-match-helper": "^1.0.3",
+        "posthtml-render": "^3.0.0"
+      }
+    },
     "posthtml-mso": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/posthtml-mso/-/posthtml-mso-1.0.4.tgz",
@@ -23643,9 +23621,9 @@
       }
     },
     "posthtml-safe-class-names": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/posthtml-safe-class-names/-/posthtml-safe-class-names-2.0.0.tgz",
-      "integrity": "sha512-wyqfteOT1zgP4y4gqFhgpM5kfD2itaz1oE0sOMrQzXipn5kObpJ6OqASndRFqAfTjm7suoGX8H6doPYbya6gdg==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/posthtml-safe-class-names/-/posthtml-safe-class-names-3.0.0.tgz",
+      "integrity": "sha512-3UepR+RKVp6ZDYmWp/XcdTxDjcdY5DNFQoTvprBYOVLKCUbPZNSIGvaYlPm1TnRvnxb9PVRIhYi6FF0DOtE+JA==",
       "requires": {
         "css.escape": "^1.5.1",
         "postcss": "^8.1.10",
@@ -23763,19 +23741,16 @@
       }
     },
     "qs": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
-      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
-      "requires": {
-        "side-channel": "^1.0.4"
-      }
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
+      "integrity": "sha512-AY4g8t3LMboim0t6XWFdz6J5OuJ1ZNYu54SXihS/OMpgyCqYmcAJnWqkNSOjSjWmq3xxy+GF9uWQI2lI/7tKIA=="
     },
     "query-string": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
-      "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
+      "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
       "requires": {
-        "decode-uri-component": "^0.2.2",
+        "decode-uri-component": "^0.2.0",
         "filter-obj": "^1.1.0",
         "split-on-first": "^1.0.0",
         "strict-uri-encode": "^2.0.0"
@@ -24543,6 +24518,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
       "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "dev": true,
       "requires": {
         "call-bind": "^1.0.0",
         "get-intrinsic": "^1.0.2",
@@ -24745,14 +24721,14 @@
       "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
     },
     "socket.io-client": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz",
-      "integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.2.tgz",
+      "integrity": "sha512-naqYfFu7CLDiQ1B7AlLhRXKX3gdeaIMfgigwavDzgJoIUYulc1qHH5+2XflTsXTPY7BlPH5rppJyUjhjrKQKLg==",
       "requires": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.2",
-        "engine.io-client": "~6.2.3",
-        "socket.io-parser": "~4.2.1"
+        "engine.io-client": "~6.2.1",
+        "socket.io-parser": "~4.2.0"
       }
     },
     "socket.io-parser": {
@@ -25186,14 +25162,6 @@
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
       "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="
     },
-    "style-to-object": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
-      "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
-      "requires": {
-        "inline-style-parser": "0.1.1"
-      }
-    },
     "stylehacks": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -25343,9 +25311,9 @@
       }
     },
     "tailwindcss": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.6.tgz",
-      "integrity": "sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==",
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
+      "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==",
       "requires": {
         "arg": "^5.0.2",
         "chokidar": "^3.5.3",
@@ -25361,12 +25329,12 @@
         "normalize-path": "^3.0.0",
         "object-hash": "^3.0.0",
         "picocolors": "^1.0.0",
-        "postcss": "^8.0.9",
+        "postcss": "^8.4.18",
         "postcss-import": "^14.1.0",
         "postcss-js": "^4.0.0",
         "postcss-load-config": "^3.1.4",
         "postcss-nested": "6.0.0",
-        "postcss-selector-parser": "^6.0.11",
+        "postcss-selector-parser": "^6.0.10",
         "postcss-value-parser": "^4.2.0",
         "quick-lru": "^5.1.1",
         "resolve": "^1.22.1"
@@ -25679,9 +25647,9 @@
       },
       "dependencies": {
         "json5": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
-          "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+          "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
           "dev": true,
           "requires": {
             "minimist": "^1.2.0"
diff --git a/package.json b/package.json
index 058b225..28b9aaa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@maizzle/framework",
-  "version": "4.4.0-beta.10",
+  "version": "4.3.1",
   "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
   "license": "MIT",
   "main": "src/index.js",
@@ -42,7 +42,7 @@
   "dependencies": {
     "@maizzle/cli": "^1.5.1",
     "autoprefixer": "^10.4.13",
-    "browser-sync": "^2.27.11",
+    "browser-sync": "^2.26.13",
     "color-shorthand-hex-to-six-digit": "^3.0.2",
     "email-comb": "^5.2.0",
     "front-matter": "^4.0.0",
@@ -53,31 +53,32 @@
     "juice": "^8.0.0",
     "lodash": "^4.17.20",
     "ora": "^5.1.0",
-    "postcss": "^8.4.21",
+    "postcss": "^8.4.19",
     "postcss-import": "^15.0.0",
     "postcss-merge-longhand": "^5.1.7",
     "posthtml": "^0.16.6",
     "posthtml-attrs-parser": "^0.1.1",
     "posthtml-base-url": "^1.0.1",
-    "posthtml-component": "^1.0.0-beta.16",
     "posthtml-content": "^0.1.0",
+    "posthtml-expressions": "^1.8.1",
     "posthtml-extend": "^0.6.0",
     "posthtml-extra-attributes": "^1.0.0",
     "posthtml-fetch": "^2.2.0",
-    "posthtml-markdownit": "^1.3.1",
+    "posthtml-markdownit": "^1.3.0",
     "posthtml-match-helper": "^1.0.3",
+    "posthtml-modules": "^0.9.0",
     "posthtml-mso": "^1.0.4",
     "posthtml-postcss-merge-longhand": "^1.0.2",
-    "posthtml-safe-class-names": "^2.0.0",
+    "posthtml-safe-class-names": "^3.0.0",
     "posthtml-url-parameters": "^1.0.4",
     "pretty": "^2.0.0",
-    "query-string": "^7.1.3",
+    "query-string": "^7.1.0",
     "string-remove-widows": "^2.1.0",
     "string-strip-html": "^8.2.0",
-    "tailwindcss": "^3.2.6"
+    "tailwindcss": "^3.2.4"
   },
   "devDependencies": {
-    "ava": "^5.2.0",
+    "ava": "^5.1.0",
     "c8": "^7.11.0",
     "np": "*",
     "xo": "0.39.1"
diff --git a/src/commands/serve.js b/src/commands/serve.js
index c869781..2f5da18 100644
--- a/src/commands/serve.js
+++ b/src/commands/serve.js
@@ -9,10 +9,6 @@ const renderToString = require('../functions/render')
 const {get, merge, isObject} = require('lodash')
 const {clearConsole} = require('../utils/helpers')
 
-/**
- * Initialize Browsersync on-demand
- * https://github.com/maizzle/framework/issues/605
- */
 const browsersync = () => {
   if (!global.cachedBrowserSync) {
     const bs = require('browser-sync')
@@ -22,168 +18,142 @@ const browsersync = () => {
   return global.cachedBrowserSync
 }
 
-const getConfig = async (env = 'local', config = {}) => merge(
-  config,
-  await Config.getMerged(env)
-)
-
 const serve = async (env = 'local', config = {}) => {
-  config = await getConfig(env, merge(config, {
-    build: {
-      command: 'serve'
+  config = merge(
+    config,
+    await Config.getMerged(env),
+    {
+      build: {
+        command: 'serve'
+      }
     }
-  }))
+  )
 
   const spinner = ora()
 
-  // Build all emails first
-  await buildToFile(env, config)
+  try {
+    await buildToFile(env, config)
 
-  // Set some paths to watch
-  let templates = get(config, 'build.templates')
-  templates = Array.isArray(templates) ? templates : [templates]
+    let templates = get(config, 'build.templates')
+    templates = Array.isArray(templates) ? templates : [templates]
 
-  const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
-  const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
-  const globalPaths = [
-    'src/**',
-    ...new Set(get(config, 'build.browsersync.watch', []))
-  ]
+    const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
+    const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
+    const globalPaths = [
+      'src/**',
+      ...new Set(get(config, 'build.browsersync.watch', []))
+    ]
+    if (typeof tailwindConfig === 'string') {
+      globalPaths.push(tailwindConfig);
+    }
 
-  if (typeof tailwindConfig === 'string') {
-    globalPaths.push(tailwindConfig)
-  }
+    // Watch for Template file changes
+    browsersync()
+      .watch(templatePaths)
+      .on('change', async file => {
+        if (config.events && typeof config.events.beforeCreate === 'function') {
+          await config.events.beforeCreate(config)
+        }
 
-  // Watch for Template file changes
-  browsersync()
-    .watch(templatePaths)
-    .on('change', async file => {
-      config = await getConfig(env, config)
+        // Don't render if file type is not configured
+        // eslint-disable-next-line
+        const filetypes = templates.reduce((acc, template) => {
+          return [...acc, ...get(template, 'filetypes', ['html'])]
+        }, [])
 
-      if (config.events && typeof config.events.beforeCreate === 'function') {
-        await config.events.beforeCreate(config)
-      }
+        if (!filetypes.includes(path.extname(file).slice(1))) {
+          return
+        }
 
-      // Don't render if file type is not configured
-      // eslint-disable-next-line
-      const filetypes = templates.reduce((acc, template) => {
-        return [...acc, ...get(template, 'filetypes', ['html'])]
-      }, [])
+        if (get(config, 'build.console.clear')) {
+          clearConsole()
+        }
 
-      if (!filetypes.includes(path.extname(file).slice(1))) {
-        return
-      }
+        const start = new Date()
 
-      // Clear console if enabled
-      if (get(config, 'build.console.clear')) {
-        clearConsole()
-      }
+        spinner.start('Building email...')
 
-      // Start the spinner
-      const start = new Date()
-      spinner.start('Building email...')
-
-      // Render the template
-      renderToString(
-        await fs.readFile(file.replace(/\\/g, '/'), 'utf8'),
-        {
-          maizzle: merge(
-            config,
-            {
-              build: {
-                current: {
-                  path: path.parse(file)
-                }
-              }
-            }
-          ),
+        file = file.replace(/\\/g, '/')
+
+        const renderOptions = {
+          maizzle: config,
           ...config.events
         }
-      )
-        .then(async ({html, config}) => {
-          // Write the file to disk
-          let source = ''
-          let dest = ''
-          let ext = ''
-
-          if (Array.isArray(config.build.templates)) {
-            const match = config.build.templates.find(template => template.source === path.parse(file).dir)
-            source = path.normalize(get(match, 'source'))
-            dest = path.normalize(get(match, 'destination.path', 'build_local'))
-            ext = get(match, 'destination.ext', 'html')
-          } else if (isObject(config.build.templates)) {
-            source = path.normalize(get(config, 'build.templates.source'))
-            dest = path.normalize(get(config, 'build.templates.destination.path', 'build_local'))
-            ext = get(config, 'build.templates.destination.ext', 'html')
-          }
-
-          const fileDir = path.parse(file).dir.replace(source, '')
-          const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
-
-          await fs.outputFile(config.permalink || finalDestination, html)
-        })
-        .then(() => {
-          browsersync().reload()
-          spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
-        })
-        .catch(error => {
-          throw error
-        })
-    })
-
-  // Watch for changes in all other files
-  browsersync()
-    .watch(globalPaths, {ignored: templatePaths})
-    .on('change', () => buildToFile(env, config)
-      .then(() => browsersync().reload())
-      .catch(error => {
-        throw error
+
+        renderToString(
+          await fs.readFile(file, 'utf8'),
+          renderOptions
+        )
+          .then(async ({html, config}) => {
+            let source = ''
+            let dest = ''
+            let ext = ''
+
+            if (Array.isArray(config.build.templates)) {
+              const match = config.build.templates.find(template => template.source === path.parse(file).dir)
+              source = get(match, 'source')
+              dest = get(match, 'destination.path', 'build_local')
+              ext = get(match, 'destination.ext', 'html')
+            } else if (isObject(config.build.templates)) {
+              source = get(config, 'build.templates.source')
+              dest = get(config, 'build.templates.destination.path', 'build_local')
+              ext = get(config, 'build.templates.destination.ext', 'html')
+            }
+
+            const fileDir = path.parse(file).dir.replace(source, '')
+            const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
+
+            await fs.outputFile(config.permalink || finalDestination, html)
+          })
+          .then(() => {
+            browsersync().reload()
+            spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
+          })
+          .catch(() => spinner.warn(`Received empty HTML, please save your file again [${file}]`))
       })
-    )
-    .on('unlink', () => buildToFile(env, config)
-      .then(() => browsersync().reload())
-      .catch(error => {
-        throw error
+
+    // Watch for changes in all other files
+    browsersync()
+      .watch(globalPaths, {ignored: templatePaths})
+      .on('change', () => buildToFile(env, config).then(() => browsersync().reload()))
+      .on('unlink', () => buildToFile(env, config).then(() => browsersync().reload()))
+
+    // Watch for changes in config files
+    browsersync()
+      .watch('config*.js')
+      .on('change', async file => {
+        const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
+
+        Config
+          .getMerged(parsedEnv)
+          .then(config => buildToFile(parsedEnv, config).then(() => browsersync().reload()))
       })
-    )
-
-  // Watch for changes in config files
-  browsersync()
-    .watch('config*.js')
-    .on('change', async file => {
-      const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
-
-      Config
-        .getMerged(parsedEnv)
-        .then(config => buildToFile(parsedEnv, config)
-          .then(() => browsersync().reload())
-          .catch(error => {
-            throw error
-          })
-        )
-    })
-
-  // Browsersync options
-  const baseDir = templates.map(t => t.destination.path)
-
-  // Initialize Browsersync
-  browsersync()
-    .init(
-      merge(
-        {
-          notify: false,
-          open: false,
-          port: 3000,
-          server: {
-            baseDir,
-            directory: true
+
+    // Browsersync options
+    const baseDir = templates.map(t => t.destination.path)
+
+    // Initialize Browsersync
+    browsersync()
+      .init(
+        merge(
+          {
+            notify: false,
+            open: false,
+            port: 3000,
+            server: {
+              baseDir,
+              directory: true
+            },
+            tunnel: false,
+            ui: {port: 3001},
+            logFileChanges: false
           },
-          tunnel: false,
-          ui: {port: 3001},
-          logFileChanges: false
-        },
-        get(config, 'build.browsersync', {})
-      ), () => {})
+          get(config, 'build.browsersync', {})
+        ), () => {})
+  } catch (error) {
+    spinner.fail(error)
+    throw error
+  }
 }
 
 module.exports = serve
diff --git a/src/generators/config.js b/src/generators/config.js
index 3a61046..58197c6 100644
--- a/src/generators/config.js
+++ b/src/generators/config.js
@@ -13,23 +13,16 @@ module.exports = {
 
     const cwd = env === 'maizzle-ci' ? './test/stubs/config' : process.cwd()
 
-    for (const module of ['./config', './config.cjs', './config.local', './config.local.cjs']) {
+    for (const module of ['./config', './config.local']) {
       try {
         baseConfig = merge(baseConfig, requireUncached(path.resolve(cwd, module)))
       } catch {}
     }
 
     if (typeof env === 'string' && env !== 'local') {
-      let loaded = false
-      for (const module of [`./config.${env}`, `./config.${env}.cjs`]) {
-        try {
-          envConfig = merge(envConfig, requireUncached(path.resolve(cwd, module)))
-          loaded = true
-          break
-        } catch {}
-      }
-
-      if (!loaded) {
+      try {
+        envConfig = merge(envConfig, requireUncached(path.resolve(cwd, `./config.${env}`)))
+      } catch {
         throw new Error(`could not load config.${env}.js`)
       }
     }
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index e11f29a..9e1fa44 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -2,6 +2,7 @@ const path = require('path')
 const fs = require('fs-extra')
 const glob = require('glob-promise')
 const {get, isEmpty, merge} = require('lodash')
+const {asyncForEach} = require('../../utils/helpers')
 
 const Config = require('../config')
 const Tailwind = require('../tailwindcss')
@@ -27,15 +28,10 @@ module.exports = async (env, spinner, config) => {
 
   const css = (typeof get(config, 'build.tailwind.compiled') === 'string')
     ? config.build.tailwind.compiled
-    : await Tailwind.compile({config})
+    : await Tailwind.compile('', '', {}, config)
 
   // Parse each template config object
-  for await (const templateConfig of templatesConfig) {
-    if (!templateConfig) {
-      const configFileName = env === 'local' ? 'config.js' : `config.${env}.js`
-      throw new Error(`No template sources defined in \`build.templates\`, check your ${configFileName} file`)
-    }
-
+  await asyncForEach(templatesConfig, async templateConfig => {
     const outputDir = get(templateConfig, 'destination.path', `build_${env}`)
 
     await fs.remove(outputDir)
@@ -71,45 +67,23 @@ module.exports = async (env, spinner, config) => {
       }
     }
 
-    // Create a pipe-delimited list of allowed extensions
-    // We only compile these, the rest are copied as-is
-    const extensions = Array.isArray(templateConfig.filetypes)
-      ? templateConfig.filetypes.join('|')
-      : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
-
-    // List of files that won't be copied to the output directory
-    const omitted = Array.isArray(templateConfig.omit)
-      ? templateConfig.omit
-      : [get(templateConfig, 'omit', '')]
-
     // Parse each template source
-    for await (const source of templateSource) {
+    await asyncForEach(templateSource, async source => {
       /**
        * Copy single-file sources correctly
        * If `src` is a file, `dest` cannot be a directory
        * https://github.com/jprichardson/node-fs-extra/issues/323
        */
-      const out = fs.lstatSync(source).isFile()
-        ? `${outputDir}/${path.basename(source)}`
-        : outputDir
+      const out = fs.lstatSync(source).isFile() ? `${outputDir}/${path.basename(source)}` : outputDir
 
       await fs
-        .copy(source, out, {filter: file => {
-          // Do not copy omitted files
-          return !omitted
-            .filter(Boolean)
-            .some(omit => path.normalize(file).includes(path.normalize(omit)))
-        }})
+        .copy(source, out)
         .then(async () => {
-          const allSourceFiles = await glob(`${outputDir}/**/*.+(${extensions})`)
+          const extensions = Array.isArray(templateConfig.filetypes)
+            ? templateConfig.filetypes.join('|')
+            : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
 
-          const skipped = Array.isArray(templateConfig.skip) ?
-            templateConfig.skip :
-            [get(templateConfig, 'skip', '')]
-
-          const templates = allSourceFiles.filter(template => {
-            return !skipped.includes(template.replace(`${outputDir}/`, ''))
-          })
+          const templates = await glob(`${outputDir}/**/*.+(${extensions})`)
 
           if (templates.length === 0) {
             spinner.warn(`Error: no files with the .${extensions} extension found in ${templateConfig.source}`)
@@ -120,7 +94,7 @@ module.exports = async (env, spinner, config) => {
             await config.events.beforeCreate(config)
           }
 
-          for await (const file of templates) {
+          await asyncForEach(templates, async file => {
             config.build.current = {
               path: path.parse(file)
             }
@@ -140,7 +114,7 @@ module.exports = async (env, spinner, config) => {
                 ...config.events
               })
 
-              const destination = get(compiled, 'config.permalink', file)
+              const destination = config.permalink || file
 
               /**
                * Generate plaintext
@@ -151,7 +125,7 @@ module.exports = async (env, spinner, config) => {
 
               // Check if plaintext: true globally, fallback to template's front matter
               const plaintextConfig = get(templateConfig, 'plaintext', get(compiled.config, 'plaintext', false))
-              const plaintextPath = get(plaintextConfig, 'destination.path', destination)
+              const plaintextPath = get(plaintextConfig, 'destination.path', config.permalink || file)
 
               if (Boolean(plaintextConfig) || !isEmpty(plaintextConfig)) {
                 await Plaintext
@@ -203,23 +177,19 @@ module.exports = async (env, spinner, config) => {
                   throw error
               }
             }
-          }
+          })
 
           const assets = {source: '', destination: 'assets', ...get(templateConfig, 'assets')}
 
           if (Array.isArray(assets.source)) {
-            for await (const source of assets.source) {
+            await asyncForEach(assets.source, async source => {
               if (fs.existsSync(source)) {
-                await fs
-                  .copy(source, path.join(templateConfig.destination.path, assets.destination))
-                  .catch(error => spinner.warn(error.message))
+                await fs.copy(source, path.join(templateConfig.destination.path, assets.destination)).catch(error => spinner.warn(error.message))
               }
-            }
+            })
           } else {
             if (fs.existsSync(assets.source)) {
-              await fs
-                .copy(assets.source, path.join(templateConfig.destination.path, assets.destination))
-                .catch(error => spinner.warn(error.message))
+              await fs.copy(assets.source, path.join(templateConfig.destination.path, assets.destination)).catch(error => spinner.warn(error.message))
             }
           }
 
@@ -229,8 +199,8 @@ module.exports = async (env, spinner, config) => {
             })
         })
         .catch(error => spinner.warn(error.message))
-    }
-  }
+    })
+  })
 
   if (config.events && typeof config.events.afterBuild === 'function') {
     await config.events.afterBuild(files)
diff --git a/src/generators/output/to-string.js b/src/generators/output/to-string.js
index cd0ad89..3a90e31 100644
--- a/src/generators/output/to-string.js
+++ b/src/generators/output/to-string.js
@@ -22,6 +22,9 @@ module.exports = async (html, options) => {
 
   let config = merge(fileConfig, get(options, 'maizzle', {}))
 
+  const tailwindConfig = get(options, 'tailwind.config', {})
+  const cssString = get(options, 'tailwind.css', '')
+
   const {frontmatter} = fm(html)
 
   html = `---\n${frontmatter}\n---\n\n${fm(html).body}`
@@ -31,17 +34,7 @@ module.exports = async (html, options) => {
   if (typeof get(options, 'tailwind.compiled') === 'string') {
     config.css = options.tailwind.compiled
   } else {
-    config.css = await Tailwind.compile({
-      css: get(options, 'tailwind.css', ''),
-      html,
-      config: merge(config, {
-        build: {
-          tailwind: {
-            config: get(options, 'tailwind.config')
-          }
-        }
-      })
-    })
+    config.css = await Tailwind.compile(cssString, html, tailwindConfig, config)
   }
 
   if (options && typeof options.beforeRender === 'function') {
diff --git a/src/generators/postcss.js b/src/generators/postcss.js
index 416de6c..4e30cb8 100644
--- a/src/generators/postcss.js
+++ b/src/generators/postcss.js
@@ -9,9 +9,7 @@ module.exports = {
     return postcss([
       postcssImport(),
       postcssNested(),
-      get(maizzleConfig, 'shorthandCSS', get(maizzleConfig, 'shorthandInlineCSS')) === true ?
-        mergeLonghand() :
-        () => {},
+      maizzleConfig.env === 'local' ? () => {} : mergeLonghand(),
       ...get(maizzleConfig, 'build.postcss.plugins', [])
     ])
       .process(css, {from: undefined})
diff --git a/src/generators/posthtml/index.js b/src/generators/posthtml/index.js
index 96d3a98..9824831 100644
--- a/src/generators/posthtml/index.js
+++ b/src/generators/posthtml/index.js
@@ -3,23 +3,23 @@ const posthtml = require('posthtml')
 const {get, merge} = require('lodash')
 const fetch = require('posthtml-fetch')
 const layouts = require('posthtml-extend')
-const components = require('posthtml-component')
+const modules = require('posthtml-modules')
 const defaultConfig = require('./defaultConfig')
+const expressions = require('posthtml-expressions')
 
 module.exports = async (html, config) => {
   const layoutsOptions = get(config, 'build.layouts', {})
-  const componentsOptions = get(config, 'build.components', {})
-  const expressionsOptions = merge(
-    {
-      loopTags: ['each', 'for'],
-      strictMode: false
-    },
-    get(config, 'build.posthtml.expressions', {})
-  )
+
+  const modulesOptions = get(config, 'build.components', {})
+  // Fake `from` option so we can reference modules relatively
+  const modulesRoot = modulesOptions.root || './'
+  const modulesFrom = modulesOptions.from || `${modulesRoot}/fake`
 
   const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
   const posthtmlPlugins = get(config, 'build.posthtml.plugins', [])
 
+  const expressionsOptions = merge({strictMode: false}, get(config, 'build.posthtml.expressions', {}))
+
   const locals = merge(
     get(expressionsOptions, 'locals', {}),
     get(config, 'locals', {}),
@@ -37,6 +37,7 @@ module.exports = async (html, config) => {
 
   return posthtml([
     fetchPlugin,
+    expressions({...expressionsOptions, locals}),
     layouts(
       merge(
         {
@@ -46,20 +47,20 @@ module.exports = async (html, config) => {
         layoutsOptions
       )
     ),
-    components(
-      merge(
-        {
-          root: componentsOptions.root || './',
-          folders: ['src/components', 'src/layouts', 'src/templates'],
-          tag: 'component',
-          attribute: 'src',
-          yield: 'content',
-          propsAttribute: 'locals',
-          expressions: {...expressionsOptions, locals}
-        },
-        componentsOptions
-      )
-    ),
+    modules({
+      expressions: expressionsOptions,
+      parser: posthtmlOptions,
+      attributeAsLocals: true,
+      from: modulesFrom,
+      root: modulesRoot,
+      tag: 'component',
+      attribute: 'src',
+      plugins: [
+        fetchPlugin
+      ],
+      locals,
+      ...modulesOptions
+    }),
     ...posthtmlPlugins
   ])
     .process(html, {...posthtmlOptions})
diff --git a/src/generators/tailwindcss.js b/src/generators/tailwindcss.js
index e89594f..42a9e24 100644
--- a/src/generators/tailwindcss.js
+++ b/src/generators/tailwindcss.js
@@ -9,14 +9,14 @@ const mergeLonghand = require('postcss-merge-longhand')
 const {get, isObject, isEmpty, merge} = require('lodash')
 
 module.exports = {
-  compile: async ({css = '', html = '', config = {}}) => {
-    // Compute the Tailwind config to use
-    const userConfig = config => {
-      const tailwindUserConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
+  compile: async (css = '', html = '', tailwindConfig = {}, maizzleConfig = {}) => {
+    tailwindConfig = (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) ? tailwindConfig : get(maizzleConfig, 'build.tailwind.config', 'tailwind.config.js')
 
+    // Compute the Tailwind config to use
+    const userConfig = () => {
       // If a custom config object was passed, use that
-      if (isObject(tailwindUserConfig) && !isEmpty(tailwindUserConfig)) {
-        return tailwindUserConfig
+      if (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) {
+        return tailwindConfig
       }
 
       /**
@@ -24,17 +24,17 @@ module.exports = {
        * This will use the default Tailwind config (with rem units etc)
        */
       try {
-        return requireUncached(path.resolve(process.cwd(), tailwindUserConfig))
+        return requireUncached(path.resolve(process.cwd(), tailwindConfig))
       } catch {
         return {}
       }
     }
 
     // Merge user's Tailwind config on top of a 'base' config
-    const layoutsRoot = get(config, 'build.layouts.root')
-    const componentsRoot = get(config, 'build.components.root')
+    const layoutsRoot = get(maizzleConfig, 'build.layouts.root')
+    const componentsRoot = get(maizzleConfig, 'build.components.root')
 
-    const tailwindConfig = merge({
+    const config = merge({
       important: true,
       content: {
         files: [
@@ -47,20 +47,20 @@ module.exports = {
           {raw: html, extension: 'html'}
         ]
       }
-    }, userConfig(config))
+    }, userConfig())
 
     // Add back the `{raw: html}` option if user provided own config
-    if (Array.isArray(tailwindConfig.content)) {
-      tailwindConfig.content = {
+    if (Array.isArray(config.content)) {
+      config.content = {
         files: [
-          ...tailwindConfig.content,
+          ...config.content,
           {raw: html, extension: 'html'}
         ]
       }
     }
 
     // Include all `build.templates.source` paths when scanning for selectors to preserve
-    const buildTemplates = get(config, 'build.templates')
+    const buildTemplates = get(maizzleConfig, 'build.templates')
 
     if (buildTemplates) {
       const templateObjects = Array.isArray(buildTemplates) ? buildTemplates : [buildTemplates]
@@ -68,12 +68,12 @@ module.exports = {
         const source = get(template, 'source')
 
         if (typeof source === 'function') {
-          const sources = source(config)
+          const sources = source(maizzleConfig)
 
           if (Array.isArray(sources)) {
-            sources.map(s => tailwindConfig.content.files.push(s))
+            sources.map(s => config.content.files.push(s))
           } else if (typeof sources === 'string') {
-            tailwindConfig.content.files.push(sources)
+            config.content.files.push(sources)
           }
 
           // Must return a valid `content` entry
@@ -82,7 +82,7 @@ module.exports = {
 
         // Support single-file sources i.e. src/templates/index.html
         if (typeof source === 'string' && Boolean(path.extname(source))) {
-          tailwindConfig.content.files.push(source)
+          config.content.files.push(source)
 
           return {raw: '', extension: 'html'}
         }
@@ -90,24 +90,21 @@ module.exports = {
         return `${source}/**/*.*`
       })
 
-      tailwindConfig.content.files.push(...templateSources)
+      config.content.files.push(...templateSources)
     }
 
-    const userFilePath = get(config, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
+    const userFilePath = get(maizzleConfig, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
     const userFileExists = await fs.pathExists(userFilePath)
 
     const toProcess = [
       postcssNested(),
-      tailwindcss(tailwindConfig),
-      get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS')) === true ?
-        mergeLonghand() :
-        () => {},
-      ...get(config, 'build.postcss.plugins', [])
+      tailwindcss(config),
+      maizzleConfig.env === 'local' ? () => {} : mergeLonghand(),
+      ...get(maizzleConfig, 'build.postcss.plugins', [])
     ]
 
     if (userFileExists) {
       css = await fs.readFile(path.resolve(userFilePath), 'utf8') + css
-
       toProcess.unshift(
         postcssImport({path: path.dirname(userFilePath)})
       )
diff --git a/src/transformers/baseUrl.js b/src/transformers/baseUrl.js
index 8eccd7e..023251c 100644
--- a/src/transformers/baseUrl.js
+++ b/src/transformers/baseUrl.js
@@ -5,7 +5,7 @@ const {get, merge, isObject, isEmpty} = require('lodash')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config = {}, direct = false) => {
-  const url = direct ? config : get(config, 'baseURL', get(config, 'baseUrl'))
+  const url = direct ? config : get(config, 'baseURL')
   const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
 
   // Handle `baseUrl` as a string
@@ -19,15 +19,11 @@ module.exports = async (html, config = {}, direct = false) => {
       .then(result => result.html)
   }
 
-  // Handle `baseURL` as an object
+  // Handle `baseUrl` as an object
   if (isObject(url) && !isEmpty(url)) {
-    html = rewriteVMLs(html, get(url, 'url', ''))
+    html = rewriteVMLs(html, url.url)
 
-    return posthtml([
-      baseUrl(merge({styleTag: true, inlineCss: true}, url))
-    ])
-      .process(html, posthtmlOptions)
-      .then(result => result.html)
+    return posthtml([baseUrl(url)]).process(html, posthtmlOptions).then(result => result.html)
   }
 
   return html
diff --git a/src/transformers/filters/index.js b/src/transformers/filters/index.js
index 39bdbd1..f7f94d2 100644
--- a/src/transformers/filters/index.js
+++ b/src/transformers/filters/index.js
@@ -22,19 +22,10 @@ module.exports = async (html, config = {}, direct = false) => {
    * Compile CSS in <style {post|tailwind}css> tags
    */
   const maizzleConfig = omit(config, ['build.tailwind.css', 'css'])
+  const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
 
   filters.postcss = css => PostCSS.process(css, maizzleConfig)
-  filters.tailwindcss = css => Tailwind.compile({
-    css,
-    html,
-    config: merge({
-      build: {
-        tailwind: {
-          config: get(config, 'build.tailwind.config', {})
-        }
-      }
-    }, maizzleConfig)
-  })
+  filters.tailwindcss = css => Tailwind.compile(css, html, tailwindConfig, maizzleConfig)
 
   const posthtmlPlugins = [
     styleDataEmbed(),
diff --git a/src/transformers/index.js b/src/transformers/index.js
index bff21bd..cee5d92 100644
--- a/src/transformers/index.js
+++ b/src/transformers/index.js
@@ -26,8 +26,9 @@ exports.process = async (html, config) => {
   html = await preventWidows(html, config)
   html = await attributeToStyle(html, config)
   html = await inline(html, config)
-  html = await removeUnusedCSS(html, config)
   html = await shorthandInlineCSS(html, config)
+  html = await removeUnusedCSS(html, config)
+  html = await removeInlinedClasses(html, config)
   html = await removeInlineSizes(html, config)
   html = await removeInlineBgColor(html, config)
   html = await removeAttributes(html, config)
@@ -53,7 +54,7 @@ exports.addURLParams = (html, config) => addURLParams(html, config, true)
 exports.preventWidows = (html, config) => preventWidows(html, config)
 exports.replaceStrings = (html, config) => replaceStrings(html, config, true)
 exports.safeClassNames = (html, config) => safeClassNames(html, config, true)
-exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config)
+exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config, true)
 exports.removeAttributes = (html, config) => removeAttributes(html, config, true)
 exports.attributeToStyle = (html, config) => attributeToStyle(html, config, true)
 exports.removeInlineSizes = (html, config) => removeInlineSizes(html, config, true)
diff --git a/src/transformers/removeInlinedSelectors.js b/src/transformers/removeInlinedSelectors.js
index 1a4f6eb..a0ecfe6 100644
--- a/src/transformers/removeInlinedSelectors.js
+++ b/src/transformers/removeInlinedSelectors.js
@@ -20,26 +20,16 @@ const plugin = posthtmlOptions => tree => {
     // For each style tag...
     if (node.tag === 'style') {
       const {root} = postcss().process(node.content)
-      const preservedClasses = []
 
-      // Preserve selectors in at rules
-      root.walkAtRules(rule => {
-        if (['media', 'supports'].includes(rule.name)) {
-          rule.walkRules(rule => {
-            preservedClasses.push(rule.selector)
-          })
+      root.walkRules(rule => {
+        // Skip media queries and such...
+        if (rule.parent.type === 'atrule') {
+          return
         }
-      })
 
-      root.walkRules(rule => {
         const {selector} = rule
         const prop = get(rule.nodes[0], 'prop')
 
-        // Preserve pseudo selectors
-        if (selector.includes(':')) {
-          preservedClasses.push(selector)
-        }
-
         try {
           // If we find the selector in the HTML...
           tree.match(matchHelper(selector), n => {
@@ -49,16 +39,23 @@ const plugin = posthtmlOptions => tree => {
 
             // If the class is in the style attribute (inlined), remove it
             if (has(styleAttr, prop)) {
-              // Remove the class as long as it's not a preserved class
-              if (!preservedClasses.some(item => item.endsWith(selector) || item.startsWith(selector))) {
-                remove(classAttr, classToRemove => selector.includes(classToRemove))
-              }
+              // Remove the class attribute
+              remove(classAttr, s => selector.includes(s))
 
               // Remove the rule in the <style> tag
-              if (rule.parent.type !== 'atrule') {
+              rule.remove()
+            }
+
+            /**
+             * Remove from <style> selectors that were used to create shorthand declarations
+             * like `margin: 0 0 0 16px` (transformed with mergeLonghand when inlining).
+             */
+            Object.keys(styleAttr).forEach(key => {
+              if (prop && prop.includes(key)) {
                 rule.remove()
+                remove(classAttr, s => selector.includes(s))
               }
-            }
+            })
 
             n.attrs = parsedAttrs.compose()
 
diff --git a/src/transformers/removeUnusedCss.js b/src/transformers/removeUnusedCss.js
index 60c79a7..a798452 100644
--- a/src/transformers/removeUnusedCss.js
+++ b/src/transformers/removeUnusedCss.js
@@ -1,13 +1,15 @@
 const {comb} = require('email-comb')
 const {get, merge} = require('lodash')
-const removeInlinedClasses = require('./removeInlinedSelectors')
 
-module.exports = async (html, config = {}) => {
-  // If it's explicitly disabled, return the HTML
+module.exports = async (html, config = {}, direct = false) => {
   if (get(config, 'removeUnusedCSS') === false) {
     return html
   }
 
+  if (!direct && !get(config, 'removeUnusedCSS')) {
+    return html
+  }
+
   const safelist = [
     '*body*', // Gmail
     '.gmail*', // Gmail
@@ -34,9 +36,9 @@ module.exports = async (html, config = {}) => {
     whitelist: [...get(config, 'whitelist', []), ...safelist]
   }
 
-  const options = merge(defaultOptions, get(config, 'removeUnusedCSS', config))
-
-  html = await removeInlinedClasses(html, options)
+  const options = typeof config === 'boolean' && config ?
+    defaultOptions :
+    merge(defaultOptions, get(config, 'removeUnusedCSS', config))
 
   return comb(html, options).result
 }
diff --git a/src/transformers/shorthandInlineCSS.js b/src/transformers/shorthandInlineCSS.js
index 45fbb25..47d1ebf 100644
--- a/src/transformers/shorthandInlineCSS.js
+++ b/src/transformers/shorthandInlineCSS.js
@@ -1,25 +1,19 @@
 const posthtml = require('posthtml')
 const {get, merge, isObject, isEmpty} = require('lodash')
-const mergeInlineLonghand = require('posthtml-postcss-merge-longhand')
+const mergeLonghand = require('posthtml-postcss-merge-longhand')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config, direct = false) => {
-  config = direct ?
-    (isObject(config) ? config : true) :
-    get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS', []))
+  config = direct ? (isObject(config) ? config : true) : get(config, 'shorthandInlineCSS', [])
 
   const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
 
   if (typeof config === 'boolean' && config) {
-    html = await posthtml([mergeInlineLonghand()])
-      .process(html, posthtmlOptions)
-      .then(result => result.html)
+    html = await posthtml([mergeLonghand()]).process(html, posthtmlOptions).then(result => result.html)
   }
 
   if (isObject(config) && !isEmpty(config)) {
-    html = await posthtml([mergeInlineLonghand(config)])
-      .process(html, posthtmlOptions)
-      .then(result => result.html)
+    html = await posthtml([mergeLonghand(config)]).process(html, posthtmlOptions).then(result => result.html)
   }
 
   return html
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 85684da..bc6d26d 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -1,4 +1,9 @@
 module.exports = {
+  asyncForEach: async (array, callback) => {
+    for (let index = 0; index < array.length; index++) {
+      await callback(array[index], index, array) // eslint-disable-line
+    }
+  },
   requireUncached: module => {
     try {
       delete require.cache[require.resolve(module)]


Test patch:
diff --git a/test/expected/components/backwards-compatibility.html b/test/expected/components/backwards-compatibility.html
deleted file mode 100644
index 3e35f60..0000000
--- a/test/expected/components/backwards-compatibility.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<p>Variable from attribute: Example</p>
-<p>Variable from locals attribute: bar</p>
-<p class="hidden">Variable from page: maizzle-ci</p>
-<p>Variable from attribute: Nested component</p>
-<p>Variable from locals attribute: bar (nested)</p>
-<p>Variable from page (nested): maizzle-ci</p>
diff --git a/test/expected/components/kitchen-sink.html b/test/expected/components/kitchen-sink.html
deleted file mode 100644
index aea4cd0..0000000
--- a/test/expected/components/kitchen-sink.html
+++ /dev/null
@@ -1,73 +0,0 @@
-{{ test }}
-The Website Weekly Newsletter
-bar
-{% capture email_title %}Hey {{customer.first_name}} {{ customer.last_name}}, {% endcapture %}
-<!DOCTYPE html>
-<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
-<head>
-  <style>
-  </style>
-</head>
-<body class="bg-red-500">
-  <h1>H1 in fill:template</h1>
-
-  <!-- Expressions -->
-  Compiled: maizzle-ci
-  Ignored: {{ page.env }}
-  Inside component: maizzle-ci
-  Inside component (ignored): {{ page.env }}
-  Inside nested component: maizzle-ci
-  Inside nested component (ignored): {{ page.env }}
-
-  <!-- Component slots -->
-  Slot header
-
-  <!-- Markdown -->
-  <h3>Heading</h3>
-  <pre><code class="language-js">console.log('Hello world!')
-</code></pre>
-  <h3>Markdown in component</h3>
-
-  <!-- Passing props to component -->
-  Environment: maizzle-ci
-  Environment (ignore expression): {{ page.env }}
-
-  <!-- Nesting components, with props -->
-  Parent
-  Childish
-
-  <!-- <fetch> tag -->
-  <ul>
-    <li>
-      Leanne Graham
-    </li>
-    <li>
-      Ervin Howell
-    </li>
-    <li>
-      Clementine Bauch (last item)
-    </li>
-  </ul>
-
-  <!-- <outlook> tag -->
-
-  <!--[if mso]>in outlook<![endif]-->
-
-  <!-- Conditionals -->
-  <p>Using Maizzle programmatically</p>
-
-  <!-- Loops -->
-  {{ i }} is <i>1</i>
-  {{ i }} is <i>2 (last)</i>
-
-  <!-- Switch -->
-  page.foo is bar
-
-  <!-- Scope -->
-  Author: John
-  Editor: Jane
-
-  <!-- Raw -->
-  Expression should be ignored: {{ foo }}
-</body>
-</html>
diff --git a/test/expected/posthtml/component.html b/test/expected/posthtml/component.html
new file mode 100644
index 0000000..9d1cae5
--- /dev/null
+++ b/test/expected/posthtml/component.html
@@ -0,0 +1,13 @@
+Variable from attribute: Example
+
+Variable from locals attribute: bar
+
+
+Variable from page: maizzle-ci
+
+  Variable from attribute: Nested component
+
+Variable from locals attribute: bar (nested)
+
+
+Variable from page (nested): maizzle-ci
diff --git a/test/fixtures/components/kitchen-sink.html b/test/fixtures/components/kitchen-sink.html
deleted file mode 100644
index dbb5970..0000000
--- a/test/fixtures/components/kitchen-sink.html
+++ /dev/null
@@ -1,124 +0,0 @@
----
-title: "The Website Weekly Newsletter"
-roles:
-  author:
-    name: "John"
-  editor:
-    name: "Jane"
----
-
-@{{ test }}
-
-{{ page.title }}
-
-{{ page.foo }}
-
-<raw>
-  {% capture email_title %}Hey {{customer.first_name}} {{ customer.last_name}}, {% endcapture %}
-</raw>
-
-<x-layout>
-  <fill:template>
-    <h1>H1 in fill:template</h1>
-
-    <!-- Expressions -->
-    Compiled: {{ page.env }}
-    Ignored: @{{ page.env }}
-    <x-bare>
-      Inside component: {{ page.env }}
-      Inside component (ignored): @{{ page.env }}
-
-      <x-bare>
-        Inside nested component: {{ page.env }}
-        Inside nested component (ignored): @{{ page.env }}
-      </x-bare>
-    </x-bare>
-
-    <!-- Component slots -->
-    <x-bare>
-      <slot:header>Slot header</slot:header>
-    </x-bare>
-
-    <!-- Markdown -->
-    <md>
-      ### Heading
-
-      ```js
-      console.log('Hello world!')
-      ```
-    </md>
-
-    <x-bare>
-      <md>
-        ### Markdown in component
-      </md>
-    </x-bare>
-
-    <!-- Passing props to component -->
-    <x-text text="{{ page.env }}">Environment: </x-text>
-    <x-text text="@{{ page.env }}">Environment (ignore expression): </x-text>
-
-    <!-- Nesting components, with props -->
-    <x-bare aware:text="ish">
-      Parent
-      <x-text>Child</x-text>
-    </x-bare>
-
-    <!-- <fetch> tag -->
-    <x-bare>
-      <fetch url="test/stubs/data.json">
-        <ul>
-          <each loop="user in response">
-            <li>
-              <x-bare>{{ user.name + (loop.last ? ' (last item)' : '') }}</x-bare>
-            </li>
-          </each>
-        </ul>
-      </fetch>
-    </x-bare>
-
-    <!-- <outlook> tag -->
-    <x-bare>
-      <outlook>in outlook</outlook>
-    </x-bare>
-
-    <!-- Conditionals -->
-    <x-bare>
-      <if condition="page.env === 'maizzle-ci'">
-        <p>Using Maizzle programmatically</p>
-      </if>
-    </x-bare>
-
-    <!-- Loops -->
-    <x-bare>
-      <each loop="i in [1, 2]">
-        @{{ i }} is <i>{{ i }}{{ loop.last ? ' (last)' : '' }}</i>
-      </each>
-    </x-bare>
-
-    <!-- Switch -->
-    <switch expression="page.foo">
-      <case n="'bar'">
-        page.foo is {{ page.foo }}
-      </case>
-      <default>
-        default - page.foo is {{ page.foo }}
-      </default>
-    </switch>
-
-    <!-- Scope -->
-    <x-bare>
-      <scope with="page.roles">
-        Author: {{ author.name }}
-        Editor: {{ editor.name }}
-      </scope>
-    </x-bare>
-
-    <!-- Raw -->
-    <x-bare>
-      <raw>
-        Expression should be ignored: {{ foo }}
-      </raw>
-    </x-bare>
-  </fill:template>
-</x-layout>
diff --git a/test/fixtures/components/backwards-compatibility.html b/test/fixtures/posthtml/component.html
similarity index 50%
rename from test/fixtures/components/backwards-compatibility.html
rename to test/fixtures/posthtml/component.html
index 3f6c19d..b39f7b9 100644
--- a/test/fixtures/components/backwards-compatibility.html
+++ b/test/fixtures/posthtml/component.html
@@ -5,15 +5,15 @@
     "foo": "bar"
   }'
 >
-<p class="hidden">Variable from page: [[ page.env ]]</p>
+<p>Variable from page: [[ page.env ]]</p>
 
-<component
-  src="test/stubs/components/component.html"
-  text="Nested component"
-  locals='{
-    "foo": "bar (nested)"
-  }'
->
+  <component
+    src="test/stubs/components/component.html"
+    text="Nested component"
+    locals='{
+      "foo": "bar (nested)"
+    }'
+  >
 <p>Variable from page (nested): [[ page.env ]]</p>
   </component>
 </component>
diff --git a/test/stubs/components/bare.html b/test/stubs/components/bare.html
deleted file mode 100644
index 448489a..0000000
--- a/test/stubs/components/bare.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<script props>
-  module.exports = {
-    text: props.text || 'default text'
-  }
-</script>
-<slot:header></slot:header>
-<slot:aw></slot:aw>
-<content></content>
diff --git a/test/stubs/components/component.html b/test/stubs/components/component.html
index b750e1d..c65a4ea 100644
--- a/test/stubs/components/component.html
+++ b/test/stubs/components/component.html
@@ -1,10 +1,3 @@
-<script props>
-  module.export = {
-    text: props.text || 'default text',
-    foo: props.foo || 'default foo'
-  }
-</script>
-
 <p>Variable from attribute: [[ text ]]</p>
 
 <p>Variable from locals attribute: [[ foo ]]</p>
diff --git a/test/stubs/components/text.html b/test/stubs/components/text.html
deleted file mode 100644
index ee363a0..0000000
--- a/test/stubs/components/text.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<script props>
-  module.exports = {
-    text: props.text || 'default text'
-  }
-</script>
-<content></content>{{ text }}
diff --git a/test/stubs/layouts/layout.html b/test/stubs/layouts/layout.html
deleted file mode 100644
index 9b05f77..0000000
--- a/test/stubs/layouts/layout.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE {{{ page.doctype || 'html' }}}>
-<html lang="{{ page.language || 'en' }}" xmlns:v="urn:schemas-microsoft-com:vml">
-  <head>
-    <stack name="styles"></stack>
-    <style>
-      {{{ page.css }}}
-    </style>
-  </head>
-
-  <body class="{{ page.bodyClass ?? 'bg-red-500' }}">
-    <slot:template></slot:template>
-  </body>
-</html>
diff --git a/test/stubs/templates/nested/3.html b/test/stubs/templates/nested/3.html
deleted file mode 100644
index 1d49a0d..0000000
--- a/test/stubs/templates/nested/3.html
+++ /dev/null
@@ -1,1 +0,0 @@
-<div class="inline">nested template</div>
diff --git a/test/test-posthtml.js b/test/test-posthtml.js
index 496a554..6e932a6 100644
--- a/test/test-posthtml.js
+++ b/test/test-posthtml.js
@@ -1,15 +1,5 @@
 const test = require('ava')
-const {render, prettify} = require('../src')
-
-const path = require('path')
-const fs = require('fs')
-
-const readFile = (dir, filename) => fs.promises
-  .readFile(path.join(__dirname, dir, `${filename}.html`), 'utf8')
-  .then(html => html.trim())
-
-const fixture = file => readFile('fixtures', file)
-const expected = file => readFile('expected', file)
+const {render} = require('../src')
 
 const renderString = (string, options = {}) => render(string, options).then(({html}) => html)
 
@@ -51,53 +41,54 @@ template: second
 })
 
 test('components', async t => {
-  const source = await fixture('components/kitchen-sink')
+  const source = `<component
+  src="test/stubs/components/component.html"
+  text="Example"
+  locals='{
+    "foo": "bar"
+  }'
+>
+<p class="hidden">Variable from page: [[ page.env ]]</p>
+
+  <component
+    src="test/stubs/components/component.html"
+    text="Nested component"
+    locals='{
+      "foo": "bar (nested)"
+    }'
+  >
+<p>Variable from page (nested): [[ page.env ]]</p>
+  </component>
+</component>`
 
   const options = {
     maizzle: {
-      env: 'maizzle-ci',
+      env: 'prod',
       build: {
         components: {
-          folders: ['test/stubs/layouts', 'test/stubs/components']
+          expressions: {
+            delimiters: ['[[', ']]']
+          }
         }
-      },
-      removeUnusedCSS: false
-    },
-    beforeRender(html, config) {
-      config.foo = 'bar'
-
-      return html
+      }
     }
   }
 
   const html = await renderString(source, options)
 
-  t.is(
-    await prettify(html, {ocd: true}),
-    await expected('components/kitchen-sink')
-  )
-})
+  t.is(html.trim(), `<p>Variable from attribute: Example</p>
 
-test('components (backwards compatibility)', async t => {
-  const source = await fixture('components/backwards-compatibility')
+<p>Variable from locals attribute: bar</p>
 
-  const options = {
-    maizzle: {
-      env: 'maizzle-ci',
-      build: {
-        posthtml: {
-          expressions: {
-            delimiters: ['[[', ']]']
-          }
-        }
-      },
-      removeUnusedCSS: false
-    }
-  }
 
-  const html = await renderString(source, options)
+<p class="hidden">Variable from page: prod</p>
+
+  <p>Variable from attribute: Nested component</p>
+
+<p>Variable from locals attribute: bar (nested)</p>
+
 
-  t.is(html.replace(/\n+/g, '\n').trim(), await expected('components/backwards-compatibility'))
+<p>Variable from page (nested): prod</p>`)
 })
 
 test('fetch component', async t => {
diff --git a/test/test-serve.js b/test/test-serve.js
deleted file mode 100644
index 9cff306..0000000
--- a/test/test-serve.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const test = require('ava')
-const fs = require('fs-extra')
-const Maizzle = require('../src')
-
-test.beforeEach(t => {
-  t.context.folder = '_temp_' + Math.random().toString(36).slice(2, 9)
-  t.context.log = console.log()
-})
-
-test.afterEach.always(async t => {
-  if (t.context.folder) {
-    await fs.remove(t.context.folder)
-    delete t.context.folder
-  }
-})
-
-test('throws if it cannot spin up local development server', async t => {
-  // Should throw because there are no template sources in `build.templates`
-  await t.throwsAsync(async () => {
-    await Maizzle.serve('local', {})
-  }, {instanceOf: Error})
-})
-
-test('local server does not compile unwanted file types', async t => {
-  await Maizzle.serve('local', {
-    build: {
-      console: {
-        clear: true
-      },
-      browsersync: {
-        ui: false
-      },
-      templates: {
-        source: 'test/stubs/templates',
-        destination: {
-          path: `${t.context.folder}`
-        }
-      }
-    },
-    events: {
-      beforeCreate(config) {
-        config.foo = 'bar'
-      }
-    }
-  })
-
-  t.true(await fs.pathExists(`${t.context.folder}`))
-  t.true(await fs.pathExists(`${t.context.folder}/2.test`))
-
-  // Tests watching changes to files
-  await fs.outputFile('test/stubs/templates/2.html', '<div class="inline">html modified</div>')
-  t.is(await fs.readFile('test/stubs/templates/2.html', 'utf8'), '<div class="inline">html modified</div>')
-
-  // Don't trigger rebuilds on files not in `filetypes`
-  await fs.outputFile('test/stubs/templates/2.test', 'test')
-  t.is(await fs.readFile('test/stubs/templates/2.test', 'utf8'), 'test')
-})
diff --git a/test/test-tailwindcss.js b/test/test-tailwindcss.js
index ae81030..c38c5df 100644
--- a/test/test-tailwindcss.js
+++ b/test/test-tailwindcss.js
@@ -3,25 +3,25 @@ const Tailwind = require('../src/generators/tailwindcss')
 
 test('throws on compile error', async t => {
   await t.throwsAsync(async () => {
-    await Tailwind.compile({
-      css: 'div {@apply inexistent;}',
-      html: '<div class="inline">Test</a>'
-    })
+    await Tailwind.compile('div {@apply inexistent;}', '<div class="inline">Test</a>', {}, {})
   }, {instanceOf: SyntaxError})
 })
 
 test('uses defaults if no config specified', async t => {
-  const css = await Tailwind.compile({
-    css: '@tailwind utilities;',
-    html: '<p class="xl:w-1"></p>'
-  })
+  const css = await Tailwind.compile(
+    '@tailwind utilities;',
+    '<p class="xl:z-0"></p>',
+    {},
+    {env: 'production'}
+  )
 
   t.not(css, undefined)
-  t.true(css.includes('@media (min-width: 1280px)'))
+  t.true(css.includes('.xl\\:z-0'))
 })
 
 test('uses css file provided in environment config', async t => {
   const config = {
+    env: 'production',
     build: {
       tailwind: {
         css: './test/stubs/main.css'
@@ -29,10 +29,7 @@ test('uses css file provided in environment config', async t => {
     }
   }
 
-  const css = await Tailwind.compile({
-    html: '<div class="text-center foo">test</div>',
-    config
-  })
+  const css = await Tailwind.compile('', '<div class="text-center foo">test</div>', {}, config)
 
   t.not(css, undefined)
   t.true(css.includes('.text-center'))
@@ -40,69 +37,68 @@ test('uses css file provided in environment config', async t => {
 })
 
 test('works with custom `content` sources', async t => {
-  const css = await Tailwind.compile({
-    config: {
-      build: {
-        tailwind: {
-          config: {
-            content: ['./test/stubs/tailwind/*.html']
-          }
-        }
-      }
+  const css = await Tailwind.compile(
+    '@tailwind utilities;',
+    '<div class="hidden"></div>',
+    {
+      content: ['./test/stubs/tailwind/*.*']
     }
-  })
+  )
 
   t.true(css.includes('.hidden'))
 })
 
 test('works with custom `files` sources', async t => {
-  const css = await Tailwind.compile({
-    config: {
-      build: {
-        tailwind: {
-          config: {
-            content: {
-              files: ['./test/stubs/tailwind/*.html']
-            }
-          }
-        }
+  const css = await Tailwind.compile(
+    '@tailwind utilities;',
+    '<div class="inline"></div>',
+    {
+      content: {
+        files: ['./test/stubs/tailwind/*.*']
       }
     }
-  })
+  )
 
   t.true(css.includes('.hidden'))
 })
 
 test('uses maizzle template path as content source', async t => {
-  const css = await Tailwind.compile({
-    config: {
+  const css = await Tailwind.compile(
+    '@tailwind utilities;',
+    '<div class="inline"></div>',
+    {},
+    {
       build: {
         templates: {
           source: './test/stubs/tailwind'
         }
       }
     }
-  })
+  )
 
   t.true(css.includes('.hidden'))
 })
 
 test('uses maizzle template path as content source (single file)', async t => {
-  const css = await Tailwind.compile({
-    config: {
+  const css = await Tailwind.compile(
+    '@tailwind utilities;',
+    '<div class="inline"></div>',
+    {},
+    {
       build: {
         templates: {
           source: './test/stubs/tailwind/content-source.html'
         }
       }
     }
-  })
+  )
 
   t.true(css.includes('.hidden'))
 })
 
 test('uses custom postcss plugins from the maizzle config', async t => {
-  const config = {
+  const maizzleConfig = {
+    env: 'production',
     build: {
       postcss: {
         plugins: [
@@ -112,37 +108,8 @@ test('uses custom postcss plugins from the maizzle config', async t => {
     }
   }
 
-  const css = await Tailwind.compile({
-    css: '.test {transform: scale(0.5)}',
-    html: '<div class="test inline">Test</div>',
-    config
-  })
+  const css = await Tailwind.compile('.test {transform: scale(0.5)}', '<div class="test inline">Test</a>', {}, maizzleConfig)
 
   t.not(css, undefined)
   t.is(css.trim(), '.inline {display: inline !important} .test {-webkit-transform: scale(0.5);transform: scale(0.5)}')
 })
-
-test('respects `shorthandCSS` in maizzle config', async t => {
-  const shorthandDisabled = await Tailwind.compile({
-    css: '@layer utilities { .padded {@apply px-4 py-6;} }',
-    html: '<div class="padded">Test</div>'
-  })
-
-  const shorthandEnabled = await Tailwind.compile({
-    css: '@layer utilities { .padded {@apply px-4 py-6;} }',
-    html: '<div class="padded">Test</div>',
-    config: {
-      shorthandCSS: true
-    }
-  })
-
-  t.is(
-    shorthandDisabled.replace(/\s+/g, '').trim(),
-    '.padded{padding-left:1rem;padding-right:1rem;padding-top:1.5rem;padding-bottom:1.5rem}'
-  )
-
-  t.is(
-    shorthandEnabled.replace(/\s+/g, '').trim(),
-    '.padded{padding:1.5rem1rem}'
-  )
-})
diff --git a/test/test-todisk.js b/test/test-todisk.js
index bfbedf5..c831a04 100644
--- a/test/test-todisk.js
+++ b/test/test-todisk.js
@@ -63,8 +63,8 @@ test('outputs files at the correct location', async t => {
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(string.length, 4)
-  t.is(array.length, 4)
+  t.is(string.length, 3)
+  t.is(array.length, 3)
 })
 
 test('outputs files at the correct location if multiple template sources are used', async t => {
@@ -89,7 +89,7 @@ test('outputs files at the correct location if multiple template sources are use
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 7)
+  t.is(files.length, 6)
 })
 
 test('copies all files in the `filetypes` option to destination', async t => {
@@ -107,7 +107,7 @@ test('copies all files in the `filetypes` option to destination', async t => {
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 4)
+  t.is(files.length, 3)
 })
 
 test('outputs files with the correct extension', async t => {
@@ -283,7 +283,7 @@ test('runs the `afterBuild` event', async t => {
 })
 
 test('supports multiple asset paths', async t => {
-  const {files} = await Maizzle.build('maizzle-ci', {
+  await Maizzle.build('maizzle-ci', {
     build: {
       fail: 'silent',
       templates: {
@@ -299,9 +299,9 @@ test('supports multiple asset paths', async t => {
     }
   })
 
-  t.true(files.includes(`${t.context.folder}/extras/foo.bar`))
-  t.true(files.includes(`${t.context.folder}/extras/plaintext.html`))
-  t.false(files.includes(`${t.context.folder}/extras/invalid`))
+  t.true(await fs.pathExists(`${t.context.folder}/extras/foo.bar`))
+  t.true(await fs.pathExists(`${t.context.folder}/extras/plaintext.html`))
+  t.false(await fs.pathExists(`${t.context.folder}/extras/invalid`))
 })
 
 test('warns if a template cannot be rendered and `fail` option is undefined', async t => {
@@ -351,13 +351,48 @@ test('warns if a template cannot be rendered and `fail` option is `silent`', asy
   t.false(files.includes('empty.html'))
 })
 
-test('`templates.source` undefined', async t => {
+test('local server does not compile unwanted file types', async t => {
+  await Maizzle.serve('local', {
+    build: {
+      console: {
+        clear: true
+      },
+      browsersync: {
+        ui: false
+      },
+      templates: {
+        source: 'test/stubs/templates',
+        destination: {
+          path: `${t.context.folder}`
+        }
+      }
+    },
+    events: {
+      beforeCreate(config) {
+        config.foo = 'bar'
+      }
+    }
+  })
+
+  t.true(await fs.pathExists(`${t.context.folder}`))
+  t.true(await fs.pathExists(`${t.context.folder}/2.test`))
+
+  // Tests watching changes to files
+  await fs.outputFile('test/stubs/templates/2.html', '<div class="inline">html modified</div>')
+  t.is(await fs.readFile('test/stubs/templates/2.html', 'utf8'), '<div class="inline">html modified</div>')
+
+  // Don't trigger rebuilds on files not in `filetypes`
+  await fs.outputFile('test/stubs/templates/2.test', 'test')
+  t.is(await fs.readFile('test/stubs/templates/2.test', 'utf8'), 'test')
+})
+
+test('throws if it cannot spin up local development server', async t => {
   await t.throwsAsync(async () => {
-    await Maizzle.build('maizzle-ci')
-  }, {instanceOf: Error})
+    await Maizzle.serve('local', {})
+  }, {instanceOf: TypeError})
 })
 
-test('`templates.source` defined as function (string paths)', async t => {
+test('works with templates.source defined as function (string paths)', async t => {
   const {files} = await Maizzle.build('maizzle-ci', {
     build: {
       fail: 'silent',
@@ -370,10 +405,11 @@ test('`templates.source` defined as function (string paths)', async t => {
     }
   })
 
-  t.is(files.length, 4)
+  t.true(await fs.pathExists(t.context.folder))
+  t.is(files.length, 3)
 })
 
-test('`templates.source` defined as function (array paths)', async t => {
+test('works with templates.source defined as function (array paths)', async t => {
   const {files} = await Maizzle.build('maizzle-ci', {
     build: {
       fail: 'silent',
@@ -389,7 +425,8 @@ test('`templates.source` defined as function (array paths)', async t => {
     }
   })
 
-  t.is(files.length, 4)
+  t.true(await fs.pathExists(t.context.folder))
+  t.is(files.length, 3)
 })
 
 test('throws if templates path is invalid', async t => {
@@ -447,89 +484,10 @@ test('sets config.build.current.path', async t => {
   t.deepEqual(t.context.current, {
     path: {
       root: '',
-      dir: `${t.context.folder}/nested`,
-      base: '3.html',
+      dir: t.context.folder,
+      base: '2.html',
       ext: '.html',
-      name: '3'
+      name: '2'
     }
   })
 })
-
-test('skips compiling templates', async t => {
-  let parsedFilesFromStringSource = 0
-  await Maizzle.build('maizzle-ci', {
-    build: {
-      fail: 'silent',
-      templates: {
-        source: 'test/stubs/templates',
-        skip: '1.html',
-        destination: {
-          path: t.context.folder
-        }
-      }
-    },
-    // Increment counter for each file that was actually parsed
-    events: {
-      afterRender(html) {
-        parsedFilesFromStringSource++
-
-        return html
-      }
-    }
-  })
-
-  let parsedFilesFromArraySource = 0
-  await Maizzle.build('maizzle-ci', {
-    build: {
-      fail: 'silent',
-      templates: {
-        source: ['test/stubs/templates'],
-        skip: ['1.html', 'nested/3.html'],
-        destination: {
-          path: t.context.folder
-        }
-      }
-    },
-    events: {
-      afterRender(html) {
-        parsedFilesFromArraySource++
-
-        return html
-      }
-    }
-  })
-
-  t.is(parsedFilesFromStringSource, 2)
-  t.is(parsedFilesFromArraySource, 1)
-})
-
-test('does not output omitted files', async t => {
-  const {files: fromStringSource} = await Maizzle.build('maizzle-ci', {
-    build: {
-      fail: 'silent',
-      templates: {
-        source: 'test/stubs/templates',
-        omit: '1.html',
-        destination: {
-          path: t.context.folder
-        }
-      }
-    }
-  })
-
-  const {files: fromArraySource} = await Maizzle.build('maizzle-ci', {
-    build: {
-      fail: 'silent',
-      templates: {
-        source: ['test/stubs/templates'],
-        omit: ['1.html', 'nested/3.html'],
-        destination: {
-          path: t.context.folder
-        }
-      }
-    }
-  })
-
-  t.is(fromStringSource.length, 3)
-  t.is(fromArraySource.length, 2)
-})
diff --git a/test/test-tostring.js b/test/test-tostring.js
index 829d1c8..db9a261 100644
--- a/test/test-tostring.js
+++ b/test/test-tostring.js
@@ -154,12 +154,10 @@ test('preserves css in marked style tags (tailwindcss)', async t => {
 })
 
 test('@import css files in marked style tags', async t => {
-  const source = `<style postcss>@import "test/stubs/post.css";</style>`
-  const html = await renderString(source, {
-    maizzle: {
-      shorthandCSS: true
-    }
-  })
+  const source = `<style class="inline" postcss>@import "test/stubs/post.css";</style>`
+  const html = await renderString(source)
 
-  t.is(html.replace(/\n {2,}/g, ''), `<style>div {margin: 1px 2px 3px 4px;\n}</style>`)
+  t.is(html, `<style class="inline">div {
+  margin: 1px 2px 3px 4px;
+}</style>`)
 })
diff --git a/test/test-transformers.js b/test/test-transformers.js
index 9c20999..9cb74b9 100644
--- a/test/test-transformers.js
+++ b/test/test-transformers.js
@@ -162,11 +162,7 @@ test('remove unused CSS', async t => {
   <html>
     <head>
       <style>
-        @media (screen) {
-          .ignore {color: yellow}
-        }
         .foo {color: red}
-        .foo:hover {color: blue}
         .bar-baz {color: blue}
         .baz {color: white}
       </style>
@@ -176,38 +172,70 @@ test('remove unused CSS', async t => {
     </body>
   </html>`
 
-  const withOptionsResult = `<!DOCTYPE html>
+  const enabledResult = `<!DOCTYPE html>
   <html>
     <head>
       <style>
         .foo {color: red}
-        .foo:hover {color: blue}
-        .bar-baz {color: blue}</style>
+      </style>
     </head>
     <body>
       <div class="foo {{ test }}">test div with some text</div>
     </body>
   </html>`
 
-  const unsetResult = `<!DOCTYPE html>
+  const withOptionsResult = `<!DOCTYPE html>
   <html>
     <head>
       <style>
         .foo {color: red}
-        .foo:hover {color: blue}</style>
+        .bar-baz {color: blue}
+      </style>
     </head>
     <body>
       <div class="foo {{ test }}">test div with some text</div>
     </body>
   </html>`
 
+  const enabled = await Maizzle.removeUnusedCSS(html)
   const disabled = await Maizzle.removeUnusedCSS(html, {removeUnusedCSS: false})
   const withOptions = await Maizzle.removeUnusedCSS(html, {whitelist: ['.bar*']})
-  const unset = await Maizzle.removeUnusedCSS(html)
 
+  t.is(enabled, enabledResult)
   t.is(disabled, html)
   t.is(withOptions, withOptionsResult)
-  t.is(unset, unsetResult)
+})
+
+test('remove unused CSS (disabled)', async t => {
+  const html = `<!DOCTYPE html>
+  <html>
+    <head>
+      <style>
+        .foo {color: red}
+      </style>
+    </head>
+    <body>
+      <div class="foo">test div with some text</div>
+    </body>
+  </html>`
+
+  const result = `<!DOCTYPE html>
+  <html>
+    <head>
+      <style>
+        .foo {color: red}
+      </style>
+    </head>
+    <body>
+      <div class="foo">test div with some text</div>
+    </body>
+  </html>`
+
+  const disabled = await Maizzle.removeUnusedCSS(html, {removeUnusedCSS: false})
+  const unset = await Maizzle.removeUnusedCSS(html)
+
+  t.is(disabled, result)
+  t.is(unset, result)
 })
 
 test('remove attributes', async t => {
@@ -246,7 +274,9 @@ test('base URL (object)', async t => {
   const source = await fixture('base-url')
   const html = await Maizzle.applyBaseUrl(source, {
     url: 'https://example.com/',
-    allTags: true
+    allTags: true,
+    styleTag: true,
+    inlineCss: true
   })
 
   t.is(html, await expected('base-url'))
@@ -358,10 +388,7 @@ test('filters (postcss)', async t => {
         margin-bottom: 3px;
         margin-left: 4px;
       }
-    </style>`,
-    {
-      shorthandCSS: true
-    }
+    </style>`
   )
 
   t.is(html.replace(/\n {2,}/g, ''), '<style>div {margin: 1px 2px 3px 4px;}</style>')
@@ -512,9 +539,13 @@ test('remove inlined selectors', async t => {
           color: #00a8ff;
         }
 
-        .padded {
-          padding: 0 20px;
-        }
+        .m-0 {margin: 0}
+
+        .mb-4 {margin-bottom: 16px}
+
+        .mt-0 {margin-top: 0}
+
+        .remove {color: red}
 
         [data-ogsc] .hidden {display: none}
 
@@ -538,10 +569,10 @@ test('remove inlined selectors', async t => {
       </style>
     </head>
     <body>
-      <div no-value id="keepId" class="keep ignore foo-class" style="color: red; display: inline">
-        <h1 class="padded hover-text-blue" style="padding: 0 20px">Title</h1>
+      <div no-value id="keepId" class="remove keep ignore foo-class" style="color: red; display: inline">
+        <h1 class="m-0 mb-4 mt-0 hover-text-blue" style="margin: 0 0 16px;">Title</h1>
         <img src="https://example.com/image.jpg" style="border: 0; vertical-align: middle">
-        <div id="keepId" class="keep ignore" style="color: red; display: inline">text</div>
+        <div id="keepId" class="remove keep ignore" style="color: red; display: inline">text</div>
       </div>
     </body>
   </html>`
@@ -568,10 +599,10 @@ test('remove inlined selectors', async t => {
       <style>.keep {margin: 0}</style>
     </head>
     <body>
-      <div no-value id="keepId" class="keep ignore foo-class" style="color: red; display: inline">
-        <h1 class="hover-text-blue" style="padding: 0 20px">Title</h1>
+      <div no-value id="keepId" class="keep foo-class" style="color: red; display: inline">
+        <h1 class="hover-text-blue" style="margin: 0 0 16px">Title</h1>
         <img src="https://example.com/image.jpg" style="border: 0; vertical-align: middle">
-        <div id="keepId" class="keep ignore" style="color: red; display: inline">text</div>
+        <div id="keepId" class="keep" style="color: red; display: inline">text</div>
       </div>
     </body>
   </html>`