Repository: tailwindlabs/tailwindcss
Pull request #5390 (2021-09-04 15:20:43)

Remove lodash
This PR will get rid of the `lodash` dependency since it is rather big. I replaced most of the functions with plain built-ins. I've added a few extra utilities to mimic lodash' behaviour.
Some files (like the `resolveConfig`) can use some refactoring, however I tried to mainly focus on `lodash`'s removal.


Patch:
diff --git a/defaultConfig.js b/defaultConfig.js
index 820ae3e1..0a131abc 100644
--- a/defaultConfig.js
+++ b/defaultConfig.js
@@ -1,4 +1,4 @@
-const cloneDeep = require('lodash/cloneDeep')
-const defaultConfig = require('./stubs/defaultConfig.stub.js')
+let { cloneDeep } = require('./src/util/cloneDeep')
+let defaultConfig = require('./stubs/defaultConfig.stub.js')
 
 module.exports = cloneDeep(defaultConfig)
diff --git a/defaultTheme.js b/defaultTheme.js
index 70e74af7..4a42a998 100644
--- a/defaultTheme.js
+++ b/defaultTheme.js
@@ -1,4 +1,4 @@
-const cloneDeep = require('lodash/cloneDeep')
-const defaultConfig = require('./stubs/defaultConfig.stub.js')
+let { cloneDeep } = require('./src/util/cloneDeep')
+let defaultConfig = require('./stubs/defaultConfig.stub.js')
 
 module.exports = cloneDeep(defaultConfig.theme)
