Repository: maizzle/framework (js)

Issue:
<style postcss> using default tailwind config
Currently, if you add this inside a template:

```html
<block name="head">
  <style postcss>
    @screen sm {
      .foo {
        color: red;
      }
    }
  </style>
</block>
```

... the compiled CSS will use Tailwind's _default_ config instead of the one that our project uses (as defined in the environment config that we ran the build command based on - i.e. in `build.tailwind.config`).

This is because `fromFile()` expects a Tailwind config object:

https://github.com/maizzle/framework/blob/472ece434a9c74955da9093380fc9ae61c59b4c3/src/generators/tailwind.js#L76

... but we're passing it `false` instead:

https://github.com/maizzle/framework/blob/472ece434a9c74955da9093380fc9ae61c59b4c3/src/transformers/transform.js#L10

This could be solved by modifying the transformer to attempt to read the `tailwind.config.js` file set in the environment config, but only if non-object or an empty object is provided. 

We could finally fallback to an empty object, that will use the default Tailwind config.




Pull request:
Refactor tailwind
This PR refactors the Tailwind generator into a single reusable function and fixes #169's root cause, so we don't need to modify that transformer.


Patch:
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index 1b7771f..922f019 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -20,7 +20,7 @@ module.exports = async (env, spinner, config = {}) => {
     })
   }
 
-  const css = await Tailwind.fromFile(config, env)
+  const css = await Tailwind.compile('', '', {}, config)
 
   const sourceDir = getPropValue(config, 'build.templates.root') || 'src/templates'
   const outputDir = getPropValue(config, 'build.destination.path') || `build_${env}`
