Repository: maizzle/framework (js)

Issue:
Components `folders` config isn't additive contrary to what the docs say
- Maizzle Version: 4.5.0
- Node.js Version: 18.16.0

The documentation for the components `folders` configuration (https://maizzle.com/docs/configuration/components) states that “_The paths you defined will be added to the default folders._” This is not what happens in my experience.

What I did:

1. `npx degit maizzle/maizzle my-project` and `npm i`
2. In `src`, add an `includes` folder with a file named `header.html`.
3. In the `promotional.html` template, add an `<x-header>` tag.
4. In `config.js`, add the following config under `build`:

```js
    components: {
      folders: ['src/includes'],
    },
```
5. `npm run build`

Result: 

```
✖ Failed to compile template: promotional.html
⚠ [components] <x-v-fill> could not find v-fill.html in the defined root paths (src/includes, src/layouts, src/templates, src/includes)
```

The components in `src/components/` can no longer be found. I need to manually add all the default folders for it to work properly.


Pull request description:
fix: object merging
Switched to using a custom function based on `lodash.mergeWith` instead of `lodash.merge`, to avoid overwriting array values when merging objects ([reference](https://github.com/lodash/lodash/issues/5384)). Fixes #1057.

Given this:

```js
merge(
  {foo: ["a", "b", "c"]},
  {foo: ["d"]}
)
```

Previously: 

```js
{
  foo: ["d", "b", "c"]
}
```

Now:

```js
{
  foo: ["a", "b", "c", "d"]
}
```



Patch:
diff --git a/src/commands/serve.js b/src/commands/serve.js
index c869781..17b136b 100644
--- a/src/commands/serve.js
+++ b/src/commands/serve.js
@@ -6,8 +6,8 @@ const Config = require('../generators/config')
 const buildToFile = require('../commands/build')
 const renderToString = require('../functions/render')
 
-const {get, merge, isObject} = require('lodash')
-const {clearConsole} = require('../utils/helpers')
+const {get, isObject} = require('lodash')
+const {clearConsole, merge} = require('../utils/helpers')
 
 /**
  * Initialize Browsersync on-demand
diff --git a/src/generators/config.js b/src/generators/config.js
index 3a61046..934fd5a 100644
--- a/src/generators/config.js
+++ b/src/generators/config.js
@@ -1,6 +1,5 @@
 const path = require('path')
-const {merge} = require('lodash')
-const {requireUncached} = require('../utils/helpers')
+const {merge, requireUncached} = require('../utils/helpers')
 
 module.exports = {
   getMerged: async (env = 'local') => {
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index 878a90d..5856f7b 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -1,7 +1,8 @@
 const path = require('path')
 const fs = require('fs-extra')
 const glob = require('glob-promise')
-const {get, isEmpty, merge} = require('lodash')
+const {get, isEmpty} = require('lodash')
+const {merge} = require('../../utils/helpers')
 
 const Config = require('../config')
 const Tailwind = require('../tailwindcss')
diff --git a/src/generators/output/to-string.js b/src/generators/output/to-string.js
index cd0ad89..2c4bcd0 100644
--- a/src/generators/output/to-string.js
+++ b/src/generators/output/to-string.js
@@ -1,6 +1,7 @@
 const fm = require('front-matter')
-const {get, merge} = require('lodash')
+const {get} = require('lodash')
 const posthtml = require('../posthtml')
+const {merge} = require('../../utils/helpers')
 const Transformers = require('../../transformers')
 const Tailwind = require('../tailwindcss')
 const Config = require('../config')
diff --git a/src/generators/plaintext.js b/src/generators/plaintext.js
index 0461688..4890b56 100644
--- a/src/generators/plaintext.js
+++ b/src/generators/plaintext.js
@@ -1,6 +1,7 @@
 const path = require('path')
+const {get} = require('lodash')
 const posthtml = require('posthtml')
-const {get, merge} = require('lodash')
+const {merge} = require('../utils/helpers')
 const {stripHtml} = require('string-strip-html')
 const defaultConfig = require('./posthtml/defaultConfig')
 
diff --git a/src/generators/posthtml/index.js b/src/generators/posthtml/index.js
index e41f61c..e38b468 100644
--- a/src/generators/posthtml/index.js
+++ b/src/generators/posthtml/index.js
@@ -1,26 +1,28 @@
 const fm = require('front-matter')
 const posthtml = require('posthtml')
-const {get, merge} = require('lodash')
+const {get, omit} = require('lodash')
 const fetch = require('posthtml-fetch')
 const layouts = require('posthtml-extend')
+const {merge} = require('../../utils/helpers')
 const components = require('posthtml-component')
-const defaultConfig = require('./defaultConfig')
+const defaultPosthtmlConfig = require('./defaultConfig')
 const defaultComponentsConfig = require('./defaultComponentsConfig')
 
 module.exports = async (html, config) => {
-  const layoutsOptions = get(config, 'build.layouts', {})
-  const componentsOptions = get(config, 'build.components', {})
+  const posthtmlOptions = merge(defaultPosthtmlConfig, get(config, 'build.posthtml.options', {}))
+  const posthtmlPlugins = get(config, 'build.posthtml.plugins', [])
+
+  const componentsUserOptions = get(config, 'build.components', {})
+
   const expressionsOptions = merge(
     {
       loopTags: ['each', 'for'],
       strictMode: false
     },
-    get(config, 'build.posthtml.expressions', {})
+    get(config, 'build.posthtml.expressions', {}),
+    get(componentsUserOptions, 'expressions', {})
   )
 
-  const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
-  const posthtmlPlugins = get(config, 'build.posthtml.plugins', [])
-
   const locals = merge(
     get(expressionsOptions, 'locals', {}),
     get(config, 'locals', {}),
@@ -30,24 +32,29 @@ module.exports = async (html, config) => {
   const fetchPlugin = fetch(
     merge(
       {
-        expressions: merge({...expressionsOptions, locals})
+        expressions: merge(expressionsOptions, {locals})
       },
       get(config, 'build.posthtml.fetch', {})
     )
   )
 
-  const defaultComponentsOptions = merge(
+  const componentsOptions = merge(
     {
       ...defaultComponentsConfig,
       folders: [
-        ...defaultComponentsConfig.folders,
-        ...get(componentsOptions, 'folders', [])
+        ...get(componentsUserOptions, 'folders', []),
+        ...defaultComponentsConfig.folders
       ],
-      expressions: {...expressionsOptions, locals}
+      expressions: merge(expressionsOptions, {locals})
     },
     {
-      root: componentsOptions.root || './'
-    }
+      root: componentsUserOptions.root || './'
+    },
+    /**
+     * We omit `folders`, `root` and `expressions` in order to prevent duplicate
+     * array values, as they are already added above
+     */
+    omit(componentsUserOptions, ['folders', 'root', 'expressions'])
   )
 
   return posthtml([
@@ -56,17 +63,12 @@ module.exports = async (html, config) => {
       merge(
         {
           strict: false,
-          expressions: merge({...expressionsOptions, locals})
+          expressions: merge(expressionsOptions, {locals})
         },
-        layoutsOptions
-      )
-    ),
-    components(
-      merge(
-        defaultComponentsOptions,
-        componentsOptions
+        get(config, 'build.layouts', {})
       )
     ),
+    components(componentsOptions),
     ...posthtmlPlugins
   ])
     .process(html, {...posthtmlOptions})
diff --git a/src/generators/tailwindcss.js b/src/generators/tailwindcss.js
index 4333b46..2473549 100644
--- a/src/generators/tailwindcss.js
+++ b/src/generators/tailwindcss.js
@@ -3,10 +3,10 @@ const fs = require('fs-extra')
 const postcss = require('postcss')
 const tailwindcss = require('tailwindcss')
 const postcssImport = require('postcss-import')
+const {get, isObject, isEmpty} = require('lodash')
 const postcssNested = require('tailwindcss/nesting')
-const {requireUncached} = require('../utils/helpers')
+const {merge, requireUncached} = require('../utils/helpers')
 const mergeLonghand = require('postcss-merge-longhand')
-const {get, isObject, isEmpty, merge} = require('lodash')
 const defaultComponentsConfig = require('./posthtml/defaultComponentsConfig')
 
 const addImportantPlugin = () => {
diff --git a/src/transformers/attributeToStyle.js b/src/transformers/attributeToStyle.js
index e39a734..274d264 100644
--- a/src/transformers/attributeToStyle.js
+++ b/src/transformers/attributeToStyle.js
@@ -1,7 +1,8 @@
 const posthtml = require('posthtml')
+const {merge} = require('../utils/helpers')
 const parseAttrs = require('posthtml-attrs-parser')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
-const {get, merge, forEach, intersection, keys, isEmpty} = require('lodash')
+const {get, forEach, intersection, keys, isEmpty} = require('lodash')
 
 module.exports = async (html, config = {}, direct = false) => {
   const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
diff --git a/src/transformers/baseUrl.js b/src/transformers/baseUrl.js
index 8eccd7e..269d422 100644
--- a/src/transformers/baseUrl.js
+++ b/src/transformers/baseUrl.js
@@ -1,7 +1,8 @@
 const posthtml = require('posthtml')
 const isUrl = require('is-url-superb')
+const {merge} = require('../utils/helpers')
 const baseUrl = require('posthtml-base-url')
-const {get, merge, isObject, isEmpty} = require('lodash')
+const {get, isObject, isEmpty} = require('lodash')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config = {}, direct = false) => {
diff --git a/src/transformers/extraAttributes.js b/src/transformers/extraAttributes.js
index 2fc8f2a..3b33be8 100644
--- a/src/transformers/extraAttributes.js
+++ b/src/transformers/extraAttributes.js
@@ -1,5 +1,6 @@
 const posthtml = require('posthtml')
-const {get, merge, isObject} = require('lodash')
+const {get, isObject} = require('lodash')
+const {merge} = require('../utils/helpers')
 const addAttributes = require('posthtml-extra-attributes')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/transformers/filters/index.js b/src/transformers/filters/index.js
index ad68f7b..959ba0e 100644
--- a/src/transformers/filters/index.js
+++ b/src/transformers/filters/index.js
@@ -1,5 +1,6 @@
 const posthtml = require('posthtml')
-const {get, omit, has, merge} = require('lodash')
+const {get, omit, has} = require('lodash')
+const {merge} = require('../../utils/helpers')
 const defaultFilters = require('./defaultFilters')
 const PostCSS = require('../../generators/postcss')
 const posthtmlContent = require('posthtml-content')
diff --git a/src/transformers/markdown.js b/src/transformers/markdown.js
index 43397e0..ff44668 100644
--- a/src/transformers/markdown.js
+++ b/src/transformers/markdown.js
@@ -1,5 +1,6 @@
+const {get} = require('lodash')
 const posthtml = require('posthtml')
-const {get, merge} = require('lodash')
+const {merge} = require('../utils/helpers')
 const markdown = require('posthtml-markdownit')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/transformers/posthtmlMso.js b/src/transformers/posthtmlMso.js
index f7b589f..7b1c84c 100644
--- a/src/transformers/posthtmlMso.js
+++ b/src/transformers/posthtmlMso.js
@@ -1,6 +1,7 @@
+const {get} = require('lodash')
 const posthtml = require('posthtml')
-const {get, merge} = require('lodash')
 const outlook = require('posthtml-mso')
+const {merge} = require('../utils/helpers')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config) => {
diff --git a/src/transformers/prettify.js b/src/transformers/prettify.js
index 7106179..f761823 100644
--- a/src/transformers/prettify.js
+++ b/src/transformers/prettify.js
@@ -1,6 +1,7 @@
 /* eslint-disable camelcase */
 const pretty = require('pretty')
-const {get, merge, isEmpty, isObject} = require('lodash')
+const {merge} = require('../utils/helpers')
+const {get, isEmpty, isObject} = require('lodash')
 
 module.exports = async (html, config = {}, direct = false) => {
   const defaultConfig = {
diff --git a/src/transformers/preventWidows.js b/src/transformers/preventWidows.js
index 5a0e3d9..5a88fac 100644
--- a/src/transformers/preventWidows.js
+++ b/src/transformers/preventWidows.js
@@ -1,5 +1,6 @@
 const posthtml = require('posthtml')
-const {get, merge, isEmpty} = require('lodash')
+const {get, isEmpty} = require('lodash')
+const {merge} = require('../utils/helpers')
 const {removeWidows} = require('string-remove-widows')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/transformers/removeAttributes.js b/src/transformers/removeAttributes.js
index b55122b..3654d29 100644
--- a/src/transformers/removeAttributes.js
+++ b/src/transformers/removeAttributes.js
@@ -1,5 +1,6 @@
+const {get} = require('lodash')
 const posthtml = require('posthtml')
-const {get, merge} = require('lodash')
+const {merge} = require('../utils/helpers')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config = {}, direct = false) => {
diff --git a/src/transformers/removeInlineBackgroundColor.js b/src/transformers/removeInlineBackgroundColor.js
index 777f99c..c865337 100644
--- a/src/transformers/removeInlineBackgroundColor.js
+++ b/src/transformers/removeInlineBackgroundColor.js
@@ -1,7 +1,7 @@
 const posthtml = require('posthtml')
-const {get, merge, isEmpty} = require('lodash')
+const {get, isEmpty} = require('lodash')
 const parseAttrs = require('posthtml-attrs-parser')
-const {toStyleString} = require('../utils/helpers')
+const {merge, toStyleString} = require('../utils/helpers')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config = {}, direct = false) => {
diff --git a/src/transformers/removeInlineSizes.js b/src/transformers/removeInlineSizes.js
index 5f497f5..b02b86c 100644
--- a/src/transformers/removeInlineSizes.js
+++ b/src/transformers/removeInlineSizes.js
@@ -1,7 +1,7 @@
 const posthtml = require('posthtml')
-const {get, merge, isEmpty} = require('lodash')
+const {get, isEmpty} = require('lodash')
 const parseAttrs = require('posthtml-attrs-parser')
-const {toStyleString} = require('../utils/helpers')
+const {merge, toStyleString} = require('../utils/helpers')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
 module.exports = async (html, config = {}, direct = false) => {
diff --git a/src/transformers/removeInlinedSelectors.js b/src/transformers/removeInlinedSelectors.js
index 0623d56..534615d 100644
--- a/src/transformers/removeInlinedSelectors.js
+++ b/src/transformers/removeInlinedSelectors.js
@@ -1,6 +1,7 @@
 const postcss = require('postcss')
 const posthtml = require('posthtml')
-const {get, merge, has, remove} = require('lodash')
+const {merge} = require('../utils/helpers')
+const {get, has, remove} = require('lodash')
 const parseAttrs = require('posthtml-attrs-parser')
 const matchHelper = require('posthtml-match-helper')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
diff --git a/src/transformers/removeUnusedCss.js b/src/transformers/removeUnusedCss.js
index 9de85ce..81576a2 100644
--- a/src/transformers/removeUnusedCss.js
+++ b/src/transformers/removeUnusedCss.js
@@ -1,5 +1,6 @@
 const {comb} = require('email-comb')
-const {get, merge, isEmpty, isObject} = require('lodash')
+const {merge} = require('../utils/helpers')
+const {get, isEmpty, isObject} = require('lodash')
 const removeInlinedClasses = require('./removeInlinedSelectors')
 
 module.exports = async (html, config = {}, direct = false) => {
diff --git a/src/transformers/safeClassNames.js b/src/transformers/safeClassNames.js
index 3019316..ad51cda 100644
--- a/src/transformers/safeClassNames.js
+++ b/src/transformers/safeClassNames.js
@@ -1,5 +1,6 @@
 const posthtml = require('posthtml')
-const {get, merge, isEmpty} = require('lodash')
+const {get, isEmpty} = require('lodash')
+const {merge} = require('../utils/helpers')
 const safeClassNames = require('posthtml-safe-class-names')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/transformers/shorthandInlineCSS.js b/src/transformers/shorthandInlineCSS.js
index 45fbb25..4f355c0 100644
--- a/src/transformers/shorthandInlineCSS.js
+++ b/src/transformers/shorthandInlineCSS.js
@@ -1,5 +1,6 @@
 const posthtml = require('posthtml')
-const {get, merge, isObject, isEmpty} = require('lodash')
+const {merge} = require('../utils/helpers')
+const {get, isObject, isEmpty} = require('lodash')
 const mergeInlineLonghand = require('posthtml-postcss-merge-longhand')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/transformers/sixHex.js b/src/transformers/sixHex.js
index 145e0c0..5ea361c 100644
--- a/src/transformers/sixHex.js
+++ b/src/transformers/sixHex.js
@@ -1,5 +1,6 @@
+const {get} = require('lodash')
 const posthtml = require('posthtml')
-const {get, merge} = require('lodash')
+const {merge} = require('../utils/helpers')
 const {conv} = require('color-shorthand-hex-to-six-digit')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/transformers/urlParameters.js b/src/transformers/urlParameters.js
index 56e1629..ef4916a 100644
--- a/src/transformers/urlParameters.js
+++ b/src/transformers/urlParameters.js
@@ -1,5 +1,6 @@
 const posthtml = require('posthtml')
-const {get, merge, isEmpty} = require('lodash')
+const {get, isEmpty} = require('lodash')
+const {merge} = require('../utils/helpers')
 const urlParams = require('posthtml-url-parameters')
 const defaultConfig = require('../generators/posthtml/defaultConfig')
 
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 85684da..cb58e3f 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -1,3 +1,5 @@
+const {mergeWith} = require('lodash')
+
 module.exports = {
   requireUncached: module => {
     try {
@@ -9,5 +11,24 @@ module.exports = {
   },
   // https://github.com/lukeed/console-clear
   clearConsole: () => process.stdout.write('\x1B[H\x1B[2J'),
-  toStyleString: (object = {}) => Object.entries(object).map(([k, v]) => `${k}: ${v}`).join('; ')
+  toStyleString: (object = {}) => Object.entries(object).map(([k, v]) => `${k}: ${v}`).join('; '),
+  merge: (...objects) => {
+    if (objects.length < 2) {
+      return objects[0]
+    }
+
+    let merged = {}
+
+    for (const object of objects) {
+      merged = mergeWith(merged, object, (existingValue, newValue) => {
+        if (Array.isArray(existingValue) && Array.isArray(newValue)) {
+          return [...existingValue, ...newValue]
+        }
+
+        return undefined
+      })
+    }
+
+    return merged
+  }
 }


Test patch:
diff --git a/test/test-misc.js b/test/test-misc.js
index 149cd49..bb31afd 100644
--- a/test/test-misc.js
+++ b/test/test-misc.js
@@ -1,8 +1,29 @@
 const test = require('ava')
 const path = require('path')
-const {requireUncached} = require('../src/utils/helpers')
+const h = require('../src/utils/helpers')
 
 test('requires an uncached module', t => {
-  const helpers = requireUncached(path.resolve(process.cwd(), 'src/utils/helpers'))
+  const helpers = h.requireUncached(path.resolve(process.cwd(), 'src/utils/helpers'))
   t.is(typeof helpers.requireUncached, 'function')
 })
+
+test('merges multiple objects containing arrays', t => {
+  const object1 = {foo: 'bar', arr: [1, 2]}
+  const object2 = {arr: [3, 4], baz: 'qux'}
+  const object3 = {what: 'ever', arr: [5, 6]}
+  const merged = h.merge(object1, object2, object3)
+
+  t.deepEqual(merged, {
+    arr: [1, 2, 3, 4, 5, 6],
+    baz: 'qux',
+    foo: 'bar',
+    what: 'ever'
+  })
+})
+
+test('returns object when less than two objects are provided', t => {
+  const object = {arr: [1, 2]}
+  const merged = h.merge(object)
+
+  t.deepEqual(merged, object)
+})