Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Sep 23, 2022
1 parent bc0f59e commit 85adfe3
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 46 deletions.
121 changes: 88 additions & 33 deletions src/lib/generateRules.js
Expand Up @@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser'
import parseObjectStyles from '../util/parseObjectStyles'
import isPlainObject from '../util/isPlainObject'
import prefixSelector from '../util/prefixSelector'
import { updateAllClasses } from '../util/pluginUtils'
import { updateAllClasses, typeMap } from '../util/pluginUtils'
import log from '../util/log'
import * as sharedState from './sharedState'
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
Expand Down Expand Up @@ -535,15 +535,28 @@ function* resolveMatches(candidate, context, original = candidate) {
}

if (matchesPerPlugin.length > 0) {
typesByMatches.set(matchesPerPlugin, sort.options?.type)
let matchingTypes = (sort.options?.types ?? [])
.map(({ type }) => type)
.filter((type) => {
return Boolean(
typeMap[type](modifier, sort.options, {
tailwindConfig: context.tailwindConfig,
})
)
})

if (matchingTypes.length > 0) {
typesByMatches.set(matchesPerPlugin, matchingTypes)
}

matches.push(matchesPerPlugin)
}
}

if (isArbitraryValue(modifier)) {
// When generated arbitrary values are ambiguous, we can't know
// which to pick so don't generate any utilities for them
if (matches.length > 1) {
// When generated arbitrary values are ambiguous, we can't know which to pick so don't
// generate any utilities for them
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))

// Remove duplicates, so that we can detect proper unique types for each plugin.
Expand All @@ -564,39 +577,81 @@ function* resolveMatches(candidate, context, original = candidate) {
}
}

let messages = []

for (let [idx, group] of typesPerPlugin.entries()) {
for (let type of group) {
let rules = matches[idx]
.map(([, rule]) => rule)
.flat()
.map((rule) =>
rule
.toString()
.split('\n')
.slice(1, -1) // Remove selector and closing '}'
.map((line) => line.trim())
.map((x) => ` ${x}`) // Re-indent
.join('\n')
)
.join('\n\n')

messages.push(
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
// Partition
let [withAny, withoutAny] = matches.reduce(
(group, plugin) => {
let hasAnyType = plugin.some(([{ options }]) =>
options.types.some(({ type }) => type === 'any')
)
break

if (hasAnyType) {
group[0].push(plugin)
} else {
group[1].push(plugin)
}
return group
},
[[], []]
)

function findFallback(matches) {
if (matches.length === 1) {
return matches[0]
}

return matches.find((rules) => {
let matchingTypes = typesByMatches.get(rules)
return rules.some(([{ options }, rule]) => {
if (!isParsableNode(rule)) {
return false
}

return options.types.some(
({ type, disambiguate }) => matchingTypes.includes(type) && disambiguate
)
})
})
}

log.warn([
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
...messages,
`If this is content and not a class, replace it with \`${candidate
.replace('[', '[')
.replace(']', ']')}\` to silence this warning.`,
])
continue
let fallback = findFallback(withoutAny) ?? findFallback(withAny)

if (fallback) {
matches = [fallback]
} else {
let messages = []

for (let [idx, group] of typesPerPlugin.entries()) {
for (let type of group) {
let rules = matches[idx]
.map(([, rule]) => rule)
.flat()
.map((rule) =>
rule
.toString()
.split('\n')
.slice(1, -1) // Remove selector and closing '}'
.map((line) => line.trim())
.map((x) => ` ${x}`) // Re-indent
.join('\n')
)
.join('\n\n')

messages.push(
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
)
break
}
}

log.warn([
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
...messages,
`If this is content and not a class, replace it with \`${candidate
.replace('[', '[')
.replace(']', ']')}\` to silence this warning.`,
])
continue
}
}

matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
Expand Down
42 changes: 31 additions & 11 deletions src/lib/setupContextUtils.js
Expand Up @@ -30,6 +30,20 @@ function prefix(context, selector) {
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
}

function normalizeOptionTypes({ type = 'any', ...options }) {
let types = [].concat(type)

return {
...options,
types: types.map((type) => {
if (Array.isArray(type)) {
return { type: type[0], ...type[1] }
}
return { type, disambiguate: false }
}),
}
}

function parseVariantFormatString(input) {
if (input.includes('{')) {
if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
Expand Down Expand Up @@ -346,7 +360,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
respectImportant: true,
}

options = { ...defaultOptions, ...options }
options = normalizeOptionTypes({ ...defaultOptions, ...options })

let offset = offsets.create('utilities')

Expand All @@ -357,16 +371,24 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
classList.add([prefixedIdentifier, options])

function wrapped(modifier, { isOnlyPlugin }) {
let { type = 'any' } = options
type = [].concat(type)
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)

if (value === undefined) {
return []
}

if (!type.includes(coercedType) && !isOnlyPlugin) {
return []
if (!options.types.some(({ type }) => type === coercedType)) {
if (isOnlyPlugin) {
log.warn([
`Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`,
`You can safely update it to \`${identifier}-${modifier.replace(
coercedType + ':',
''
)}\`.`,
])
} else {
return []
}
}

if (!isValidArbitraryValue(value)) {
Expand Down Expand Up @@ -398,7 +420,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
respectImportant: false,
}

options = { ...defaultOptions, ...options }
options = normalizeOptionTypes({ ...defaultOptions, ...options })

let offset = offsets.create('components')

Expand All @@ -409,15 +431,13 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
classList.add([prefixedIdentifier, options])

function wrapped(modifier, { isOnlyPlugin }) {
let { type = 'any' } = options
type = [].concat(type)
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)

if (value === undefined) {
return []
}

if (!type.includes(coercedType)) {
if (!options.types.some(({ type }) => type === coercedType)) {
if (isOnlyPlugin) {
log.warn([
`Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`,
Expand Down
4 changes: 2 additions & 2 deletions src/util/pluginUtils.js
Expand Up @@ -146,7 +146,7 @@ function guess(validate) {
}
}

let typeMap = {
export let typeMap = {
any: asValue,
color: asColor,
url: guess(url),
Expand Down Expand Up @@ -195,7 +195,7 @@ export function coerceValue(types, modifier, options, tailwindConfig) {
}

// Find first matching type
for (let type of [].concat(types)) {
for (let { type } of types) {
let result = typeMap[type](modifier, options, { tailwindConfig })
if (result !== undefined) return [result, type]
}
Expand Down

0 comments on commit 85adfe3

Please sign in to comment.