From a34bd62bb6a82886c1a03611b1b85ccae37a5f7b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 6 Sep 2021 20:15:10 +0200 Subject: [PATCH] Remove lodash (#5390) * remove `lodash` usage * implement custom cloneDeep to replace lodash's * drop lodash in processPlugins * add `toPath` utility * add `tap` utility * add `cloneDeep` utility * drop lodash in evaluateTailwindFunctions * add `defaults` utility * drop lodash from `resolveConfig` * remove `lodash` dependency --- defaultConfig.js | 4 +- defaultTheme.js | 4 +- package-lock.json | 8 +-- package.json | 1 - src/featureFlags.js | 7 ++- src/lib/evaluateTailwindFunctions.js | 35 +++++++----- src/lib/setupContextUtils.js | 39 +------------- src/lib/substituteResponsiveAtRules.js | 28 +++++----- src/lib/substituteScreenAtRules.js | 3 +- src/plugins/container.js | 65 ++++++++++------------ src/plugins/dropShadow.js | 5 +- src/util/buildMediaQuery.js | 21 +++----- src/util/buildSelectorVariant.js | 2 +- src/util/cloneDeep.js | 11 ++++ src/util/defaults.js | 11 ++++ src/util/escapeClassName.js | 5 +- src/util/generateVariantFunction.js | 12 ++--- src/util/parseObjectStyles.js | 3 +- src/util/prefixNegativeModifiers.js | 4 +- src/util/prefixSelector.js | 2 +- src/util/processPlugins.js | 40 +++++++------- src/util/resolveConfig.js | 74 +++++++++++++++++--------- src/util/tap.js | 4 ++ src/util/toColorValue.js | 4 +- src/util/toPath.js | 4 ++ src/util/withAlphaVariable.js | 5 +- tests/to-path.test.js | 17 ++++++ tests/util/invokePlugin.js | 8 +-- 28 files changed, 218 insertions(+), 208 deletions(-) create mode 100644 src/util/cloneDeep.js create mode 100644 src/util/defaults.js create mode 100644 src/util/tap.js create mode 100644 src/util/toPath.js create mode 100644 tests/to-path.test.js diff --git a/defaultConfig.js b/defaultConfig.js index 820ae3e162ab..0a131abc3fbb 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 70e74af7f103..4a42a998368a 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 4becce56a6f5..678a8e84fdc4 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 218d320bc865..bcf259ee9ac6 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 9525beb312ea..957c079c2082 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -1,4 +1,3 @@ -import _ from 'lodash' import chalk from 'chalk' import log from './util/log' @@ -9,11 +8,11 @@ const featureFlags = { 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 0e2faa0ff336..fb3474b2cfa3 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/setupContextUtils.js b/src/lib/setupContextUtils.js index 26ba8e295f4f..c6a9732a9298 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 3147fc566c9c..d15f2cea23aa 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 84c2fdde8d9c..1b61d38f35d6 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 21e24cd13671..4093750ff5bd 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 0d47693e6430..0711dda872e2 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 bb42c9db967f..ba3b696e889b 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 dbb291d6ebab..542c5ebdc6c8 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 000000000000..47a121765cd9 --- /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 000000000000..709387a684bf --- /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 2469c37ad24f..cb5924a9f9d5 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 2bd17cbeb2fc..c3b426d1a9b2 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 f22034eb3611..cb54787dec7a 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 1642b155c7f7..70cefa4c5fc6 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 3b61f31dd2da..4816b681aee7 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 bbe3014d034a..5f544159bdfc 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 c031b0ee1811..3fae7426655c 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 000000000000..0590e4bfd85c --- /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 a721d2c81961..288d907846b5 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 000000000000..acf55d8d03e8 --- /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 1c9e8d48ae91..70c552cd20a7 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})` }), diff --git a/tests/to-path.test.js b/tests/to-path.test.js new file mode 100644 index 000000000000..dbd39c54a517 --- /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 c58d1f07d0dc..226d6470256e 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, ]), }