diff --git a/src/generators/output/to-string.js b/src/generators/output/to-string.js
index 1801838..21406c9 100644
--- a/src/generators/output/to-string.js
+++ b/src/generators/output/to-string.js
@@ -27,7 +27,7 @@ module.exports = async (html, options) => {
   if (typeof getPropValue(options, 'tailwind.compiled') === 'string') {
     config.css = options.tailwind.compiled
   } else {
-    config.css = await Tailwind.fromString(cssString, html, tailwindConfig, config)
+    config.css = await Tailwind.compile(cssString, html, tailwindConfig, config)
   }
 
   if (options && typeof options.beforeRender === 'function') {
diff --git a/src/generators/tailwind.js b/src/generators/tailwind.js
index a3bbe52..ab81eb0 100644
--- a/src/generators/tailwind.js
+++ b/src/generators/tailwind.js
@@ -12,13 +12,13 @@ const {getPropValue, isObject, isEmptyObject, requireUncached} = require('../uti
 const defaultPurgeCSSExtractor = /[\w-/:%.]+(?<!:)/g
 
 module.exports = {
-  fromFile: async (config, env) => {
-    const tailwindConfig = getPropValue(config, 'build.tailwind.config') || 'tailwind.config.js'
-    const tailwindConfigObject = isObject(tailwindConfig) ? {} : requireUncached(path.resolve(process.cwd(), tailwindConfig))
+  compile: async (css = '', html = '', tailwindConfig = {}, maizzleConfig = {}) => {
+    tailwindConfig = (isObject(tailwindConfig) && !isEmptyObject(tailwindConfig)) ? tailwindConfig : getPropValue(maizzleConfig, 'build.tailwind.config') || 'tailwind.config.js'
+    const tailwindConfigObject = (isObject(tailwindConfig) && !isEmptyObject(tailwindConfig)) ? tailwindConfig : requireUncached(path.resolve(process.cwd(), tailwindConfig))
 
-    const purgeCSSOptions = getPropValue(config, 'purgeCSS') || {}
+    const purgeCSSOptions = getPropValue(maizzleConfig, 'purgeCSS') || {}
 
-    const templatesRoot = getPropValue(config, 'build.templates.root')
+    const templatesRoot = getPropValue(maizzleConfig, 'build.templates.root')
 
     const templateSources = Array.isArray(templatesRoot) ? templatesRoot.map(item => `${item}/**/*.*`) : [`./${templatesRoot}/**/*.*`]
     const tailwindSources = Array.isArray(tailwindConfigObject.purge) ? tailwindConfigObject.purge : (isObject(tailwindConfigObject.purge) ? tailwindConfigObject.purge.content || [] : [])
@@ -30,28 +30,22 @@ module.exports = {
       'src/components/**/*.*',
       ...templateSources,
       ...tailwindSources,
-      ...extraPurgeSources
+      ...extraPurgeSources,
+      {raw: html}
     ]
 
     const extractor = getPropValue(tailwindConfigObject, 'purge.options.extractor') || purgeCSSOptions.extractor || defaultPurgeCSSExtractor
     const purgeWhitelist = getPropValue(tailwindConfigObject, 'purge.options.whitelist') || purgeCSSOptions.whitelist || []
     const purgewhitelistPatterns = getPropValue(tailwindConfigObject, 'purge.options.whitelistPatterns') || purgeCSSOptions.whitelistPatterns || []
 
-    const purgeCssPlugin = env === 'local' ? () => {} : purgecss({
+    const purgeCssPlugin = maizzleConfig.env === 'local' ? () => {} : purgecss({
       content: purgeSources,
       defaultExtractor: content => content.match(extractor) || [],
       whitelist: purgeWhitelist,
       whitelistPatterns: purgewhitelistPatterns
     })
 
-    const mergeLonghandPlugin = env === 'local' ? () => {} : mergeLonghand()
-
-    const postcssUserPlugins = getPropValue(config, 'build.postcss.plugins') || []
-
-    const userFilePath = getPropValue(config, 'build.tailwind.css')
-
-    const cssString = await fs.pathExists(userFilePath)
-      .then(exists => exists ? fs.readFile(path.resolve(userFilePath), 'utf8') : '@tailwind components; @tailwind utilities;')
+    const mergeLonghandPlugin = maizzleConfig.env === 'local' ? () => {} : mergeLonghand()
 
     const tailwindPlugin = isEmptyObject(tailwindConfigObject) ? tailwind() : tailwind({
       target: 'ie11',
@@ -61,57 +55,29 @@ module.exports = {
       }
     })
 
-    return postcss([
-      atImport({path: userFilePath ? path.dirname(userFilePath) : []}),
-      postcssNested(),
-      tailwindPlugin,
-      purgeCssPlugin,
-      mqpacker({sort: true}),
-      mergeLonghandPlugin,
-      ...postcssUserPlugins
-    ])
-      .process(cssString, {from: undefined})
-      .then(result => result.css)
-  },
-  fromString: async (css, html, tailwindConfig = {}, maizzleConfig = {}) => {
-    const extractor = getPropValue(tailwindConfig, 'purge.options.extractor') || getPropValue(maizzleConfig, 'purgeCSS.extractor') || defaultPurgeCSSExtractor
-    const purgeWhitelist = getPropValue(tailwindConfig, 'purge.options.whitelist') || getPropValue(maizzleConfig, 'purgeCSS.whitelist') || []
-    const purgewhitelistPatterns = getPropValue(tailwindConfig, 'purge.options.whitelistPatterns') || getPropValue(maizzleConfig, 'purgeCSS.whitelistPatterns') || []
-
-    const tailwindSources = Array.isArray(tailwindConfig.purge) ? tailwindConfig.purge : (isObject(tailwindConfig.purge) ? tailwindConfig.purge.content || [] : [])
-    const purgeContent = getPropValue(maizzleConfig, 'purgeCSS.content') || []
-
     const postcssUserPlugins = getPropValue(maizzleConfig, 'build.postcss.plugins') || []
 
-    const tailwindPlugin = tailwind({
-      target: 'ie11',
-      ...tailwindConfig,
-      purge: {
-        enabled: false
+    const userFilePath = getPropValue(maizzleConfig, 'build.tailwind.css')
+
+    css = await fs.pathExists(userFilePath).then(async exists => {
+      if (exists) {
+        const userFileCSS = await fs.readFile(path.resolve(userFilePath), 'utf8')
+        return css + userFileCSS
       }
+
+      return `@tailwind components;\n ${css}\n @tailwind utilities;`
     })
 
     return postcss([
+      atImport({path: userFilePath ? path.dirname(userFilePath) : []}),
       postcssNested(),
       tailwindPlugin,
-      purgecss({
-        content: [
-          ...tailwindSources,
-          ...purgeContent,
-          {raw: html}
-        ],
-        defaultExtractor: content => content.match(extractor) || [],
-        whitelist: purgeWhitelist,
-        whitelistPatterns: purgewhitelistPatterns
-      }),
-      mqpacker(),
-      mergeLonghand(),
+      purgeCssPlugin,
+      mqpacker({sort: true}),
+      mergeLonghandPlugin,
       ...postcssUserPlugins
     ])
       .process(css, {from: undefined})
       .then(result => result.css)
-      .catch(error => {
-        throw error
-      })
   }
 }
diff --git a/src/transformers/transform.js b/src/transformers/transform.js
index 455f6cb..264920b 100644
--- a/src/transformers/transform.js
+++ b/src/transformers/transform.js
@@ -7,7 +7,7 @@ module.exports = async (html, config) => {
   const replacements = config.transform || {}
   const options = getPropValue(config, 'build.posthtml.options') || {}
 
-  replacements.postcss = css => Tailwind.fromString(css, html, false, config)
+  replacements.postcss = css => Tailwind.compile(css, html, {}, config)
 
   return posthtml([posthtmlContent(replacements)]).process(html, options).then(result => result.html)
 }
diff --git a/xo.config.js b/xo.config.js
index 2e6589e..4c081ce 100644
--- a/xo.config.js
+++ b/xo.config.js
@@ -2,6 +2,7 @@ module.exports = {
   space: true,
   rules: {
     semi: 0,
+    complexity: 0,
     'unicorn/string-content': 0,
     'promise/prefer-await-to-then': 0,
     'unicorn/no-abusive-eslint-disable': 0,


Test patch:
diff --git a/test/expected/email-comb.html b/test/expected/email-comb.html
index dff571c..4c1f224 100644
--- a/test/expected/email-comb.html
+++ b/test/expected/email-comb.html
@@ -1,10 +1,11 @@
 <!DOCTYPE html>
 <html>
 <head>
-    <style> .text-center {
-  text-align: center
-} .w-full {
+    <style>.w-full {
   width: 100%
+}
+.text-center {
+  text-align: center
 }
       /*
         @tab Page
diff --git a/test/expected/inline.html b/test/expected/inline.html
index 5bc46f2..bcc76e4 100644
--- a/test/expected/inline.html
+++ b/test/expected/inline.html
@@ -2,36 +2,54 @@
 <html>
 <head>
 
-    <style>.cursor-move {
-  cursor: move
-} .table {
-  display: table
-} .h-full {
-  height: 100%
-} .mx-2 {
-  margin-left: 0.5rem;
-  margin-right: 0.5rem
-} .my-4 {
-  margin-top: 1rem;
-  margin-bottom: 1rem
-} .px-2 {
+    <style>.w-full {
+  width: 100%
+}
+
+.align-middle {
+  vertical-align: middle
+}
+
+.text-center {
+  text-align: center
+}
+
+.px-2 {
   padding-left: 0.5rem;
   padding-right: 0.5rem
-} .py-4 {
+}
+
+.py-4 {
   padding-top: 1rem;
   padding-bottom: 1rem
-} .text-center {
-  text-align: center
-} .align-middle {
-  vertical-align: middle
-} .w-full {
-  width: 100%
+}
+
+.mx-2 {
+  margin-left: 0.5rem;
+  margin-right: 0.5rem
+}
+
+.my-4 {
+  margin-top: 1rem;
+  margin-bottom: 1rem
+}
+
+.h-full {
+  height: 100%
+}
+
+.table {
+  display: table
+}
+
+.cursor-move {
+  cursor: move
 }</style>
   </head>
 <body>
-  <table class="w-full cursor-move text-center" style="text-align: center; width: 100%;" width="100%" align="center" cellpadding="0" cellspacing="0" role="presentation">
+  <table class="w-full cursor-move text-center" style="width: 100%; text-align: center;" width="100%" align="center" cellpadding="0" cellspacing="0" role="presentation">
     <tr>
-      <td class="h-full align-middle" style="height: 100%; vertical-align: middle;" height="100%">Test</td>
+      <td class="h-full align-middle" style="vertical-align: middle; height: 100%;" height="100%">Test</td>
     </tr>
     <tr>
       <td class="px-2 py-4" style="padding: 1rem 0.5rem;">
diff --git a/test/expected/minify.html b/test/expected/minify.html
index ee266dc..ac225a2 100644
--- a/test/expected/minify.html
+++ b/test/expected/minify.html
@@ -1,2 +1,2 @@
-<!DOCTYPE html><html><head><style>.table{display:table}.w-full{width:100%}</style></head><body>
+<!DOCTYPE html><html><head><style>.w-full{width:100%}.table{display:table}</style></head><body>
 <table class="w-full" cellpadding="0" cellspacing="0" role="presentation"><tr><td>Test</td></tr></table></body></html>
diff --git a/test/expected/prettify.html b/test/expected/prettify.html
index 071b492..8f262eb 100644
--- a/test/expected/prettify.html
+++ b/test/expected/prettify.html
@@ -4,13 +4,13 @@
   <head>
 
     <style>
-      .table {
-        display: table
-      }
-
       .w-full {
         width: 100%
       }
+
+      .table {
+        display: table
+      }
     </style>
   </head>
 
diff --git a/test/fixtures/transform.html b/test/fixtures/transform.html
index e4d5954..cf0e7c0 100644
--- a/test/fixtures/transform.html
+++ b/test/fixtures/transform.html
@@ -6,4 +6,4 @@
   }
 </style>
 
-<div class="foo" uppercase>Lorem ipsum dolor sit amet</div>
+<div class="foo" upper>Lorem ipsum dolor sit amet</div>
diff --git a/test/stubs/main.css b/test/stubs/main.css
new file mode 100644
index 0000000..58b3c79
--- /dev/null
+++ b/test/stubs/main.css
@@ -0,0 +1,5 @@
+@import "tailwindcss/utilities";
+
+.foo {
+  color: red;
+}
diff --git a/test/test-tailwind.js b/test/test-tailwind.js
index f9aca94..9b8f8b2 100644
--- a/test/test-tailwind.js
+++ b/test/test-tailwind.js
@@ -1,21 +1,34 @@
 const test = require('ava')
 const Tailwind = require('../src/generators/tailwind')
 
-test('throws if CSS provided to fromString is not a CSS string', async t => {
-  await t.throwsAsync(async () => {
-    await Tailwind.fromString(null, '<div class="text-center">Test</a>', {}, {})
-  }, {instanceOf: Error, message: 'PostCSS received null instead of CSS string'})
-})
-
-test('uses default Tailwind if no config specified', async t => {
+test('uses Tailwind defaults if no config specified', async t => {
   const config = {
+    env: 'node',
     purgeCSS: {
       content: [{raw: '<div class="xl:z-0"></div>'}]
     }
   }
-  const css = await Tailwind.fromFile(config)
+
+  const css = await Tailwind.compile('', '', {}, config)
+
   t.not(css, undefined)
-  t.is(css, '@media (min-width: 1280px) { .xl\\:z-0 {\n    z-index: 0\n  }\n}')
+  t.is(css, '@media (min-width: 1280px) {\n\n  .xl\\:z-0 {\n    z-index: 0\n  }\n}')
+})
+
+test('uses CSS file provided in environment config', async t => {
+  const config = {
+    env: 'node',
+    build: {
+      tailwind: {
+        css: './test/stubs/main.css'
+      }
+    }
+  }
+
+  const css = await Tailwind.compile('', '<div class="text-center foo">test</div>', {}, config)
+
+  t.not(css, undefined)
+  t.is(css, '.text-center {\n  text-align: center;\n}\n\n.foo {\n  color: red;\n}\n')
 })
 
 test('uses purgeCSS options provided in the config', async t => {
@@ -25,12 +38,15 @@ test('uses purgeCSS options provided in the config', async t => {
       whitelist: ['z-10']
     }
   }
-  const css = await Tailwind.fromFile(config, 'production')
-  t.is(css, '.z-0 {\n  z-index: 0\n} .z-10 {\n  z-index: 10\n}')
+
+  const css = await Tailwind.compile('', '', {}, config)
+
+  t.is(css, '.z-0 {\n  z-index: 0\n}\n\n.z-10 {\n  z-index: 10\n}')
 })
 
 test('uses postcss plugins from the config when compiling from string', async t => {
   const maizzleConfig = {
+    env: 'node',
     build: {
       postcss: {
         plugins: [
@@ -39,26 +55,9 @@ test('uses postcss plugins from the config when compiling from string', async t
       }
     }
   }
-  const css = await Tailwind.fromString('.test {transform: scale(0.5)}', '<div class="test">Test</a>', {}, maizzleConfig)
-  t.not(css, undefined)
-  t.is(css, '.test {\n  -webkit-transform: scale(0.5);\n      -ms-transform: scale(0.5);\n          transform: scale(0.5)\n}')
-})
 
-test('uses postcss plugins from the config when compiling from file', async t => {
-  const config = {
-    build: {
-      postcss: {
-        plugins: [
-          require('autoprefixer')({overrideBrowserslist: ['> 0.1%']})
-        ]
-      }
-    },
-    purgeCSS: {
-      content: [{raw: '<div class="object-fill"></div>'}]
-    }
-  }
-  const css = await Tailwind.fromFile(config)
+  const css = await Tailwind.compile('.test {transform: scale(0.5)}', '<div class="test">Test</a>', {}, maizzleConfig)
 
   t.not(css, undefined)
-  t.is(css, '.object-fill {\n  -o-object-fit: fill;\n     object-fit: fill\n}')
+  t.is(css, '.test {\n  -webkit-transform: scale(0.5);\n      -ms-transform: scale(0.5);\n          transform: scale(0.5)\n}')
 })
diff --git a/test/test-transformers.js b/test/test-transformers.js
index 4267ca4..7009c61 100644
--- a/test/test-transformers.js
+++ b/test/test-transformers.js
@@ -173,7 +173,7 @@ test('six digit hex', t => {
 test('transform contents', t => {
   return processFile(t, 'transform', maizzleConfig({
     transform: {
-      uppercase: text => text.toUpperCase()
+      upper: text => text.toUpperCase()
     }
   }))
 })