Repository: maizzle/framework
Pull request #890 (2023-02-04 15:48:52)

Refactor Tailwind CSS generator to accept parameters through an object
This PR refactors the Tailwind CSS generator function to accept a single object as a parameter, fixing a bug where your custom Tailwind config object was not being used when rendering emails through the Maizzle API.

To give an example, the following would have generated `bg-emerald-200` as `#a7f3d0` instead of `emerald` like defined in the custom Tailwind config object passed to the `render` function:

```js
const Maizzle = require('@maizzle/framework')

let template = `---
title: 'Hello World'
---

<x-main>
  <fill:template>
    <div class="bg-emerald-200">
		Sample
	</div>
  </fill:template>
</x-main>
`

const tailwindConfig = {
  theme: {
    extend: {
      colors: {
        emerald: {
          200: 'emerald',
        },
      },
    },
  },
  corePlugins: {
    preflight: false,
    backgroundOpacity: false,
  },
}

async function main() {
  const { html } = await Maizzle.render(template, {
    maizzle: {
      inlineCSS: true,
      removeUnusedCSS: true,
    },
    tailwind: {
      config: tailwindConfig
    },
  })

  console.log(html)
}

main()
```


Patch:
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index fedbc12..a9612a6 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -28,7 +28,7 @@ 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
   await asyncForEach(templatesConfig, async templateConfig => {
@@ -209,12 +209,16 @@ module.exports = async (env, spinner, config) => {
           if (Array.isArray(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))
             }
           }
 
diff --git a/src/generators/output/to-string.js b/src/generators/output/to-string.js
index 3a90e31..f3a2645 100644
--- a/src/generators/output/to-string.js
+++ b/src/generators/output/to-string.js
@@ -22,9 +22,6 @@ 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}`
@@ -34,7 +31,17 @@ module.exports = async (html, options) => {
   if (typeof get(options, 'tailwind.compiled') === 'string') {
     config.css = options.tailwind.compiled
   } else {
-    config.css = await Tailwind.compile(cssString, html, tailwindConfig, config)
+    config.css = await Tailwind.compile({
+      css: get(options, 'tailwind.css', ''),
+      html,
+      config: merge(config, {
+        build: {
+          tailwind: {
+            config: get(options, 'tailwind.config', {})
+          }
+        }
+      })
+    })
   }
 
   if (options && typeof options.beforeRender === 'function') {
diff --git a/src/generators/tailwindcss.js b/src/generators/tailwindcss.js
index 34c4a2f..e89594f 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 = '', tailwindConfig = {}, maizzleConfig = {}) => {
-    tailwindConfig = (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) ? tailwindConfig : get(maizzleConfig, 'build.tailwind.config', 'tailwind.config.js')
-
+  compile: async ({css = '', html = '', config = {}}) => {
     // Compute the Tailwind config to use
-    const userConfig = () => {
+    const userConfig = config => {
+      const tailwindUserConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
+
       // If a custom config object was passed, use that
-      if (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) {
-        return tailwindConfig
+      if (isObject(tailwindUserConfig) && !isEmpty(tailwindUserConfig)) {
+        return tailwindUserConfig
       }
 
       /**
@@ -24,17 +24,17 @@ module.exports = {
        * This will use the default Tailwind config (with rem units etc)
        */
       try {
-        return requireUncached(path.resolve(process.cwd(), tailwindConfig))
+        return requireUncached(path.resolve(process.cwd(), tailwindUserConfig))
       } catch {
         return {}
       }
     }
 
     // Merge user's Tailwind config on top of a 'base' config
-    const layoutsRoot = get(maizzleConfig, 'build.layouts.root')
-    const componentsRoot = get(maizzleConfig, 'build.components.root')
+    const layoutsRoot = get(config, 'build.layouts.root')
+    const componentsRoot = get(config, 'build.components.root')
 
-    const config = merge({
+    const tailwindConfig = merge({
       important: true,
       content: {
         files: [
@@ -47,20 +47,20 @@ module.exports = {
           {raw: html, extension: 'html'}
         ]
       }
-    }, userConfig())
+    }, userConfig(config))
 
     // Add back the `{raw: html}` option if user provided own config
-    if (Array.isArray(config.content)) {
-      config.content = {
+    if (Array.isArray(tailwindConfig.content)) {
+      tailwindConfig.content = {
         files: [
-          ...config.content,
+          ...tailwindConfig.content,
           {raw: html, extension: 'html'}
         ]
       }
     }
 
     // Include all `build.templates.source` paths when scanning for selectors to preserve
-    const buildTemplates = get(maizzleConfig, 'build.templates')
+    const buildTemplates = get(config, '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(maizzleConfig)
+          const sources = source(config)
 
           if (Array.isArray(sources)) {
-            sources.map(s => config.content.files.push(s))
+            sources.map(s => tailwindConfig.content.files.push(s))
           } else if (typeof sources === 'string') {
-            config.content.files.push(sources)
+            tailwindConfig.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))) {
-          config.content.files.push(source)
+          tailwindConfig.content.files.push(source)
 
           return {raw: '', extension: 'html'}
         }
@@ -90,23 +90,24 @@ module.exports = {
         return `${source}/**/*.*`
       })
 
-      config.content.files.push(...templateSources)
+      tailwindConfig.content.files.push(...templateSources)
     }
 
-    const userFilePath = get(maizzleConfig, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
+    const userFilePath = get(config, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
     const userFileExists = await fs.pathExists(userFilePath)
 
     const toProcess = [
       postcssNested(),
-      tailwindcss(config),
-      get(maizzleConfig, 'shorthandCSS', get(maizzleConfig, 'shorthandInlineCSS')) === true ?
+      tailwindcss(tailwindConfig),
+      get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS')) === true ?
         mergeLonghand() :
         () => {},
-      ...get(maizzleConfig, 'build.postcss.plugins', [])
+      ...get(config, '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/filters/index.js b/src/transformers/filters/index.js
index f7f94d2..39bdbd1 100644
--- a/src/transformers/filters/index.js
+++ b/src/transformers/filters/index.js
@@ -22,10 +22,19 @@ 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, tailwindConfig, maizzleConfig)
+  filters.tailwindcss = css => Tailwind.compile({
+    css,
+    html,
+    config: merge({
+      build: {
+        tailwind: {
+          config: get(config, 'build.tailwind.config', {})
+        }
+      }
+    }, maizzleConfig)
+  })
 
   const posthtmlPlugins = [
     styleDataEmbed(),


Test patch:
diff --git a/test/test-tailwindcss.js b/test/test-tailwindcss.js
index f17ca89..ae81030 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('div {@apply inexistent;}', '<div class="inline">Test</a>', {}, {})
+    await Tailwind.compile({
+      css: 'div {@apply inexistent;}',
+      html: '<div class="inline">Test</a>'
+    })
   }, {instanceOf: SyntaxError})
 })
 
 test('uses defaults if no config specified', async t => {
-  const css = await Tailwind.compile(
-    '@tailwind utilities;',
-    '<p class="xl:z-0"></p>',
-    {},
-    {env: 'maizzle-ci'}
-  )
+  const css = await Tailwind.compile({
+    css: '@tailwind utilities;',
+    html: '<p class="xl:w-1"></p>'
+  })
 
   t.not(css, undefined)
-  t.true(css.includes('.xl\\:z-0'))
+  t.true(css.includes('@media (min-width: 1280px)'))
 })
 
 test('uses css file provided in environment config', async t => {
   const config = {
-    env: 'maizzle-ci',
     build: {
       tailwind: {
         css: './test/stubs/main.css'
@@ -29,7 +29,10 @@ test('uses css file provided in environment config', async t => {
     }
   }
 
-  const css = await Tailwind.compile('', '<div class="text-center foo">test</div>', {}, config)
+  const css = await Tailwind.compile({
+    html: '<div class="text-center foo">test</div>',
+    config
+  })
 
   t.not(css, undefined)
   t.true(css.includes('.text-center'))
@@ -37,68 +40,69 @@ test('uses css file provided in environment config', async t => {
 })
 
 test('works with custom `content` sources', async t => {
-  const css = await Tailwind.compile(
-    '@tailwind utilities;',
-    '<div class="hidden"></div>',
-    {
-      content: ['./test/stubs/tailwind/*.*']
+  const css = await Tailwind.compile({
+    config: {
+      build: {
+        tailwind: {
+          config: {
+            content: ['./test/stubs/tailwind/*.html']
+          }
+        }
+      }
     }
-  )
+  })
 
   t.true(css.includes('.hidden'))
 })
 
 test('works with custom `files` sources', async t => {
-  const css = await Tailwind.compile(
-    '@tailwind utilities;',
-    '<div class="inline"></div>',
-    {
-      content: {
-        files: ['./test/stubs/tailwind/*.*']
+  const css = await Tailwind.compile({
+    config: {
+      build: {
+        tailwind: {
+          config: {
+            content: {
+              files: ['./test/stubs/tailwind/*.html']
+            }
+          }
+        }
       }
     }
-  )
+  })
 
   t.true(css.includes('.hidden'))
 })
 
 test('uses maizzle template path as content source', async t => {
-  const css = await Tailwind.compile(
-    '@tailwind utilities;',
-    '<div class="inline"></div>',
-    {},
-    {
+  const css = await Tailwind.compile({
+    config: {
       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(
-    '@tailwind utilities;',
-    '<div class="inline"></div>',
-    {},
-    {
+  const css = await Tailwind.compile({
+    config: {
       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 maizzleConfig = {
-    env: 'maizzle-ci',
+  const config = {
     build: {
       postcss: {
         plugins: [
@@ -108,29 +112,29 @@ test('uses custom postcss plugins from the maizzle config', async t => {
     }
   }
 
-  const css = await Tailwind.compile('.test {transform: scale(0.5)}', '<div class="test inline">Test</div>', {}, maizzleConfig)
+  const css = await Tailwind.compile({
+    css: '.test {transform: scale(0.5)}',
+    html: '<div class="test inline">Test</div>',
+    config
+  })
 
   t.not(css, undefined)
   t.is(css.trim(), '.inline {display: inline !important} .test {-webkit-transform: scale(0.5);transform: scale(0.5)}')
 })
 
-test('respects `shorthandInlineCSS` in maizzle config', async t => {
-  const shorthandDisabled = await Tailwind.compile(
-    '@layer utilities { .padded {@apply px-4 py-6;} }',
-    '<div class="padded">Test</div>',
-    {},
-    {env: 'maizzle-ci'}
-  )
+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(
-    '@layer utilities { .padded {@apply px-4 py-6;} }',
-    '<div class="padded">Test</div>',
-    {},
-    {
-      env: 'maizzle-ci',
+  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(),