|
import fs from 'fs' |
|
import url from 'url' |
|
import postcss from 'postcss' |
|
import dlv from 'dlv' |
|
import selectorParser from 'postcss-selector-parser' |
|
|
|
import transformThemeValue from '../util/transformThemeValue' |
|
import parseObjectStyles from '../util/parseObjectStyles' |
|
import prefixSelector from '../util/prefixSelector' |
|
import isPlainObject from '../util/isPlainObject' |
|
import escapeClassName from '../util/escapeClassName' |
|
import nameClass, { formatClass } from '../util/nameClass' |
|
import { coerceValue } from '../util/pluginUtils' |
|
import { variantPlugins, corePlugins } from '../corePlugins' |
|
import * as sharedState from './sharedState' |
|
import { env } from './sharedState' |
|
import { toPath } from '../util/toPath' |
|
import log from '../util/log' |
|
import negateValue from '../util/negateValue' |
|
import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPropertyValue' |
|
import { generateRules, getClassNameFromSelector } from './generateRules' |
|
import { hasContentChanged } from './cacheInvalidation.js' |
|
import { Offsets } from './offsets.js' |
|
import { flagEnabled } from '../featureFlags.js' |
|
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector' |
|
|
|
export const INTERNAL_FEATURES = Symbol() |
|
|
|
const VARIANT_TYPES = { |
|
AddVariant: Symbol.for('ADD_VARIANT'), |
|
MatchVariant: Symbol.for('MATCH_VARIANT'), |
|
} |
|
|
|
const VARIANT_INFO = { |
|
Base: 1 << 0, |
|
Dynamic: 1 << 1, |
|
} |
|
|
|
function prefix(context, selector) { |
|
let prefix = context.tailwindConfig.prefix |
|
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, preferOnConflict: false } |
|
}), |
|
} |
|
} |
|
|
|
function parseVariantFormatString(input) { |
|
|
|
let parts = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let current = '' |
|
let depth = 0 |
|
|
|
for (let idx = 0; idx < input.length; idx++) { |
|
let char = input[idx] |
|
|
|
if (char === '\\') { |
|
|
|
current += '\\' + input[++idx] |
|
} else if (char === '{') { |
|
|
|
++depth |
|
parts.push(current.trim()) |
|
current = '' |
|
} else if (char === '}') { |
|
|
|
if (--depth < 0) { |
|
throw new Error(`Your { and } are unbalanced.`) |
|
} |
|
|
|
parts.push(current.trim()) |
|
current = '' |
|
} else { |
|
|
|
current += char |
|
} |
|
} |
|
|
|
if (current.length > 0) { |
|
parts.push(current.trim()) |
|
} |
|
|
|
parts = parts.filter((part) => part !== '') |
|
|
|
return parts |
|
} |
|
|
|
function insertInto(list, value, { before = [] } = {}) { |
|
before = [].concat(before) |
|
|
|
if (before.length <= 0) { |
|
list.push(value) |
|
return |
|
} |
|
|
|
let idx = list.length - 1 |
|
for (let other of before) { |
|
let iidx = list.indexOf(other) |
|
if (iidx === -1) continue |
|
idx = Math.min(idx, iidx) |
|
} |
|
|
|
list.splice(idx, 0, value) |
|
} |
|
|
|
function parseStyles(styles) { |
|
if (!Array.isArray(styles)) { |
|
return parseStyles([styles]) |
|
} |
|
|
|
return styles.flatMap((style) => { |
|
let isNode = !Array.isArray(style) && !isPlainObject(style) |
|
return isNode ? style : parseObjectStyles(style) |
|
}) |
|
} |
|
|
|
function getClasses(selector, mutate) { |
|
let parser = selectorParser((selectors) => { |
|
let allClasses = [] |
|
|
|
if (mutate) { |
|
mutate(selectors) |
|
} |
|
|
|
selectors.walkClasses((classNode) => { |
|
allClasses.push(classNode.value) |
|
}) |
|
|
|
return allClasses |
|
}) |
|
return parser.transformSync(selector) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ignoreNot(selectors) { |
|
selectors.walkPseudos((pseudo) => { |
|
if (pseudo.value === ':not') { |
|
pseudo.remove() |
|
} |
|
}) |
|
} |
|
|
|
function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) { |
|
let classes = [] |
|
let selectors = [] |
|
|
|
if (node.type === 'rule') { |
|
|
|
selectors.push(...node.selectors) |
|
} else if (node.type === 'atrule') { |
|
|
|
node.walkRules((rule) => selectors.push(...rule.selectors)) |
|
} |
|
|
|
for (let selector of selectors) { |
|
let classCandidates = getClasses(selector, ignoreNot) |
|
|
|
|
|
if (classCandidates.length === 0) { |
|
state.containsNonOnDemandable = true |
|
} |
|
|
|
for (let classCandidate of classCandidates) { |
|
classes.push(classCandidate) |
|
} |
|
} |
|
|
|
if (depth === 0) { |
|
return [state.containsNonOnDemandable || classes.length === 0, classes] |
|
} |
|
|
|
return classes |
|
} |
|
|
|
function withIdentifiers(styles) { |
|
return parseStyles(styles).flatMap((node) => { |
|
let nodeMap = new Map() |
|
let [containsNonOnDemandableSelectors, candidates] = extractCandidates(node) |
|
|
|
|
|
if (containsNonOnDemandableSelectors) { |
|
candidates.unshift(sharedState.NOT_ON_DEMAND) |
|
} |
|
|
|
|
|
|
|
|
|
return candidates.map((c) => { |
|
if (!nodeMap.has(node)) { |
|
nodeMap.set(node, node) |
|
} |
|
return [c, nodeMap.get(node)] |
|
}) |
|
}) |
|
} |
|
|
|
export function isValidVariantFormatString(format) { |
|
return format.startsWith('@') || format.includes('&') |
|
} |
|
|
|
export function parseVariant(variant) { |
|
variant = variant |
|
.replace(/\n+/g, '') |
|
.replace(/\s{1,}/g, ' ') |
|
.trim() |
|
|
|
let fns = parseVariantFormatString(variant) |
|
.map((str) => { |
|
if (!str.startsWith('@')) { |
|
return ({ format }) => format(str) |
|
} |
|
|
|
let [, name, params] = /@(\S*)( .+|[({].*)?/g.exec(str) |
|
return ({ wrap }) => wrap(postcss.atRule({ name, params: params?.trim() ?? '' })) |
|
}) |
|
.reverse() |
|
|
|
return (api) => { |
|
for (let fn of fns) { |
|
fn(api) |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) { |
|
function getConfigValue(path, defaultValue) { |
|
return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig |
|
} |
|
|
|
function applyConfiguredPrefix(selector) { |
|
return prefixSelector(tailwindConfig.prefix, selector) |
|
} |
|
|
|
function prefixIdentifier(identifier, options) { |
|
if (identifier === sharedState.NOT_ON_DEMAND) { |
|
return sharedState.NOT_ON_DEMAND |
|
} |
|
|
|
if (!options.respectPrefix) { |
|
return identifier |
|
} |
|
|
|
return context.tailwindConfig.prefix + identifier |
|
} |
|
|
|
function resolveThemeValue(path, defaultValue, opts = {}) { |
|
let parts = toPath(path) |
|
let value = getConfigValue(['theme', ...parts], defaultValue) |
|
return transformThemeValue(parts[0])(value, opts) |
|
} |
|
|
|
let variantIdentifier = 0 |
|
let api = { |
|
postcss, |
|
prefix: applyConfiguredPrefix, |
|
e: escapeClassName, |
|
config: getConfigValue, |
|
theme: resolveThemeValue, |
|
corePlugins: (path) => { |
|
if (Array.isArray(tailwindConfig.corePlugins)) { |
|
return tailwindConfig.corePlugins.includes(path) |
|
} |
|
|
|
return getConfigValue(['corePlugins', path], true) |
|
}, |
|
variants: () => { |
|
|
|
return [] |
|
}, |
|
addBase(base) { |
|
for (let [identifier, rule] of withIdentifiers(base)) { |
|
let prefixedIdentifier = prefixIdentifier(identifier, {}) |
|
let offset = offsets.create('base') |
|
|
|
if (!context.candidateRuleMap.has(prefixedIdentifier)) { |
|
context.candidateRuleMap.set(prefixedIdentifier, []) |
|
} |
|
|
|
context.candidateRuleMap |
|
.get(prefixedIdentifier) |
|
.push([{ sort: offset, layer: 'base' }, rule]) |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
addDefaults(group, declarations) { |
|
const groups = { |
|
[`@defaults ${group}`]: declarations, |
|
} |
|
|
|
for (let [identifier, rule] of withIdentifiers(groups)) { |
|
let prefixedIdentifier = prefixIdentifier(identifier, {}) |
|
|
|
if (!context.candidateRuleMap.has(prefixedIdentifier)) { |
|
context.candidateRuleMap.set(prefixedIdentifier, []) |
|
} |
|
|
|
context.candidateRuleMap |
|
.get(prefixedIdentifier) |
|
.push([{ sort: offsets.create('defaults'), layer: 'defaults' }, rule]) |
|
} |
|
}, |
|
addComponents(components, options) { |
|
let defaultOptions = { |
|
preserveSource: false, |
|
respectPrefix: true, |
|
respectImportant: false, |
|
} |
|
|
|
options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) |
|
|
|
for (let [identifier, rule] of withIdentifiers(components)) { |
|
let prefixedIdentifier = prefixIdentifier(identifier, options) |
|
|
|
classList.add(prefixedIdentifier) |
|
|
|
if (!context.candidateRuleMap.has(prefixedIdentifier)) { |
|
context.candidateRuleMap.set(prefixedIdentifier, []) |
|
} |
|
|
|
context.candidateRuleMap |
|
.get(prefixedIdentifier) |
|
.push([{ sort: offsets.create('components'), layer: 'components', options }, rule]) |
|
} |
|
}, |
|
addUtilities(utilities, options) { |
|
let defaultOptions = { |
|
preserveSource: false, |
|
respectPrefix: true, |
|
respectImportant: true, |
|
} |
|
|
|
options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) |
|
|
|
for (let [identifier, rule] of withIdentifiers(utilities)) { |
|
let prefixedIdentifier = prefixIdentifier(identifier, options) |
|
|
|
classList.add(prefixedIdentifier) |
|
|
|
if (!context.candidateRuleMap.has(prefixedIdentifier)) { |
|
context.candidateRuleMap.set(prefixedIdentifier, []) |
|
} |
|
|
|
context.candidateRuleMap |
|
.get(prefixedIdentifier) |
|
.push([{ sort: offsets.create('utilities'), layer: 'utilities', options }, rule]) |
|
} |
|
}, |
|
matchUtilities: function (utilities, options) { |
|
let defaultOptions = { |
|
respectPrefix: true, |
|
respectImportant: true, |
|
modifiers: false, |
|
} |
|
|
|
options = normalizeOptionTypes({ ...defaultOptions, ...options }) |
|
|
|
let offset = offsets.create('utilities') |
|
|
|
for (let identifier in utilities) { |
|
let prefixedIdentifier = prefixIdentifier(identifier, options) |
|
let rule = utilities[identifier] |
|
|
|
classList.add([prefixedIdentifier, options]) |
|
|
|
function wrapped(modifier, { isOnlyPlugin }) { |
|
let [value, coercedType, utilityModifier] = coerceValue( |
|
options.types, |
|
modifier, |
|
options, |
|
tailwindConfig |
|
) |
|
|
|
if (value === undefined) { |
|
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 (!isSyntacticallyValidPropertyValue(value)) { |
|
return [] |
|
} |
|
|
|
let extras = { |
|
get modifier() { |
|
if (!options.modifiers) { |
|
log.warn(`modifier-used-without-options-for-${identifier}`, [ |
|
'Your plugin must set `modifiers: true` in its options to support modifiers.', |
|
]) |
|
} |
|
|
|
return utilityModifier |
|
}, |
|
} |
|
|
|
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') |
|
|
|
let ruleSets = [] |
|
.concat(modifiersEnabled ? rule(value, extras) : rule(value)) |
|
.filter(Boolean) |
|
.map((declaration) => ({ |
|
[nameClass(identifier, modifier)]: declaration, |
|
})) |
|
|
|
return ruleSets |
|
} |
|
|
|
let withOffsets = [{ sort: offset, layer: 'utilities', options }, wrapped] |
|
|
|
if (!context.candidateRuleMap.has(prefixedIdentifier)) { |
|
context.candidateRuleMap.set(prefixedIdentifier, []) |
|
} |
|
|
|
context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) |
|
} |
|
}, |
|
matchComponents: function (components, options) { |
|
let defaultOptions = { |
|
respectPrefix: true, |
|
respectImportant: false, |
|
modifiers: false, |
|
} |
|
|
|
options = normalizeOptionTypes({ ...defaultOptions, ...options }) |
|
|
|
let offset = offsets.create('components') |
|
|
|
for (let identifier in components) { |
|
let prefixedIdentifier = prefixIdentifier(identifier, options) |
|
let rule = components[identifier] |
|
|
|
classList.add([prefixedIdentifier, options]) |
|
|
|
function wrapped(modifier, { isOnlyPlugin }) { |
|
let [value, coercedType, utilityModifier] = coerceValue( |
|
options.types, |
|
modifier, |
|
options, |
|
tailwindConfig |
|
) |
|
|
|
if (value === undefined) { |
|
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 (!isSyntacticallyValidPropertyValue(value)) { |
|
return [] |
|
} |
|
|
|
let extras = { |
|
get modifier() { |
|
if (!options.modifiers) { |
|
log.warn(`modifier-used-without-options-for-${identifier}`, [ |
|
'Your plugin must set `modifiers: true` in its options to support modifiers.', |
|
]) |
|
} |
|
|
|
return utilityModifier |
|
}, |
|
} |
|
|
|
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') |
|
|
|
let ruleSets = [] |
|
.concat(modifiersEnabled ? rule(value, extras) : rule(value)) |
|
.filter(Boolean) |
|
.map((declaration) => ({ |
|
[nameClass(identifier, modifier)]: declaration, |
|
})) |
|
|
|
return ruleSets |
|
} |
|
|
|
let withOffsets = [{ sort: offset, layer: 'components', options }, wrapped] |
|
|
|
if (!context.candidateRuleMap.has(prefixedIdentifier)) { |
|
context.candidateRuleMap.set(prefixedIdentifier, []) |
|
} |
|
|
|
context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) |
|
} |
|
}, |
|
addVariant(variantName, variantFunctions, options = {}) { |
|
variantFunctions = [].concat(variantFunctions).map((variantFunction) => { |
|
if (typeof variantFunction !== 'string') { |
|
|
|
return (api = {}) => { |
|
let { args, modifySelectors, container, separator, wrap, format } = api |
|
let result = variantFunction( |
|
Object.assign( |
|
{ modifySelectors, container, separator }, |
|
options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format } |
|
) |
|
) |
|
|
|
if (typeof result === 'string' && !isValidVariantFormatString(result)) { |
|
throw new Error( |
|
`Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.` |
|
) |
|
} |
|
|
|
if (Array.isArray(result)) { |
|
return result |
|
.filter((variant) => typeof variant === 'string') |
|
.map((variant) => parseVariant(variant)) |
|
} |
|
|
|
|
|
|
|
return result && typeof result === 'string' && parseVariant(result)(api) |
|
} |
|
} |
|
|
|
if (!isValidVariantFormatString(variantFunction)) { |
|
throw new Error( |
|
`Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.` |
|
) |
|
} |
|
|
|
return parseVariant(variantFunction) |
|
}) |
|
|
|
insertInto(variantList, variantName, options) |
|
variantMap.set(variantName, variantFunctions) |
|
context.variantOptions.set(variantName, options) |
|
}, |
|
matchVariant(variant, variantFn, options) { |
|
|
|
|
|
let id = options?.id ?? ++variantIdentifier |
|
let isSpecial = variant === '@' |
|
|
|
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') |
|
|
|
for (let [key, value] of Object.entries(options?.values ?? {})) { |
|
if (key === 'DEFAULT') continue |
|
|
|
api.addVariant( |
|
isSpecial ? `${variant}${key}` : `${variant}-${key}`, |
|
({ args, container }) => { |
|
return variantFn( |
|
value, |
|
modifiersEnabled ? { modifier: args?.modifier, container } : { container } |
|
) |
|
}, |
|
|
|
{ |
|
...options, |
|
value, |
|
id, |
|
type: VARIANT_TYPES.MatchVariant, |
|
variantInfo: VARIANT_INFO.Base, |
|
} |
|
) |
|
} |
|
|
|
let hasDefault = 'DEFAULT' in (options?.values ?? {}) |
|
|
|
api.addVariant( |
|
variant, |
|
({ args, container }) => { |
|
if (args?.value === sharedState.NONE && !hasDefault) { |
|
return null |
|
} |
|
|
|
return variantFn( |
|
args?.value === sharedState.NONE |
|
? options.values.DEFAULT |
|
: |
|
|
|
args?.value ?? (typeof args === 'string' ? args : ''), |
|
modifiersEnabled ? { modifier: args?.modifier, container } : { container } |
|
) |
|
}, |
|
{ |
|
...options, |
|
id, |
|
type: VARIANT_TYPES.MatchVariant, |
|
variantInfo: VARIANT_INFO.Dynamic, |
|
} |
|
) |
|
}, |
|
} |
|
|
|
return api |
|
} |
|
|
|
let fileModifiedMapCache = new WeakMap() |
|
export function getFileModifiedMap(context) { |
|
if (!fileModifiedMapCache.has(context)) { |
|
fileModifiedMapCache.set(context, new Map()) |
|
} |
|
return fileModifiedMapCache.get(context) |
|
} |
|
|
|
function trackModified(files, fileModifiedMap) { |
|
let changed = false |
|
let mtimesToCommit = new Map() |
|
|
|
for (let file of files) { |
|
if (!file) continue |
|
|
|
let parsed = url.parse(file) |
|
let pathname = parsed.hash ? parsed.href.replace(parsed.hash, '') : parsed.href |
|
pathname = parsed.search ? pathname.replace(parsed.search, '') : pathname |
|
let newModified = fs.statSync(decodeURIComponent(pathname), { throwIfNoEntry: false })?.mtimeMs |
|
if (!newModified) { |
|
|
|
|
|
|
|
|
|
continue |
|
} |
|
|
|
if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { |
|
changed = true |
|
} |
|
|
|
mtimesToCommit.set(file, newModified) |
|
} |
|
|
|
return [changed, mtimesToCommit] |
|
} |
|
|
|
function extractVariantAtRules(node) { |
|
node.walkAtRules((atRule) => { |
|
if (['responsive', 'variants'].includes(atRule.name)) { |
|
extractVariantAtRules(atRule) |
|
atRule.before(atRule.nodes) |
|
atRule.remove() |
|
} |
|
}) |
|
} |
|
|
|
function collectLayerPlugins(root) { |
|
let layerPlugins = [] |
|
|
|
root.each((node) => { |
|
if (node.type === 'atrule' && ['responsive', 'variants'].includes(node.name)) { |
|
node.name = 'layer' |
|
node.params = 'utilities' |
|
} |
|
}) |
|
|
|
|
|
root.walkAtRules('layer', (layerRule) => { |
|
extractVariantAtRules(layerRule) |
|
|
|
if (layerRule.params === 'base') { |
|
for (let node of layerRule.nodes) { |
|
layerPlugins.push(function ({ addBase }) { |
|
addBase(node, { respectPrefix: false }) |
|
}) |
|
} |
|
layerRule.remove() |
|
} else if (layerRule.params === 'components') { |
|
for (let node of layerRule.nodes) { |
|
layerPlugins.push(function ({ addComponents }) { |
|
addComponents(node, { respectPrefix: false, preserveSource: true }) |
|
}) |
|
} |
|
layerRule.remove() |
|
} else if (layerRule.params === 'utilities') { |
|
for (let node of layerRule.nodes) { |
|
layerPlugins.push(function ({ addUtilities }) { |
|
addUtilities(node, { respectPrefix: false, preserveSource: true }) |
|
}) |
|
} |
|
layerRule.remove() |
|
} |
|
}) |
|
|
|
return layerPlugins |
|
} |
|
|
|
function resolvePlugins(context, root) { |
|
let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins }) |
|
.map(([name, plugin]) => { |
|
if (!context.tailwindConfig.corePlugins.includes(name)) { |
|
return null |
|
} |
|
|
|
return plugin |
|
}) |
|
.filter(Boolean) |
|
|
|
let userPlugins = context.tailwindConfig.plugins.map((plugin) => { |
|
if (plugin.__isOptionsFunction) { |
|
plugin = plugin() |
|
} |
|
|
|
return typeof plugin === 'function' ? plugin : plugin.handler |
|
}) |
|
|
|
let layerPlugins = collectLayerPlugins(root) |
|
|
|
|
|
|
|
let beforeVariants = [ |
|
variantPlugins['childVariant'], |
|
variantPlugins['pseudoElementVariants'], |
|
variantPlugins['pseudoClassVariants'], |
|
variantPlugins['hasVariants'], |
|
variantPlugins['ariaVariants'], |
|
variantPlugins['dataVariants'], |
|
] |
|
let afterVariants = [ |
|
variantPlugins['supportsVariants'], |
|
variantPlugins['reducedMotionVariants'], |
|
variantPlugins['prefersContrastVariants'], |
|
variantPlugins['screenVariants'], |
|
variantPlugins['orientationVariants'], |
|
variantPlugins['directionVariants'], |
|
variantPlugins['darkVariants'], |
|
variantPlugins['forcedColorsVariants'], |
|
variantPlugins['printVariant'], |
|
] |
|
|
|
|
|
|
|
let isLegacyDarkMode = |
|
context.tailwindConfig.darkMode === 'class' || |
|
(Array.isArray(context.tailwindConfig.darkMode) && |
|
context.tailwindConfig.darkMode[0] === 'class') |
|
|
|
if (isLegacyDarkMode) { |
|
afterVariants = [ |
|
variantPlugins['supportsVariants'], |
|
variantPlugins['reducedMotionVariants'], |
|
variantPlugins['prefersContrastVariants'], |
|
variantPlugins['darkVariants'], |
|
variantPlugins['screenVariants'], |
|
variantPlugins['orientationVariants'], |
|
variantPlugins['directionVariants'], |
|
variantPlugins['forcedColorsVariants'], |
|
variantPlugins['printVariant'], |
|
] |
|
} |
|
|
|
return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins] |
|
} |
|
|
|
function registerPlugins(plugins, context) { |
|
let variantList = [] |
|
let variantMap = new Map() |
|
context.variantMap = variantMap |
|
|
|
let offsets = new Offsets() |
|
context.offsets = offsets |
|
|
|
let classList = new Set() |
|
|
|
let pluginApi = buildPluginApi(context.tailwindConfig, context, { |
|
variantList, |
|
variantMap, |
|
offsets, |
|
classList, |
|
}) |
|
|
|
for (let plugin of plugins) { |
|
if (Array.isArray(plugin)) { |
|
for (let pluginItem of plugin) { |
|
pluginItem(pluginApi) |
|
} |
|
} else { |
|
plugin?.(pluginApi) |
|
} |
|
} |
|
|
|
|
|
offsets.recordVariants(variantList, (variant) => variantMap.get(variant).length) |
|
|
|
|
|
for (let [variantName, variantFunctions] of variantMap.entries()) { |
|
context.variantMap.set( |
|
variantName, |
|
variantFunctions.map((variantFunction, idx) => [ |
|
offsets.forVariant(variantName, idx), |
|
variantFunction, |
|
]) |
|
) |
|
} |
|
|
|
let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean) |
|
if (safelist.length > 0) { |
|
let checks = [] |
|
|
|
for (let value of safelist) { |
|
if (typeof value === 'string') { |
|
context.changedContent.push({ content: value, extension: 'html' }) |
|
continue |
|
} |
|
|
|
if (value instanceof RegExp) { |
|
log.warn('root-regex', [ |
|
'Regular expressions in `safelist` work differently in Tailwind CSS v3.0.', |
|
'Update your `safelist` configuration to eliminate this warning.', |
|
'https://tailwindcss.com/docs/content-configuration#safelisting-classes', |
|
]) |
|
continue |
|
} |
|
|
|
checks.push(value) |
|
} |
|
|
|
if (checks.length > 0) { |
|
let patternMatchingCount = new Map() |
|
let prefixLength = context.tailwindConfig.prefix.length |
|
let checkImportantUtils = checks.some((check) => check.pattern.source.includes('!')) |
|
|
|
for (let util of classList) { |
|
let utils = Array.isArray(util) |
|
? (() => { |
|
let [utilName, options] = util |
|
let values = Object.keys(options?.values ?? {}) |
|
let classes = values.map((value) => formatClass(utilName, value)) |
|
|
|
if (options?.supportsNegativeValues) { |
|
|
|
|
|
classes = [...classes, ...classes.map((cls) => '-' + cls)] |
|
|
|
|
|
|
|
|
|
|
|
classes = [ |
|
...classes, |
|
...classes.map( |
|
(cls) => cls.slice(0, prefixLength) + '-' + cls.slice(prefixLength) |
|
), |
|
] |
|
} |
|
|
|
if (options.types.some(({ type }) => type === 'color')) { |
|
classes = [ |
|
...classes, |
|
...classes.flatMap((cls) => |
|
Object.keys(context.tailwindConfig.theme.opacity).map( |
|
(opacity) => `${cls}/${opacity}` |
|
) |
|
), |
|
] |
|
} |
|
|
|
if (checkImportantUtils && options?.respectImportant) { |
|
classes = [...classes, ...classes.map((cls) => '!' + cls)] |
|
} |
|
|
|
return classes |
|
})() |
|
: [util] |
|
|
|
for (let util of utils) { |
|
for (let { pattern, variants = [] } of checks) { |
|
|
|
|
|
pattern.lastIndex = 0 |
|
|
|
if (!patternMatchingCount.has(pattern)) { |
|
patternMatchingCount.set(pattern, 0) |
|
} |
|
|
|
if (!pattern.test(util)) continue |
|
|
|
patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1) |
|
|
|
context.changedContent.push({ content: util, extension: 'html' }) |
|
for (let variant of variants) { |
|
context.changedContent.push({ |
|
content: variant + context.tailwindConfig.separator + util, |
|
extension: 'html', |
|
}) |
|
} |
|
} |
|
} |
|
} |
|
|
|
for (let [regex, count] of patternMatchingCount.entries()) { |
|
if (count !== 0) continue |
|
|
|
log.warn([ |
|
`The safelist pattern \`${regex}\` doesn't match any Tailwind CSS classes.`, |
|
'Fix this pattern or remove it from your `safelist` configuration.', |
|
'https://tailwindcss.com/docs/content-configuration#safelisting-classes', |
|
]) |
|
} |
|
} |
|
} |
|
|
|
let darkClassName = [].concat(context.tailwindConfig.darkMode ?? 'media')[1] ?? 'dark' |
|
|
|
|
|
|
|
|
|
|
|
let parasiteUtilities = [ |
|
prefix(context, darkClassName), |
|
prefix(context, 'group'), |
|
prefix(context, 'peer'), |
|
] |
|
context.getClassOrder = function getClassOrder(classes) { |
|
|
|
let sorted = [...classes].sort((a, z) => { |
|
if (a === z) return 0 |
|
if (a < z) return -1 |
|
return 1 |
|
}) |
|
|
|
|
|
let sortedClassNames = new Map(sorted.map((className) => [className, null])) |
|
|
|
|
|
|
|
let rules = generateRules(new Set(sorted), context, true) |
|
rules = context.offsets.sort(rules) |
|
|
|
let idx = BigInt(parasiteUtilities.length) |
|
|
|
for (const [, rule] of rules) { |
|
let candidate = rule.raws.tailwind.candidate |
|
|
|
|
|
|
|
sortedClassNames.set(candidate, sortedClassNames.get(candidate) ?? idx++) |
|
} |
|
|
|
return classes.map((className) => { |
|
let order = sortedClassNames.get(className) ?? null |
|
let parasiteIndex = parasiteUtilities.indexOf(className) |
|
|
|
if (order === null && parasiteIndex !== -1) { |
|
|
|
|
|
|
|
order = BigInt(parasiteIndex) |
|
} |
|
|
|
return [className, order] |
|
}) |
|
} |
|
|
|
|
|
|
|
context.getClassList = function getClassList(options = {}) { |
|
let output = [] |
|
|
|
for (let util of classList) { |
|
if (Array.isArray(util)) { |
|
let [utilName, utilOptions] = util |
|
let negativeClasses = [] |
|
|
|
let modifiers = Object.keys(utilOptions?.modifiers ?? {}) |
|
|
|
if (utilOptions?.types?.some(({ type }) => type === 'color')) { |
|
modifiers.push(...Object.keys(context.tailwindConfig.theme.opacity ?? {})) |
|
} |
|
|
|
let metadata = { modifiers } |
|
let includeMetadata = options.includeMetadata && modifiers.length > 0 |
|
|
|
for (let [key, value] of Object.entries(utilOptions?.values ?? {})) { |
|
|
|
if (value == null) { |
|
continue |
|
} |
|
|
|
let cls = formatClass(utilName, key) |
|
output.push(includeMetadata ? [cls, metadata] : cls) |
|
|
|
if (utilOptions?.supportsNegativeValues && negateValue(value)) { |
|
let cls = formatClass(utilName, `-${key}`) |
|
negativeClasses.push(includeMetadata ? [cls, metadata] : cls) |
|
} |
|
} |
|
|
|
output.push(...negativeClasses) |
|
} else { |
|
output.push(util) |
|
} |
|
} |
|
|
|
return output |
|
} |
|
|
|
|
|
context.getVariants = function getVariants() { |
|
let result = [] |
|
for (let [name, options] of context.variantOptions.entries()) { |
|
if (options.variantInfo === VARIANT_INFO.Base) continue |
|
|
|
result.push({ |
|
name, |
|
isArbitrary: options.type === Symbol.for('MATCH_VARIANT'), |
|
values: Object.keys(options.values ?? {}), |
|
hasDash: name !== '@', |
|
selectors({ modifier, value } = {}) { |
|
let candidate = '__TAILWIND_PLACEHOLDER__' |
|
|
|
let rule = postcss.rule({ selector: `.${candidate}` }) |
|
let container = postcss.root({ nodes: [rule.clone()] }) |
|
|
|
let before = container.toString() |
|
|
|
let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn) |
|
let formatStrings = [] |
|
for (let fn of fns) { |
|
let localFormatStrings = [] |
|
|
|
let api = { |
|
args: { modifier, value: options.values?.[value] ?? value }, |
|
separator: context.tailwindConfig.separator, |
|
modifySelectors(modifierFunction) { |
|
|
|
container.each((rule) => { |
|
if (rule.type !== 'rule') { |
|
return |
|
} |
|
|
|
rule.selectors = rule.selectors.map((selector) => { |
|
return modifierFunction({ |
|
get className() { |
|
return getClassNameFromSelector(selector) |
|
}, |
|
selector, |
|
}) |
|
}) |
|
}) |
|
|
|
return container |
|
}, |
|
format(str) { |
|
localFormatStrings.push(str) |
|
}, |
|
wrap(wrapper) { |
|
localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`) |
|
}, |
|
container, |
|
} |
|
|
|
let ruleWithVariant = fn(api) |
|
if (localFormatStrings.length > 0) { |
|
formatStrings.push(localFormatStrings) |
|
} |
|
|
|
if (Array.isArray(ruleWithVariant)) { |
|
for (let variantFunction of ruleWithVariant) { |
|
localFormatStrings = [] |
|
variantFunction(api) |
|
formatStrings.push(localFormatStrings) |
|
} |
|
} |
|
} |
|
|
|
|
|
let manualFormatStrings = [] |
|
let after = container.toString() |
|
|
|
if (before !== after) { |
|
|
|
container.walkRules((rule) => { |
|
let modified = rule.selector |
|
|
|
|
|
|
|
|
|
|
|
let rebuiltBase = selectorParser((selectors) => { |
|
selectors.walkClasses((classNode) => { |
|
classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}` |
|
}) |
|
}).processSync(modified) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&')) |
|
}) |
|
|
|
|
|
container.walkAtRules((atrule) => { |
|
manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`) |
|
}) |
|
} |
|
|
|
let isArbitraryVariant = !(value in (options.values ?? {})) |
|
let internalFeatures = options[INTERNAL_FEATURES] ?? {} |
|
|
|
let respectPrefix = (() => { |
|
if (isArbitraryVariant) return false |
|
if (internalFeatures.respectPrefix === false) return false |
|
return true |
|
})() |
|
|
|
formatStrings = formatStrings.map((format) => |
|
format.map((str) => ({ |
|
format: str, |
|
respectPrefix, |
|
})) |
|
) |
|
|
|
manualFormatStrings = manualFormatStrings.map((format) => ({ |
|
format, |
|
respectPrefix, |
|
})) |
|
|
|
let opts = { |
|
candidate, |
|
context, |
|
} |
|
|
|
let result = formatStrings.map((formats) => |
|
finalizeSelector(`.${candidate}`, formatVariantSelector(formats, opts), opts) |
|
.replace(`.${candidate}`, '&') |
|
.replace('{ & }', '') |
|
.trim() |
|
) |
|
|
|
if (manualFormatStrings.length > 0) { |
|
result.push( |
|
formatVariantSelector(manualFormatStrings, opts) |
|
.toString() |
|
.replace(`.${candidate}`, '&') |
|
) |
|
} |
|
|
|
return result |
|
}, |
|
}) |
|
} |
|
|
|
return result |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function markInvalidUtilityCandidate(context, candidate) { |
|
if (!context.classCache.has(candidate)) { |
|
return |
|
} |
|
|
|
|
|
context.notClassCache.add(candidate) |
|
|
|
|
|
context.classCache.delete(candidate) |
|
context.applyClassCache.delete(candidate) |
|
context.candidateRuleMap.delete(candidate) |
|
context.candidateRuleCache.delete(candidate) |
|
|
|
|
|
context.stylesheetCache = null |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function markInvalidUtilityNode(context, node) { |
|
let candidate = node.raws.tailwind.candidate |
|
|
|
if (!candidate) { |
|
return |
|
} |
|
|
|
for (const entry of context.ruleCache) { |
|
if (entry[1].raws.tailwind.candidate === candidate) { |
|
context.ruleCache.delete(entry) |
|
|
|
} |
|
} |
|
|
|
markInvalidUtilityCandidate(context, candidate) |
|
} |
|
|
|
export function createContext(tailwindConfig, changedContent = [], root = postcss.root()) { |
|
let context = { |
|
disposables: [], |
|
ruleCache: new Set(), |
|
candidateRuleCache: new Map(), |
|
classCache: new Map(), |
|
applyClassCache: new Map(), |
|
|
|
notClassCache: new Set(tailwindConfig.blocklist ?? []), |
|
postCssNodeCache: new Map(), |
|
candidateRuleMap: new Map(), |
|
tailwindConfig, |
|
changedContent: changedContent, |
|
variantMap: new Map(), |
|
stylesheetCache: null, |
|
variantOptions: new Map(), |
|
|
|
markInvalidUtilityCandidate: (candidate) => markInvalidUtilityCandidate(context, candidate), |
|
markInvalidUtilityNode: (node) => markInvalidUtilityNode(context, node), |
|
} |
|
|
|
let resolvedPlugins = resolvePlugins(context, root) |
|
registerPlugins(resolvedPlugins, context) |
|
|
|
return context |
|
} |
|
|
|
let contextMap = sharedState.contextMap |
|
let configContextMap = sharedState.configContextMap |
|
let contextSourcesMap = sharedState.contextSourcesMap |
|
|
|
export function getContext( |
|
root, |
|
result, |
|
tailwindConfig, |
|
userConfigPath, |
|
tailwindConfigHash, |
|
contextDependencies |
|
) { |
|
let sourcePath = result.opts.from |
|
let isConfigFile = userConfigPath !== null |
|
|
|
env.DEBUG && console.log('Source path:', sourcePath) |
|
|
|
let existingContext |
|
|
|
if (isConfigFile && contextMap.has(sourcePath)) { |
|
existingContext = contextMap.get(sourcePath) |
|
} else if (configContextMap.has(tailwindConfigHash)) { |
|
let context = configContextMap.get(tailwindConfigHash) |
|
contextSourcesMap.get(context).add(sourcePath) |
|
contextMap.set(sourcePath, context) |
|
|
|
existingContext = context |
|
} |
|
|
|
let cssDidChange = hasContentChanged(sourcePath, root) |
|
|
|
|
|
|
|
if (existingContext) { |
|
let [contextDependenciesChanged, mtimesToCommit] = trackModified( |
|
[...contextDependencies], |
|
getFileModifiedMap(existingContext) |
|
) |
|
if (!contextDependenciesChanged && !cssDidChange) { |
|
return [existingContext, false, mtimesToCommit] |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (contextMap.has(sourcePath)) { |
|
let oldContext = contextMap.get(sourcePath) |
|
if (contextSourcesMap.has(oldContext)) { |
|
contextSourcesMap.get(oldContext).delete(sourcePath) |
|
if (contextSourcesMap.get(oldContext).size === 0) { |
|
contextSourcesMap.delete(oldContext) |
|
for (let [tailwindConfigHash, context] of configContextMap) { |
|
if (context === oldContext) { |
|
configContextMap.delete(tailwindConfigHash) |
|
} |
|
} |
|
for (let disposable of oldContext.disposables.splice(0)) { |
|
disposable(oldContext) |
|
} |
|
} |
|
} |
|
} |
|
|
|
env.DEBUG && console.log('Setting up new context...') |
|
|
|
let context = createContext(tailwindConfig, [], root) |
|
|
|
Object.assign(context, { |
|
userConfigPath, |
|
}) |
|
|
|
let [, mtimesToCommit] = trackModified([...contextDependencies], getFileModifiedMap(context)) |
|
|
|
|
|
|
|
|
|
|
|
configContextMap.set(tailwindConfigHash, context) |
|
contextMap.set(sourcePath, context) |
|
|
|
if (!contextSourcesMap.has(context)) { |
|
contextSourcesMap.set(context, new Set()) |
|
} |
|
|
|
contextSourcesMap.get(context).add(sourcePath) |
|
|
|
return [context, true, mtimesToCommit] |
|
} |
|
|