diff --git a/package-lock.json b/package-lock.json
index 4becce56..678a8e84 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,6 @@
   "requires": true,
   "packages": {
     "": {
-      "name": "tailwindcss",
       "version": "2.2.9",
       "license": "MIT",
       "dependencies": {
@@ -20,7 +19,6 @@
         "fast-glob": "^3.2.7",
         "glob-parent": "^6.0.1",
         "is-glob": "^4.0.1",
-        "lodash": "^4.17.21",
         "normalize-path": "^3.0.0",
         "object-hash": "^2.2.0",
         "postcss-js": "^3.0.3",
@@ -7031,7 +7029,8 @@
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
     },
     "node_modules/lodash.clonedeep": {
       "version": "4.5.0",
@@ -15617,7 +15616,8 @@
     "lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
     },
     "lodash.clonedeep": {
       "version": "4.5.0",
diff --git a/package.json b/package.json
index 218d320b..bcf259ee 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,6 @@
     "fast-glob": "^3.2.7",
     "glob-parent": "^6.0.1",
     "is-glob": "^4.0.1",
-    "lodash": "^4.17.21",
     "normalize-path": "^3.0.0",
     "object-hash": "^2.2.0",
     "postcss-js": "^3.0.3",
diff --git a/src/featureFlags.js b/src/featureFlags.js
index 520c8797..957c079c 100644
--- a/src/featureFlags.js
+++ b/src/featureFlags.js
@@ -1,19 +1,18 @@
-import _ from 'lodash'
 import chalk from 'chalk'
 import log from './util/log'
 
 const featureFlags = {
   future: [],
-  experimental: [],
+  experimental: ['optimizeUniversalDefaults'],
 }
 
 export function flagEnabled(config, flag) {
   if (featureFlags.future.includes(flag)) {
-    return config.future === 'all' || _.get(config, ['future', flag], false)
+    return config.future === 'all' || (config?.future?.[flag] ?? false)
   }
 
   if (featureFlags.experimental.includes(flag)) {
-    return config.experimental === 'all' || _.get(config, ['experimental', flag], false)
+    return config.experimental === 'all' || (config?.experimental?.[flag] ?? false)
   }
 
   return false
@@ -24,7 +23,7 @@ function experimentalFlagsEnabled(config) {
     return featureFlags.experimental
   }
 
-  return Object.keys(_.get(config, 'experimental', {})).filter(
+  return Object.keys(config?.experimental ?? {}).filter(
     (flag) => featureFlags.experimental.includes(flag) && config.experimental[flag]
   )
 }
diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js
index 0e2faa0f..fb3474b2 100644
--- a/src/lib/evaluateTailwindFunctions.js
+++ b/src/lib/evaluateTailwindFunctions.js
@@ -1,15 +1,20 @@
-import _ from 'lodash'
+import dlv from 'dlv'
 import didYouMean from 'didyoumean'
 import transformThemeValue from '../util/transformThemeValue'
 import parseValue from 'postcss-value-parser'
 import buildMediaQuery from '../util/buildMediaQuery'
+import { toPath } from '../util/toPath'
+
+function isObject(input) {
+  return typeof input === 'object' && input !== null
+}
 
 function findClosestExistingPath(theme, path) {
-  const parts = _.toPath(path)
+  let parts = toPath(path)
   do {
     parts.pop()
 
-    if (_.hasIn(theme, parts)) break
+    if (dlv(theme, parts) !== undefined) break
   } while (parts.length)
 
   return parts.length ? parts : undefined
@@ -32,20 +37,22 @@ function listKeys(obj) {
 }
 
 function validatePath(config, path, defaultValue) {
-  const pathString = Array.isArray(path) ? pathToString(path) : _.trim(path, `'"`)
-  const pathSegments = Array.isArray(path) ? path : _.toPath(pathString)
-  const value = _.get(config.theme, pathString, defaultValue)
+  const pathString = Array.isArray(path)
+    ? pathToString(path)
+    : path.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
+  const pathSegments = Array.isArray(path) ? path : toPath(pathString)
+  const value = dlv(config.theme, pathString, defaultValue)
 
-  if (typeof value === 'undefined') {
+  if (value === undefined) {
     let error = `'${pathString}' does not exist in your theme config.`
     const parentSegments = pathSegments.slice(0, -1)
-    const parentValue = _.get(config.theme, parentSegments)
+    const parentValue = dlv(config.theme, parentSegments)
 
-    if (_.isObject(parentValue)) {
+    if (isObject(parentValue)) {
       const validKeys = Object.keys(parentValue).filter(
         (key) => validatePath(config, [...parentSegments, key]).isValid
       )
-      const suggestion = didYouMean(_.last(pathSegments), validKeys)
+      const suggestion = didYouMean(pathSegments[pathSegments.length - 1], validKeys)
       if (suggestion) {
         error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?`
       } else if (validKeys.length > 0) {
@@ -56,8 +63,8 @@ function validatePath(config, path, defaultValue) {
     } else {
       const closestPath = findClosestExistingPath(config.theme, pathString)
       if (closestPath) {
-        const closestValue = _.get(config.theme, closestPath)
-        if (_.isObject(closestValue)) {
+        const closestValue = dlv(config.theme, closestPath)
+        if (isObject(closestValue)) {
           error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys(
             closestValue
           )}`
@@ -87,7 +94,7 @@ function validatePath(config, path, defaultValue) {
   ) {
     let error = `'${pathString}' was found but does not resolve to a string.`
 
-    if (_.isObject(value)) {
+    if (isObject(value)) {
       let validKeys = Object.keys(value).filter(
         (key) => validatePath(config, [...pathSegments, key]).isValid
       )
@@ -165,7 +172,7 @@ export default function ({ tailwindConfig: config }) {
       return value
     },
     screen: (node, screen) => {
-      screen = _.trim(screen, `'"`)
+      screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
 
       if (config.theme.screens[screen] === undefined) {
         throw node.error(`The '${screen}' screen does not exist in your theme.`)
diff --git a/src/lib/resolveDefaultsAtRules.js b/src/lib/resolveDefaultsAtRules.js
index 0727aa9e..95887b35 100644
--- a/src/lib/resolveDefaultsAtRules.js
+++ b/src/lib/resolveDefaultsAtRules.js
@@ -1,5 +1,6 @@
 import postcss from 'postcss'
 import selectorParser from 'postcss-selector-parser'
+import { flagEnabled } from '../featureFlags'
 
 function minimumImpactSelector(nodes) {
   let pseudos = nodes.filter((n) => n.type === 'pseudo')
@@ -51,7 +52,7 @@ function extractElementSelector(selector) {
   return cache.get(selector)
 }
 
-export default function resolveDefaultsAtRules() {
+export default function resolveDefaultsAtRules({ tailwindConfig }) {
   return (root) => {
     let variableNodeMap = new Map()
     let universals = new Set()
@@ -90,9 +91,11 @@ export default function resolveDefaultsAtRules() {
 
       let universalRule = postcss.rule()
 
-      // TODO: Fix this, this is a hotfix
-      // universalRule.selectors = [...selectors]
-      universalRule.selectors = ['*', '::before', '::after']
+      if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
+        universalRule.selectors = [...selectors]
+      } else {
+        universalRule.selectors = ['*', '::before', '::after']
+      }
 
       universalRule.append(universal.nodes)
       universal.before(universalRule)
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index 26ba8e29..c6a9732a 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -15,44 +15,7 @@ import bigSign from '../util/bigSign'
 import corePlugins from '../corePlugins'
 import * as sharedState from './sharedState'
 import { env } from './sharedState'
-
-function toPath(value) {
-  if (Array.isArray(value)) {
-    return value
-  }
-
-  let inBrackets = false
-  let parts = []
-  let chunk = ''
-
-  for (let i = 0; i < value.length; i++) {
-    let char = value[i]
-    if (char === '[') {
-      inBrackets = true
-      parts.push(chunk)
-      chunk = ''
-      continue
-    }
-    if (char === ']' && inBrackets) {
-      inBrackets = false
-      parts.push(chunk)
-      chunk = ''
-      continue
-    }
-    if (char === '.' && !inBrackets && chunk.length > 0) {
-      parts.push(chunk)
-      chunk = ''
-      continue
-    }
-    chunk = chunk + char
-  }
-
-  if (chunk.length > 0) {
-    parts.push(chunk)
-  }
-
-  return parts
-}
+import { toPath } from '../util/toPath'
 
 function insertInto(list, value, { before = [] } = {}) {
   before = [].concat(before)
diff --git a/src/lib/substituteResponsiveAtRules.js b/src/lib/substituteResponsiveAtRules.js
index 3147fc56..d15f2cea 100644
--- a/src/lib/substituteResponsiveAtRules.js
+++ b/src/lib/substituteResponsiveAtRules.js
@@ -1,8 +1,8 @@
-import _ from 'lodash'
 import postcss from 'postcss'
 import cloneNodes from '../util/cloneNodes'
 import buildMediaQuery from '../util/buildMediaQuery'
 import buildSelectorVariant from '../util/buildSelectorVariant'
+import { tap } from '../util/tap'
 
 function isLayer(node) {
   if (Array.isArray(node)) {
@@ -20,9 +20,9 @@ export default function (config) {
     // Wrap any `responsive` rules with a copy of their parent `layer` to
     // ensure the layer isn't lost when copying to the `screens` location.
     css.walkAtRules('layer', (layerAtRule) => {
-      const layer = layerAtRule.params
+      let layer = layerAtRule.params
       layerAtRule.walkAtRules('responsive', (responsiveAtRule) => {
-        const nestedlayerAtRule = postcss.atRule({
+        let nestedlayerAtRule = postcss.atRule({
           name: 'layer',
           params: layer,
         })
@@ -32,15 +32,15 @@ export default function (config) {
       })
     })
 
-    const {
+    let {
       theme: { screens },
       separator,
     } = config
-    const responsiveRules = postcss.root()
-    const finalRules = []
+    let responsiveRules = postcss.root()
+    let finalRules = []
 
     css.walkAtRules('responsive', (atRule) => {
-      const nodes = atRule.nodes
+      let nodes = atRule.nodes
       responsiveRules.append(...cloneNodes(nodes))
 
       // If the parent is already a `layer` (this is true for anything coming from
@@ -55,16 +55,16 @@ export default function (config) {
       atRule.remove()
     })
 
-    _.keys(screens).forEach((screen) => {
-      const mediaQuery = postcss.atRule({
+    for (let [screen, value] of Object.entries(screens ?? {})) {
+      let mediaQuery = postcss.atRule({
         name: 'media',
-        params: buildMediaQuery(screens[screen]),
+        params: buildMediaQuery(value),
       })
 
       mediaQuery.append(
-        _.tap(responsiveRules.clone(), (clonedRoot) => {
+        tap(responsiveRules.clone(), (clonedRoot) => {
           clonedRoot.walkRules((rule) => {
-            rule.selectors = _.map(rule.selectors, (selector) =>
+            rule.selectors = rule.selectors.map((selector) =>
               buildSelectorVariant(selector, screen, separator, (message) => {
                 throw rule.error(message)
               })
@@ -74,9 +74,9 @@ export default function (config) {
       )
 
       finalRules.push(mediaQuery)
-    })
+    }
 
-    const hasScreenRules = finalRules.some((i) => i.nodes.length !== 0)
+    let hasScreenRules = finalRules.some((i) => i.nodes.length !== 0)
 
     css.walkAtRules('tailwind', (atRule) => {
       if (atRule.params !== 'screens') {
diff --git a/src/lib/substituteScreenAtRules.js b/src/lib/substituteScreenAtRules.js
index 84c2fdde..1b61d38f 100644
--- a/src/lib/substituteScreenAtRules.js
+++ b/src/lib/substituteScreenAtRules.js
@@ -1,4 +1,3 @@
-import _ from 'lodash'
 import buildMediaQuery from '../util/buildMediaQuery'
 
 export default function ({ tailwindConfig: { theme } }) {
@@ -6,7 +5,7 @@ export default function ({ tailwindConfig: { theme } }) {
     css.walkAtRules('screen', (atRule) => {
       const screen = atRule.params
 
-      if (!_.has(theme.screens, screen)) {
+      if (!theme.screens?.hasOwnProperty?.(screen)) {
         throw atRule.error(`No \`${screen}\` screen found.`)
       }
 
diff --git a/src/plugins/container.js b/src/plugins/container.js
index 21e24cd1..4093750f 100644
--- a/src/plugins/container.js
+++ b/src/plugins/container.js
@@ -1,9 +1,6 @@
-/* eslint-disable no-shadow */
-import _ from 'lodash'
-
 function extractMinWidths(breakpoints) {
-  return _.flatMap(breakpoints, (breakpoints) => {
-    if (_.isString(breakpoints)) {
+  return Object.values(breakpoints ?? {}).flatMap((breakpoints) => {
+    if (typeof breakpoints === 'string') {
       breakpoints = { min: breakpoints }
     }
 
@@ -11,14 +8,13 @@ function extractMinWidths(breakpoints) {
       breakpoints = [breakpoints]
     }
 
-    return _(breakpoints)
+    return breakpoints
       .filter((breakpoint) => {
-        return _.has(breakpoint, 'min') || _.has(breakpoint, 'min-width')
+        return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width')
       })
       .map((breakpoint) => {
-        return _.get(breakpoint, 'min-width', breakpoint.min)
+        return breakpoint['min-width'] ?? breakpoint.min
       })
-      .value()
   })
 }
 
@@ -27,7 +23,7 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
     return []
   }
 
-  if (!_.isObject(paddings)) {
+  if (!(typeof paddings === 'object' && paddings !== null)) {
     return [
       {
         screen: 'DEFAULT',
@@ -37,7 +33,7 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
     ]
   }
 
-  const mapping = []
+  let mapping = []
 
   if (paddings.DEFAULT) {
     mapping.push({
@@ -47,11 +43,10 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
     })
   }
 
-  _.each(minWidths, (minWidth) => {
-    Object.keys(screens).forEach((screen) => {
-      const screenMinWidth = _.isPlainObject(screens[screen])
-        ? screens[screen].min || screens[screen]['min-width']
-        : screens[screen]
+  for (let minWidth of minWidths) {
+    for (let [screen, value] of Object.entries(screens)) {
+      let screenMinWidth =
+        typeof value === 'object' && value !== null ? value.min || value['min-width'] : value
 
       if (`${screenMinWidth}` === `${minWidth}`) {
         mapping.push({
@@ -60,20 +55,20 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
           padding: paddings[screen],
         })
       }
-    })
-  })
+    }
+  }
 
   return mapping
 }
 
 module.exports = function () {
   return function ({ addComponents, theme, variants }) {
-    const screens = theme('container.screens', theme('screens'))
-    const minWidths = extractMinWidths(screens)
-    const paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
+    let screens = theme('container.screens', theme('screens'))
+    let minWidths = extractMinWidths(screens)
+    let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
 
-    const generatePaddingFor = (minWidth) => {
-      const paddingConfig = _.find(paddings, (padding) => `${padding.minWidth}` === `${minWidth}`)
+    let generatePaddingFor = (minWidth) => {
+      let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`)
 
       if (!paddingConfig) {
         return {}
@@ -85,20 +80,16 @@ module.exports = function () {
       }
     }
 
-    const atRules = _(minWidths)
-      .sortBy((minWidth) => parseInt(minWidth))
-      .sortedUniq()
-      .map((minWidth) => {
-        return {
-          [`@media (min-width: ${minWidth})`]: {
-            '.container': {
-              'max-width': minWidth,
-              ...generatePaddingFor(minWidth),
-            },
-          },
-        }
-      })
-      .value()
+    let atRules = Array.from(
+      new Set(minWidths.slice().sort((a, z) => parseInt(a) - parseInt(z)))
+    ).map((minWidth) => ({
+      [`@media (min-width: ${minWidth})`]: {
+        '.container': {
+          'max-width': minWidth,
+          ...generatePaddingFor(minWidth),
+        },
+      },
+    }))
 
     addComponents(
       [
diff --git a/src/plugins/dropShadow.js b/src/plugins/dropShadow.js
index 0d47693e..0711dda8 100644
--- a/src/plugins/dropShadow.js
+++ b/src/plugins/dropShadow.js
@@ -1,10 +1,9 @@
-import _ from 'lodash'
 import nameClass from '../util/nameClass'
 
 export default function () {
   return function ({ addUtilities, theme, variants }) {
-    const utilities = _.fromPairs(
-      _.map(theme('dropShadow'), (value, modifier) => {
+    let utilities = Object.fromEntries(
+      Object.entries(theme('dropShadow') ?? {}).map(([modifier, value]) => {
         return [
           nameClass('drop-shadow', modifier),
           {
diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js
index bb42c9db..ba3b696e 100644
--- a/src/util/buildMediaQuery.js
+++ b/src/util/buildMediaQuery.js
@@ -1,7 +1,5 @@
-import _ from 'lodash'
-
 export default function buildMediaQuery(screens) {
-  if (_.isString(screens)) {
+  if (typeof screens === 'string') {
     screens = { min: screens }
   }
 
@@ -9,22 +7,15 @@ export default function buildMediaQuery(screens) {
     screens = [screens]
   }
 
-  return _(screens)
+  return screens
     .map((screen) => {
-      if (_.has(screen, 'raw')) {
+      if (screen?.hasOwnProperty?.('raw')) {
         return screen.raw
       }
 
-      return _(screen)
-        .map((value, feature) => {
-          feature = _.get(
-            {
-              min: 'min-width',
-              max: 'max-width',
-            },
-            feature,
-            feature
-          )
+      return Object.entries(screen)
+        .map(([feature, value]) => {
+          feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
           return `(${feature}: ${value})`
         })
         .join(' and ')
diff --git a/src/util/buildSelectorVariant.js b/src/util/buildSelectorVariant.js
index dbb291d6..542c5ebd 100644
--- a/src/util/buildSelectorVariant.js
+++ b/src/util/buildSelectorVariant.js
@@ -1,5 +1,5 @@
 import parser from 'postcss-selector-parser'
-import tap from 'lodash/tap'
+import { tap } from './tap'
 import { useMemo } from './useMemo'
 
 const buildSelectorVariant = useMemo(
diff --git a/src/util/cloneDeep.js b/src/util/cloneDeep.js
new file mode 100644
index 00000000..47a12176
--- /dev/null
+++ b/src/util/cloneDeep.js
@@ -0,0 +1,11 @@
+export function cloneDeep(value) {
+  if (Array.isArray(value)) {
+    return value.map((child) => cloneDeep(child))
+  }
+
+  if (typeof value === 'object' && value !== null) {
+    return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)]))
+  }
+
+  return value
+}
diff --git a/src/util/defaults.js b/src/util/defaults.js
new file mode 100644
index 00000000..709387a6
--- /dev/null
+++ b/src/util/defaults.js
@@ -0,0 +1,11 @@
+export function defaults(target, ...sources) {
+  for (let source of sources) {
+    for (let k in source) {
+      if (!target?.hasOwnProperty?.(k)) {
+        target[k] = source[k]
+      }
+    }
+  }
+
+  return target
+}
diff --git a/src/util/escapeClassName.js b/src/util/escapeClassName.js
index 2469c37a..cb5924a9 100644
--- a/src/util/escapeClassName.js
+++ b/src/util/escapeClassName.js
@@ -1,9 +1,8 @@
 import parser from 'postcss-selector-parser'
-import get from 'lodash/get'
 import escapeCommas from './escapeCommas'
 
 export default function escapeClassName(className) {
-  const node = parser.className()
+  let node = parser.className()
   node.value = className
-  return escapeCommas(get(node, 'raws.value', node.value))
+  return escapeCommas(node?.raws?.value ?? node.value)
 }
diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js
index 2bd17cbe..c3b426d1 100644
--- a/src/util/generateVariantFunction.js
+++ b/src/util/generateVariantFunction.js
@@ -1,13 +1,12 @@
-import _ from 'lodash'
 import postcss from 'postcss'
 import selectorParser from 'postcss-selector-parser'
 import { useMemo } from './useMemo'
 
-const classNameParser = selectorParser((selectors) => {
+let classNameParser = selectorParser((selectors) => {
   return selectors.first.filter(({ type }) => type === 'class').pop().value
 })
 
-const getClassNameFromSelector = useMemo(
+let getClassNameFromSelector = useMemo(
   (selector) => classNameParser.transformSync(selector),
   (selector) => selector
 )
@@ -16,10 +15,10 @@ export default function generateVariantFunction(generator, options = {}) {
   return {
     options,
     handler: (container, config) => {
-      const cloned = postcss.root({ nodes: container.clone().nodes })
+      let cloned = postcss.root({ nodes: container.clone().nodes })
 
       container.before(
-        _.defaultTo(
+        (
           generator({
             container: cloned,
             separator: config.separator,
@@ -40,8 +39,7 @@ export default function generateVariantFunction(generator, options = {}) {
               })
               return cloned
             },
-          }),
-          cloned
+          }) ?? cloned
         ).nodes
       )
     },
diff --git a/src/util/parseObjectStyles.js b/src/util/parseObjectStyles.js
index f22034eb..cb54787d 100644
--- a/src/util/parseObjectStyles.js
+++ b/src/util/parseObjectStyles.js
@@ -1,4 +1,3 @@
-import _ from 'lodash'
 import postcss from 'postcss'
 import postcssNested from 'postcss-nested'
 import postcssJs from 'postcss-js'
@@ -8,7 +7,7 @@ export default function parseObjectStyles(styles) {
     return parseObjectStyles([styles])
   }
 
-  return _.flatMap(styles, (style) => {
+  return styles.flatMap((style) => {
     return postcss([
       postcssNested({
         bubble: ['screen'],
diff --git a/src/util/prefixNegativeModifiers.js b/src/util/prefixNegativeModifiers.js
index 1642b155..70cefa4c 100644
--- a/src/util/prefixNegativeModifiers.js
+++ b/src/util/prefixNegativeModifiers.js
@@ -1,9 +1,7 @@
-import _ from 'lodash'
-
 export default function prefixNegativeModifiers(base, modifier) {
   if (modifier === '-') {
     return `-${base}`
-  } else if (_.startsWith(modifier, '-')) {
+  } else if (modifier.startsWith('-')) {
     return `-${base}-${modifier.slice(1)}`
   } else {
     return `${base}-${modifier}`
diff --git a/src/util/prefixSelector.js b/src/util/prefixSelector.js
index 3b61f31d..4816b681 100644
--- a/src/util/prefixSelector.js
+++ b/src/util/prefixSelector.js
@@ -1,5 +1,5 @@
 import parser from 'postcss-selector-parser'
-import tap from 'lodash/tap'
+import { tap } from './tap'
 
 export default function (prefix, selector) {
   const getPrefix =
diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js
index bbe3014d..5f544159 100644
--- a/src/util/processPlugins.js
+++ b/src/util/processPlugins.js
@@ -1,31 +1,29 @@
-import _ from 'lodash'
+import dlv from 'dlv'
 import postcss from 'postcss'
 import Node from 'postcss/lib/node'
-import isFunction from 'lodash/isFunction'
-import escapeClassName from '../util/escapeClassName'
-import generateVariantFunction from '../util/generateVariantFunction'
-import parseObjectStyles from '../util/parseObjectStyles'
-import prefixSelector from '../util/prefixSelector'
-import wrapWithVariants from '../util/wrapWithVariants'
-import cloneNodes from '../util/cloneNodes'
+import escapeClassName from './escapeClassName'
+import generateVariantFunction from './generateVariantFunction'
+import parseObjectStyles from './parseObjectStyles'
+import prefixSelector from './prefixSelector'
+import wrapWithVariants from './wrapWithVariants'
+import cloneNodes from './cloneNodes'
 import transformThemeValue from './transformThemeValue'
-import nameClass from '../util/nameClass'
-import isKeyframeRule from '../util/isKeyframeRule'
+import nameClass from './nameClass'
+import isKeyframeRule from './isKeyframeRule'
+import { toPath } from './toPath'
+import { defaults } from './defaults'
 
 function parseStyles(styles) {
   if (!Array.isArray(styles)) {
     return parseStyles([styles])
   }
 
-  return _.flatMap(styles, (style) => (style instanceof Node ? style : parseObjectStyles(style)))
+  return styles.flatMap((style) => (style instanceof Node ? style : parseObjectStyles(style)))
 }
 
 function wrapWithLayer(rules, layer) {
   return postcss
-    .atRule({
-      name: 'layer',
-      params: layer,
-    })
+    .atRule({ name: 'layer', params: layer })
     .append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
 }
 
@@ -48,7 +46,7 @@ export default function (plugins, config) {
 
     options = Array.isArray(options)
       ? Object.assign({}, defaultOptions, { variants: options })
-      : _.defaults(options, defaultOptions)
+      : defaults(options, defaultOptions)
 
     const styles = postcss.root({ nodes: parseStyles(utilities) })
 
@@ -70,21 +68,21 @@ export default function (plugins, config) {
     )
   }
 
-  const getConfigValue = (path, defaultValue) => (path ? _.get(config, path, defaultValue) : config)
+  const getConfigValue = (path, defaultValue) => (path ? dlv(config, path, defaultValue) : config)
 
   plugins.forEach((plugin) => {
     if (plugin.__isOptionsFunction) {
       plugin = plugin()
     }
 
-    const handler = isFunction(plugin) ? plugin : _.get(plugin, 'handler', () => {})
+    const handler = typeof plugin === 'function' ? plugin : plugin?.handler ?? (() => {})
 
     handler({
       postcss,
       config: getConfigValue,
       theme: (path, defaultValue) => {
-        const [pathRoot, ...subPaths] = _.toPath(path)
-        const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
+        let [pathRoot, ...subPaths] = toPath(path)
+        let value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
 
         return transformThemeValue(pathRoot)(value)
       },
@@ -135,7 +133,7 @@ export default function (plugins, config) {
 
         options = Array.isArray(options)
           ? Object.assign({}, defaultOptions, { variants: options })
-          : _.defaults(options, defaultOptions)
+          : defaults(options, defaultOptions)
 
         const styles = postcss.root({ nodes: parseStyles(components) })
 
diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js
index c031b0ee..3fae7426 100644
--- a/src/util/resolveConfig.js
+++ b/src/util/resolveConfig.js
@@ -1,20 +1,46 @@
-import some from 'lodash/some'
-import mergeWith from 'lodash/mergeWith'
-import isFunction from 'lodash/isFunction'
-import isUndefined from 'lodash/isUndefined'
-import defaults from 'lodash/defaults'
-import map from 'lodash/map'
-import get from 'lodash/get'
-import uniq from 'lodash/uniq'
-import toPath from 'lodash/toPath'
-import head from 'lodash/head'
-import isPlainObject from 'lodash/isPlainObject'
 import negateValue from './negateValue'
 import corePluginList from '../corePluginList'
 import configurePlugins from './configurePlugins'
 import defaultConfig from '../../stubs/defaultConfig.stub'
 import colors from '../../colors'
 import log from './log'
+import { defaults } from './defaults'
+import { toPath } from './toPath'
+import dlv from 'dlv'
+
+function isFunction(input) {
+  return typeof input === 'function'
+}
+
+function uniq(input) {
+  return Array.from(new Set(input))
+}
+
+function isObject(input) {
+  return typeof input === 'object' && input !== null
+}
+
+function mergeWith(target, ...sources) {
+  let customizer = sources.pop()
+
+  for (let source of sources) {
+    for (let k in source) {
+      let merged = customizer(target[k], source[k])
+
+      if (merged === undefined) {
+        if (isObject(target[k]) && isObject(source[k])) {
+          target[k] = mergeWith(target[k], source[k], customizer)
+        } else {
+          target[k] = source[k]
+        }
+      } else {
+        target[k] = merged
+      }
+    }
+  }
+
+  return target
+}
 
 const configUtils = {
   colors,
@@ -49,7 +75,7 @@ function value(valueToResolve, ...args) {
 function collectExtends(items) {
   return items.reduce((merged, { extend }) => {
     return mergeWith(merged, extend, (mergedValue, extendValue) => {
-      if (isUndefined(mergedValue)) {
+      if (mergedValue === undefined) {
         return [extendValue]
       }
 
@@ -74,12 +100,12 @@ function mergeThemes(themes) {
 
 function mergeExtensionCustomizer(merged, value) {
   // When we have an array of objects, we do want to merge it
-  if (Array.isArray(merged) && isPlainObject(head(merged))) {
+  if (Array.isArray(merged) && isObject(merged[0])) {
     return merged.concat(value)
   }
 
   // When the incoming value is an array, and the existing config is an object, prepend the existing object
-  if (Array.isArray(value) && isPlainObject(head(value)) && isPlainObject(merged)) {
+  if (Array.isArray(value) && isObject(value[0]) && isObject(merged)) {
     return [merged, ...value]
   }
 
@@ -95,7 +121,7 @@ function mergeExtensionCustomizer(merged, value) {
 function mergeExtensions({ extend, ...theme }) {
   return mergeWith(theme, extend, (themeValue, extensions) => {
     // The `extend` property is an array, so we need to check if it contains any functions
-    if (!isFunction(themeValue) && !some(extensions, isFunction)) {
+    if (!isFunction(themeValue) && !extensions.some(isFunction)) {
       return mergeWith({}, themeValue, ...extensions, mergeExtensionCustomizer)
     }
 
@@ -143,7 +169,7 @@ function extractPluginConfigs(configs) {
   configs.forEach((config) => {
     allConfigs = [...allConfigs, config]
 
-    const plugins = get(config, 'plugins', [])
+    const plugins = config?.plugins ?? []
 
     if (plugins.length === 0) {
       return
@@ -153,7 +179,7 @@ function extractPluginConfigs(configs) {
       if (plugin.__isOptionsFunction) {
         plugin = plugin()
       }
-      allConfigs = [...allConfigs, ...extractPluginConfigs([get(plugin, 'config', {})])]
+      allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])]
     })
   })
 
@@ -166,9 +192,9 @@ function mergeVariants(variants) {
       if (isFunction(pluginVariants)) {
         resolved[plugin] = pluginVariants({
           variants(path) {
-            return get(resolved, path, [])
+            return dlv(resolved, path, [])
           },
-          before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
+          before(toInsert, variant, existingPluginVariants = resolved?.[plugin] ?? []) {
             if (variant === undefined) {
               return [...toInsert, ...existingPluginVariants]
             }
@@ -185,7 +211,7 @@ function mergeVariants(variants) {
               ...existingPluginVariants.slice(index),
             ]
           },
-          after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
+          after(toInsert, variant, existingPluginVariants = resolved?.[plugin] ?? []) {
             if (variant === undefined) {
               return [...existingPluginVariants, ...toInsert]
             }
@@ -202,7 +228,7 @@ function mergeVariants(variants) {
               ...existingPluginVariants.slice(index + 1),
             ]
           },
-          without(toRemove, existingPluginVariants = get(resolved, plugin, [])) {
+          without(toRemove, existingPluginVariants = resolved?.[plugin] ?? []) {
             return existingPluginVariants.filter((v) => !toRemove.includes(v))
           },
         })
@@ -279,14 +305,14 @@ export default function resolveConfig(configs) {
     defaults(
       {
         theme: resolveFunctionKeys(
-          mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {}))))
+          mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
         ),
         variants: resolveVariants(
-          allConfigs.map((c) => get(c, 'variants', {})),
+          allConfigs.map((c) => c?.variants ?? {}),
           variantOrder
         ),
         corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
-        plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))),
+        plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
       },
       ...allConfigs
     )
diff --git a/src/util/tap.js b/src/util/tap.js
new file mode 100644
index 00000000..0590e4bf
--- /dev/null
+++ b/src/util/tap.js
@@ -0,0 +1,4 @@
+export function tap(value, mutator) {
+  mutator(value)
+  return value
+}
diff --git a/src/util/toColorValue.js b/src/util/toColorValue.js
index a721d2c8..288d9078 100644
--- a/src/util/toColorValue.js
+++ b/src/util/toColorValue.js
@@ -1,5 +1,3 @@
-import _ from 'lodash'
-
 export default function toColorValue(maybeFunction) {
-  return _.isFunction(maybeFunction) ? maybeFunction({}) : maybeFunction
+  return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction
 }
diff --git a/src/util/toPath.js b/src/util/toPath.js
new file mode 100644
index 00000000..acf55d8d
--- /dev/null
+++ b/src/util/toPath.js
@@ -0,0 +1,4 @@
+export function toPath(path) {
+  if (Array.isArray(path)) return path
+  return path.split(/[\.\]\[]+/g)
+}
diff --git a/src/util/withAlphaVariable.js b/src/util/withAlphaVariable.js
index 1c9e8d48..70c552cd 100644
--- a/src/util/withAlphaVariable.js
+++ b/src/util/withAlphaVariable.js
@@ -1,12 +1,11 @@
 import * as culori from 'culori'
-import _ from 'lodash'
 
 function isValidColor(color) {
   return culori.parse(color) !== undefined
 }
 
 export function withAlphaValue(color, alphaValue, defaultValue) {
-  if (_.isFunction(color)) {
+  if (typeof color === 'function') {
     return color({ opacityValue: alphaValue })
   }
 
@@ -40,7 +39,7 @@ export function withAlphaValue(color, alphaValue, defaultValue) {
 }
 
 export default function withAlphaVariable({ color, property, variable }) {
-  if (_.isFunction(color)) {
+  if (typeof color === 'function') {
     return {
       [variable]: '1',
       [property]: color({ opacityVariable: variable, opacityValue: `var(${variable})` }),


Test patch:
diff --git a/tests/to-path.test.js b/tests/to-path.test.js
new file mode 100644
index 00000000..dbd39c54
--- /dev/null
+++ b/tests/to-path.test.js
@@ -0,0 +1,17 @@
+import { toPath } from '../src/util/toPath'
+
+it('should keep an array as an array', () => {
+  let input = ['a', 'b', '0', 'c']
+
+  expect(toPath(input)).toBe(input)
+})
+
+it.each`
+  input         | output
+  ${'a.b.c'}    | ${['a', 'b', 'c']}
+  ${'a[0].b.c'} | ${['a', '0', 'b', 'c']}
+  ${'.a'}       | ${['', 'a']}
+  ${'[].a'}     | ${['', 'a']}
+`('should convert "$input" to "$output"', ({ input, output }) => {
+  expect(toPath(input)).toEqual(output)
+})
diff --git a/tests/util/invokePlugin.js b/tests/util/invokePlugin.js
index c58d1f07..226d6470 100644
--- a/tests/util/invokePlugin.js
+++ b/tests/util/invokePlugin.js
@@ -1,16 +1,16 @@
-import _ from 'lodash'
+import dlv from 'dlv'
 import escapeClassName from '../src/util/escapeClassName'
 
 export default function (plugin, config) {
   const addedUtilities = []
 
-  const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
+  const getConfigValue = (path, defaultValue) => dlv(config, path, defaultValue)
   const pluginApi = {
     config: getConfigValue,
     e: escapeClassName,
     theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
     variants: (path, defaultValue) => {
-      if (_.isArray(config.variants)) {
+      if (Array.isArray(config.variants)) {
         return config.variants
       }
 
@@ -36,7 +36,7 @@ export default function (plugin, config) {
 
   return {
     utilities: addedUtilities.map(([utilities, variants]) => [
-      _.merge({}, ..._.castArray(utilities)),
+      Object.assign({}, ...(Array.isArray(utilities) ? utilities : [utilities])),
       variants,
     ]),
   }