tailwindcss
Advanced tools
Comparing version 3.3.3 to 3.3.4
@@ -129,3 +129,3 @@ // @ts-check | ||
watcher.on("raw", (evt, filePath, meta)=>{ | ||
if (evt !== "rename") { | ||
if (evt !== "rename" || filePath === null) { | ||
return; | ||
@@ -132,0 +132,0 @@ } |
@@ -11,3 +11,2 @@ "use strict"; | ||
}); | ||
const _featureFlags = require("../featureFlags"); | ||
const _regex = /*#__PURE__*/ _interop_require_wildcard(require("./regex")); | ||
@@ -61,8 +60,7 @@ function _getRequireWildcardCache(nodeInterop) { | ||
var _content_match; | ||
results = [ | ||
...results, | ||
...(_content_match = content.match(pattern)) !== null && _content_match !== void 0 ? _content_match : [] | ||
]; | ||
for (let result of (_content_match = content.match(pattern)) !== null && _content_match !== void 0 ? _content_match : []){ | ||
results.push(clipAtBalancedParens(result)); | ||
} | ||
} | ||
return results.filter((v)=>v !== undefined).map(clipAtBalancedParens); | ||
return results; | ||
}; | ||
@@ -72,3 +70,2 @@ } | ||
let separator = context.tailwindConfig.separator; | ||
let variantGroupingEnabled = (0, _featureFlags.flagEnabled)(context.tailwindConfig, "variantGrouping"); | ||
let prefix = context.tailwindConfig.prefix !== "" ? _regex.optional(_regex.pattern([ | ||
@@ -85,3 +82,3 @@ /-?/, | ||
// while fixing a problem with the regex matching too much | ||
/\[[^\s:'"`]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, | ||
/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, | ||
// Utilities | ||
@@ -122,3 +119,8 @@ _regex.pattern([ | ||
]), | ||
// With variant modifier (e.g.: group-[..]/modifier) | ||
_regex.pattern([ | ||
/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, | ||
separator | ||
]), | ||
_regex.pattern([ | ||
/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, | ||
@@ -134,3 +136,8 @@ separator | ||
_regex.any([ | ||
// With variant modifier (e.g.: group-[..]/modifier) | ||
_regex.pattern([ | ||
/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, | ||
separator | ||
]), | ||
_regex.pattern([ | ||
/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, | ||
@@ -154,16 +161,3 @@ separator | ||
prefix, | ||
variantGroupingEnabled ? _regex.any([ | ||
// Or any of those things but grouped separated by commas | ||
_regex.pattern([ | ||
/\(/, | ||
utility, | ||
_regex.zeroOrMore([ | ||
/,/, | ||
utility | ||
]), | ||
/\)/ | ||
]), | ||
// Arbitrary properties, constrained utilities, arbitrary values, etc… | ||
utility | ||
]) : utility | ||
utility | ||
]); | ||
@@ -170,0 +164,0 @@ } |
@@ -485,2 +485,8 @@ "use strict"; | ||
let parentSelector = isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0 ? parent.selector.slice(importantSelector.length) : parent.selector; | ||
// If the selector becomes empty after replacing the important selector | ||
// This means that it's the same as the parent selector and we don't want to replace it | ||
// Otherwise we'll crash | ||
if (parentSelector === "") { | ||
parentSelector = parent.selector; | ||
} | ||
rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate); | ||
@@ -487,0 +493,0 @@ // And then re-add it if it was removed |
@@ -168,8 +168,22 @@ "use strict"; | ||
} else { | ||
await Promise.all(context.changedContent.map(async ({ file , content , extension })=>{ | ||
let transformer = getTransformer(context.tailwindConfig, extension); | ||
let extractor = getExtractor(context, extension); | ||
content = file ? await _fs.default.promises.readFile(file, "utf8") : content; | ||
getClassCandidates(transformer(content), extractor, candidates, seen); | ||
})); | ||
/** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */ let regexParserContent = []; | ||
for (let item of context.changedContent){ | ||
let transformer = getTransformer(context.tailwindConfig, item.extension); | ||
let extractor = getExtractor(context, item.extension); | ||
regexParserContent.push([ | ||
item, | ||
{ | ||
transformer, | ||
extractor | ||
} | ||
]); | ||
} | ||
const BATCH_SIZE = 500; | ||
for(let i = 0; i < regexParserContent.length; i += BATCH_SIZE){ | ||
let batch = regexParserContent.slice(i, i + BATCH_SIZE); | ||
await Promise.all(batch.map(async ([{ file , content }, { transformer , extractor }])=>{ | ||
content = file ? await _fs.default.promises.readFile(file, "utf8") : content; | ||
getClassCandidates(transformer(content), extractor, candidates, seen); | ||
})); | ||
} | ||
} | ||
@@ -176,0 +190,0 @@ env.DEBUG && console.timeEnd("Reading changed files"); |
@@ -503,3 +503,5 @@ "use strict"; | ||
} | ||
let normalized = (0, _dataTypes.normalize)(value); | ||
let normalized = (0, _dataTypes.normalize)(value, { | ||
property | ||
}); | ||
if (!isParsableCssValue(property, normalized)) { | ||
@@ -582,3 +584,3 @@ return null; | ||
} | ||
function* resolveMatches(candidate, context, original = candidate) { | ||
function* resolveMatches(candidate, context) { | ||
let separator = context.tailwindConfig.separator; | ||
@@ -591,10 +593,2 @@ let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse(); | ||
} | ||
if ((0, _featureFlags.flagEnabled)(context.tailwindConfig, "variantGrouping")) { | ||
if (classCandidate.startsWith("(") && classCandidate.endsWith(")")) { | ||
let base = variants.slice().reverse().join(separator); | ||
for (let part of (0, _splitAtTopLevelOnly.splitAtTopLevelOnly)(classCandidate.slice(1, -1), ",")){ | ||
yield* resolveMatches(base + separator + part, context, original); | ||
} | ||
} | ||
} | ||
// TODO: Reintroduce this in ways that doesn't break on false positives | ||
@@ -759,4 +753,3 @@ // function sortAgainst(toSort, against) { | ||
context, | ||
candidate, | ||
original | ||
candidate | ||
}); | ||
@@ -773,3 +766,3 @@ // Skip rules with invalid selectors | ||
} | ||
function applyFinalFormat(match, { context , candidate , original }) { | ||
function applyFinalFormat(match, { context , candidate }) { | ||
if (!match[0].collectedFormats) { | ||
@@ -807,6 +800,13 @@ return match; | ||
try { | ||
rule.selector = (0, _formatVariantSelector.finalizeSelector)(rule.selector, finalFormat, { | ||
candidate: original, | ||
let selector = (0, _formatVariantSelector.finalizeSelector)(rule.selector, finalFormat, { | ||
candidate, | ||
context | ||
}); | ||
// Finalize Selector determined that this candidate is irrelevant | ||
// TODO: This elimination should happen earlier so this never happens | ||
if (selector === null) { | ||
rule.remove(); | ||
return; | ||
} | ||
rule.selector = selector; | ||
} catch { | ||
@@ -852,3 +852,3 @@ // If this selector is invalid we also want to skip it | ||
} | ||
function generateRules(candidates, context) { | ||
function generateRules(candidates, context, isSorting = false) { | ||
let allRules = []; | ||
@@ -884,5 +884,7 @@ let strategy = getImportantStrategy(context.tailwindConfig.important); | ||
} | ||
// Note: We have to clone rules during sorting | ||
// so we eliminate some shared mutable state | ||
let newEntry = [ | ||
sort, | ||
rule | ||
isSorting ? rule.clone() : rule | ||
]; | ||
@@ -889,0 +891,0 @@ rules.add(newEntry); |
@@ -204,2 +204,16 @@ "use strict"; | ||
} | ||
/** | ||
* Ignore everything inside a :not(...). This allows you to write code like | ||
* `div:not(.foo)`. If `.foo` is never found in your code, then we used to | ||
* not generated it. But now we will ignore everything inside a `:not`, so | ||
* that it still gets generated. | ||
* | ||
* @param {selectorParser.Root} selectors | ||
*/ function ignoreNot(selectors) { | ||
selectors.walkPseudos((pseudo)=>{ | ||
if (pseudo.value === ":not") { | ||
pseudo.remove(); | ||
} | ||
}); | ||
} | ||
function extractCandidates(node, state = { | ||
@@ -209,31 +223,19 @@ containsNonOnDemandable: false | ||
let classes = []; | ||
// Handle normal rules | ||
let selectors = []; | ||
if (node.type === "rule") { | ||
// Ignore everything inside a :not(...). This allows you to write code like | ||
// `div:not(.foo)`. If `.foo` is never found in your code, then we used to | ||
// not generated it. But now we will ignore everything inside a `:not`, so | ||
// that it still gets generated. | ||
function ignoreNot(selectors) { | ||
selectors.walkPseudos((pseudo)=>{ | ||
if (pseudo.value === ":not") { | ||
pseudo.remove(); | ||
} | ||
}); | ||
// Handle normal rules | ||
selectors.push(...node.selectors); | ||
} else if (node.type === "atrule") { | ||
// Handle at-rules (which contains nested rules) | ||
node.walkRules((rule)=>selectors.push(...rule.selectors)); | ||
} | ||
for (let selector of selectors){ | ||
let classCandidates = getClasses(selector, ignoreNot); | ||
// At least one of the selectors contains non-"on-demandable" candidates. | ||
if (classCandidates.length === 0) { | ||
state.containsNonOnDemandable = true; | ||
} | ||
for (let selector of node.selectors){ | ||
let classCandidates = getClasses(selector, ignoreNot); | ||
// At least one of the selectors contains non-"on-demandable" candidates. | ||
if (classCandidates.length === 0) { | ||
state.containsNonOnDemandable = true; | ||
} | ||
for (let classCandidate of classCandidates){ | ||
classes.push(classCandidate); | ||
} | ||
for (let classCandidate of classCandidates){ | ||
classes.push(classCandidate); | ||
} | ||
} else if (node.type === "atrule") { | ||
node.walkRules((rule)=>{ | ||
for (let classCandidate of rule.selectors.flatMap((selector)=>getClasses(selector))){ | ||
classes.push(classCandidate); | ||
} | ||
}); | ||
} | ||
@@ -932,3 +934,3 @@ if (depth === 0) { | ||
// Non-tailwind classes won't be generated and will be left as `null` | ||
let rules = (0, _generateRules.generateRules)(new Set(sorted), context); | ||
let rules = (0, _generateRules.generateRules)(new Set(sorted), context, true); | ||
rules = context.offsets.sort(rules); | ||
@@ -935,0 +937,0 @@ let idx = BigInt(parasiteUtilities.length); |
@@ -247,5 +247,5 @@ "use strict"; | ||
postcssPlugin: "tailwindcss", | ||
Once (root, { result }) { | ||
async Once (root, { result }) { | ||
_sharedState.env.DEBUG && console.time("Compiling CSS"); | ||
(0, _processTailwindFeatures.default)(({ createContext })=>{ | ||
await (0, _processTailwindFeatures.default)(({ createContext })=>{ | ||
console.error(); | ||
@@ -252,0 +252,0 @@ console.error("Rebuilding..."); |
@@ -30,3 +30,3 @@ "use strict"; | ||
let ALPHA_SEP = /\s*[,/]\s*/; | ||
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)\)/; | ||
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/; | ||
let RGB = new RegExp(`^(rgba?)\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`); | ||
@@ -33,0 +33,0 @@ let HSL = new RegExp(`^(hsla?)\\(\\s*((?:${VALUE.source})(?:deg|rad|grad|turn)?|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`); |
@@ -71,6 +71,26 @@ "use strict"; | ||
} | ||
const placeholder = "--tw-placeholder"; | ||
const placeholderRe = new RegExp(placeholder, "g"); | ||
function normalize(value, isRoot = true) { | ||
if (value.startsWith("--")) { | ||
// These properties accept a `<dashed-ident>` as one of the values. This means that you can use them | ||
// as: `timeline-scope: --tl;` | ||
// | ||
// Without the `var(--tl)`, in these cases we don't want to normalize the value, and you should add | ||
// the `var()` yourself. | ||
// | ||
// More info: | ||
// - https://drafts.csswg.org/scroll-animations/#propdef-timeline-scope | ||
// - https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope#dashed-ident | ||
// | ||
const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([ | ||
// Concrete properties | ||
"scroll-timeline-name", | ||
"timeline-scope", | ||
"view-timeline-name", | ||
"font-palette", | ||
// Shorthand properties | ||
"scroll-timeline", | ||
"animation-timeline", | ||
"view-timeline" | ||
]); | ||
function normalize(value, context = null, isRoot = true) { | ||
let isVarException = context && AUTO_VAR_INJECTION_EXCEPTIONS.has(context.property); | ||
if (value.startsWith("--") && !isVarException) { | ||
return `var(${value})`; | ||
@@ -84,3 +104,3 @@ } | ||
} | ||
return normalize(part, false); | ||
return normalize(part, context, false); | ||
}).join(""); | ||
@@ -104,8 +124,64 @@ } | ||
*/ function normalizeMathOperatorSpacing(value) { | ||
let preventFormattingInFunctions = [ | ||
"theme" | ||
]; | ||
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match)=>{ | ||
let vars = []; | ||
return match.replace(/var\((--.+?)[,)]/g, (match, g1)=>{ | ||
vars.push(g1); | ||
return match.replace(g1, placeholder); | ||
}).replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, "$1 $2 ").replace(placeholderRe, ()=>vars.shift()); | ||
let result = ""; | ||
function lastChar() { | ||
let char = result.trimEnd(); | ||
return char[char.length - 1]; | ||
} | ||
for(let i = 0; i < match.length; i++){ | ||
function peek(word) { | ||
return word.split("").every((char, j)=>match[i + j] === char); | ||
} | ||
function consumeUntil(chars) { | ||
let minIndex = Infinity; | ||
for (let char of chars){ | ||
let index = match.indexOf(char, i); | ||
if (index !== -1 && index < minIndex) { | ||
minIndex = index; | ||
} | ||
} | ||
let result = match.slice(i, minIndex); | ||
i += result.length - 1; | ||
return result; | ||
} | ||
let char = match[i]; | ||
// Handle `var(--variable)` | ||
if (peek("var")) { | ||
// When we consume until `)`, then we are dealing with this scenario: | ||
// `var(--example)` | ||
// | ||
// When we consume until `,`, then we are dealing with this scenario: | ||
// `var(--example, 1rem)` | ||
// | ||
// In this case we do want to "format", the default value as well | ||
result += consumeUntil([ | ||
")", | ||
"," | ||
]); | ||
} else if (preventFormattingInFunctions.some((fn)=>peek(fn))) { | ||
result += consumeUntil([ | ||
")" | ||
]); | ||
} else if ([ | ||
"+", | ||
"-", | ||
"*", | ||
"/" | ||
].includes(char) && ![ | ||
"(", | ||
"+", | ||
"-", | ||
"*", | ||
"/" | ||
].includes(lastChar())) { | ||
result += ` ${char} `; | ||
} else { | ||
result += char; | ||
} | ||
} | ||
// Simplify multiple spaces | ||
return result.replace(/\s+/g, " "); | ||
}); | ||
@@ -112,0 +188,0 @@ } |
@@ -30,2 +30,3 @@ "use strict"; | ||
const _pseudoElements = require("./pseudoElements"); | ||
const _splitAtTopLevelOnly = require("./splitAtTopLevelOnly"); | ||
function _interop_require_default(obj) { | ||
@@ -142,3 +143,3 @@ return obj && obj.__esModule ? obj : { | ||
// | ||
base = base !== null && base !== void 0 ? base : candidate.split(new RegExp(`\\${separator}(?![^[]*\\])`)).pop(); | ||
base = base !== null && base !== void 0 ? base : (0, _splitAtTopLevelOnly.splitAtTopLevelOnly)(candidate, separator).pop(); | ||
// Parse the selector into an AST | ||
@@ -164,2 +165,8 @@ let selector = (0, _postcssselectorparser.default)().astSync(current); | ||
selector.each((sel)=>eliminateIrrelevantSelectors(sel, base)); | ||
// If ffter eliminating irrelevant selectors, we end up with nothing | ||
// Then the whole "rule" this is associated with does not need to exist | ||
// We use `null` as a marker value for that case | ||
if (selector.length === 0) { | ||
return null; | ||
} | ||
// If there are no formats that means there were no variants added to the candidate | ||
@@ -166,0 +173,0 @@ // so we can just return the selector as-is |
@@ -16,3 +16,3 @@ "use strict"; | ||
const prototype = Object.getPrototypeOf(value); | ||
return prototype === null || prototype === Object.prototype; | ||
return prototype === null || Object.getPrototypeOf(prototype) === null; | ||
} |
{ | ||
"name": "tailwindcss", | ||
"version": "3.3.3", | ||
"version": "3.3.4", | ||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.", | ||
@@ -61,4 +61,4 @@ "license": "MIT", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"jest": "^29.5.0", | ||
"jest-diff": "^29.5.0", | ||
"jest": "^29.6.0", | ||
"jest-diff": "^29.6.0", | ||
"lightningcss": "1.18.0", | ||
@@ -76,6 +76,6 @@ "prettier": "^2.8.8", | ||
"dlv": "^1.1.3", | ||
"fast-glob": "^3.2.12", | ||
"fast-glob": "^3.3.0", | ||
"glob-parent": "^6.0.2", | ||
"is-glob": "^4.0.3", | ||
"jiti": "^1.18.2", | ||
"jiti": "^1.19.1", | ||
"lilconfig": "^2.1.0", | ||
@@ -82,0 +82,0 @@ "micromatch": "^4.0.5", |
@@ -167,3 +167,3 @@ // @ts-check | ||
watcher.on('raw', (evt, filePath, meta) => { | ||
if (evt !== 'rename') { | ||
if (evt !== 'rename' || filePath === null) { | ||
return | ||
@@ -170,0 +170,0 @@ } |
@@ -25,3 +25,2 @@ import colors from 'picocolors' | ||
'generalizedModifiers', | ||
// 'variantGrouping', | ||
], | ||
@@ -28,0 +27,0 @@ } |
@@ -15,6 +15,8 @@ import { flagEnabled } from '../featureFlags' | ||
for (let pattern of patterns) { | ||
results = [...results, ...(content.match(pattern) ?? [])] | ||
for (let result of content.match(pattern) ?? []) { | ||
results.push(clipAtBalancedParens(result)) | ||
} | ||
} | ||
return results.filter((v) => v !== undefined).map(clipAtBalancedParens) | ||
return results | ||
} | ||
@@ -25,3 +27,2 @@ } | ||
let separator = context.tailwindConfig.separator | ||
let variantGroupingEnabled = flagEnabled(context.tailwindConfig, 'variantGrouping') | ||
let prefix = | ||
@@ -40,3 +41,3 @@ context.tailwindConfig.prefix !== '' | ||
// while fixing a problem with the regex matching too much | ||
/\[[^\s:'"`]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, | ||
/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, | ||
@@ -86,2 +87,5 @@ // Utilities | ||
// With variant modifier (e.g.: group-[..]/modifier) | ||
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, separator]), | ||
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]), | ||
@@ -93,2 +97,5 @@ regex.pattern([/[^\s"'`\[\\]+/, separator]), | ||
regex.any([ | ||
// With variant modifier (e.g.: group-[..]/modifier) | ||
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, separator]), | ||
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]), | ||
@@ -111,11 +118,3 @@ regex.pattern([/[^\s`\[\\]+/, separator]), | ||
variantGroupingEnabled | ||
? regex.any([ | ||
// Or any of those things but grouped separated by commas | ||
regex.pattern([/\(/, utility, regex.zeroOrMore([/,/, utility]), /\)/]), | ||
// Arbitrary properties, constrained utilities, arbitrary values, etc… | ||
utility, | ||
]) | ||
: utility, | ||
utility, | ||
]) | ||
@@ -122,0 +121,0 @@ } |
@@ -556,2 +556,9 @@ import postcss from 'postcss' | ||
// If the selector becomes empty after replacing the important selector | ||
// This means that it's the same as the parent selector and we don't want to replace it | ||
// Otherwise we'll crash | ||
if (parentSelector === '') { | ||
parentSelector = parent.selector | ||
} | ||
rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate) | ||
@@ -558,0 +565,0 @@ |
@@ -148,10 +148,22 @@ import fs from 'fs' | ||
} else { | ||
await Promise.all( | ||
context.changedContent.map(async ({ file, content, extension }) => { | ||
let transformer = getTransformer(context.tailwindConfig, extension) | ||
let extractor = getExtractor(context, extension) | ||
content = file ? await fs.promises.readFile(file, 'utf8') : content | ||
getClassCandidates(transformer(content), extractor, candidates, seen) | ||
}) | ||
) | ||
/** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */ | ||
let regexParserContent = [] | ||
for (let item of context.changedContent) { | ||
let transformer = getTransformer(context.tailwindConfig, item.extension) | ||
let extractor = getExtractor(context, item.extension) | ||
regexParserContent.push([item, { transformer, extractor }]) | ||
} | ||
const BATCH_SIZE = 500 | ||
for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) { | ||
let batch = regexParserContent.slice(i, i + BATCH_SIZE) | ||
await Promise.all( | ||
batch.map(async ([{ file, content }, { transformer, extractor }]) => { | ||
content = file ? await fs.promises.readFile(file, 'utf8') : content | ||
getClassCandidates(transformer(content), extractor, candidates, seen) | ||
}) | ||
) | ||
} | ||
} | ||
@@ -158,0 +170,0 @@ |
@@ -499,3 +499,3 @@ import postcss from 'postcss' | ||
let normalized = normalize(value) | ||
let normalized = normalize(value, { property }) | ||
@@ -577,3 +577,3 @@ if (!isParsableCssValue(property, normalized)) { | ||
function* resolveMatches(candidate, context, original = candidate) { | ||
function* resolveMatches(candidate, context) { | ||
let separator = context.tailwindConfig.separator | ||
@@ -588,11 +588,2 @@ let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse() | ||
if (flagEnabled(context.tailwindConfig, 'variantGrouping')) { | ||
if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) { | ||
let base = variants.slice().reverse().join(separator) | ||
for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) { | ||
yield* resolveMatches(base + separator + part, context, original) | ||
} | ||
} | ||
} | ||
// TODO: Reintroduce this in ways that doesn't break on false positives | ||
@@ -786,3 +777,3 @@ // function sortAgainst(toSort, against) { | ||
// Apply final format selector | ||
match = applyFinalFormat(match, { context, candidate, original }) | ||
match = applyFinalFormat(match, { context, candidate }) | ||
@@ -801,3 +792,3 @@ // Skip rules with invalid selectors | ||
function applyFinalFormat(match, { context, candidate, original }) { | ||
function applyFinalFormat(match, { context, candidate }) { | ||
if (!match[0].collectedFormats) { | ||
@@ -837,6 +828,15 @@ return match | ||
try { | ||
rule.selector = finalizeSelector(rule.selector, finalFormat, { | ||
candidate: original, | ||
let selector = finalizeSelector(rule.selector, finalFormat, { | ||
candidate, | ||
context, | ||
}) | ||
// Finalize Selector determined that this candidate is irrelevant | ||
// TODO: This elimination should happen earlier so this never happens | ||
if (selector === null) { | ||
rule.remove() | ||
return | ||
} | ||
rule.selector = selector | ||
} catch { | ||
@@ -891,3 +891,3 @@ // If this selector is invalid we also want to skip it | ||
function generateRules(candidates, context) { | ||
function generateRules(candidates, context, isSorting = false) { | ||
let allRules = [] | ||
@@ -927,3 +927,5 @@ let strategy = getImportantStrategy(context.tailwindConfig.important) | ||
let newEntry = [sort, rule] | ||
// Note: We have to clone rules during sorting | ||
// so we eliminate some shared mutable state | ||
let newEntry = [sort, isSorting ? rule.clone() : rule] | ||
rules.add(newEntry) | ||
@@ -930,0 +932,0 @@ context.ruleCache.add(newEntry) |
@@ -151,39 +151,41 @@ import fs from 'fs' | ||
/** | ||
* Ignore everything inside a :not(...). This allows you to write code like | ||
* `div:not(.foo)`. If `.foo` is never found in your code, then we used to | ||
* not generated it. But now we will ignore everything inside a `:not`, so | ||
* that it still gets generated. | ||
* | ||
* @param {selectorParser.Root} selectors | ||
*/ | ||
function ignoreNot(selectors) { | ||
selectors.walkPseudos((pseudo) => { | ||
if (pseudo.value === ':not') { | ||
pseudo.remove() | ||
} | ||
}) | ||
} | ||
function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) { | ||
let classes = [] | ||
let selectors = [] | ||
// Handle normal rules | ||
if (node.type === 'rule') { | ||
// Ignore everything inside a :not(...). This allows you to write code like | ||
// `div:not(.foo)`. If `.foo` is never found in your code, then we used to | ||
// not generated it. But now we will ignore everything inside a `:not`, so | ||
// that it still gets generated. | ||
function ignoreNot(selectors) { | ||
selectors.walkPseudos((pseudo) => { | ||
if (pseudo.value === ':not') { | ||
pseudo.remove() | ||
} | ||
}) | ||
} | ||
// Handle normal rules | ||
selectors.push(...node.selectors) | ||
} else if (node.type === 'atrule') { | ||
// Handle at-rules (which contains nested rules) | ||
node.walkRules((rule) => selectors.push(...rule.selectors)) | ||
} | ||
for (let selector of node.selectors) { | ||
let classCandidates = getClasses(selector, ignoreNot) | ||
// At least one of the selectors contains non-"on-demandable" candidates. | ||
if (classCandidates.length === 0) { | ||
state.containsNonOnDemandable = true | ||
} | ||
for (let selector of selectors) { | ||
let classCandidates = getClasses(selector, ignoreNot) | ||
for (let classCandidate of classCandidates) { | ||
classes.push(classCandidate) | ||
} | ||
// At least one of the selectors contains non-"on-demandable" candidates. | ||
if (classCandidates.length === 0) { | ||
state.containsNonOnDemandable = true | ||
} | ||
} | ||
// Handle at-rules (which contains nested rules) | ||
else if (node.type === 'atrule') { | ||
node.walkRules((rule) => { | ||
for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) { | ||
classes.push(classCandidate) | ||
} | ||
}) | ||
for (let classCandidate of classCandidates) { | ||
classes.push(classCandidate) | ||
} | ||
} | ||
@@ -949,3 +951,3 @@ | ||
// Non-tailwind classes won't be generated and will be left as `null` | ||
let rules = generateRules(new Set(sorted), context) | ||
let rules = generateRules(new Set(sorted), context, true) | ||
rules = context.offsets.sort(rules) | ||
@@ -952,0 +954,0 @@ |
@@ -280,5 +280,5 @@ import path from 'path' | ||
postcssPlugin: 'tailwindcss', | ||
Once(root, { result }) { | ||
async Once(root, { result }) { | ||
env.DEBUG && console.time('Compiling CSS') | ||
tailwind(({ createContext }) => { | ||
await tailwind(({ createContext }) => { | ||
console.error() | ||
@@ -285,0 +285,0 @@ console.error('Rebuilding...') |
@@ -8,3 +8,3 @@ import namedColors from './colorNames' | ||
let ALPHA_SEP = /\s*[,/]\s*/ | ||
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)\)/ | ||
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/ | ||
@@ -11,0 +11,0 @@ let RGB = new RegExp( |
@@ -13,9 +13,30 @@ import { parseColor } from './color' | ||
const placeholder = '--tw-placeholder' | ||
const placeholderRe = new RegExp(placeholder, 'g') | ||
// These properties accept a `<dashed-ident>` as one of the values. This means that you can use them | ||
// as: `timeline-scope: --tl;` | ||
// | ||
// Without the `var(--tl)`, in these cases we don't want to normalize the value, and you should add | ||
// the `var()` yourself. | ||
// | ||
// More info: | ||
// - https://drafts.csswg.org/scroll-animations/#propdef-timeline-scope | ||
// - https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope#dashed-ident | ||
// | ||
const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([ | ||
// Concrete properties | ||
'scroll-timeline-name', | ||
'timeline-scope', | ||
'view-timeline-name', | ||
'font-palette', | ||
// Shorthand properties | ||
'scroll-timeline', | ||
'animation-timeline', | ||
'view-timeline', | ||
]) | ||
// This is not a data type, but rather a function that can normalize the | ||
// correct values. | ||
export function normalize(value, isRoot = true) { | ||
if (value.startsWith('--')) { | ||
export function normalize(value, context = null, isRoot = true) { | ||
let isVarException = context && AUTO_VAR_INJECTION_EXCEPTIONS.has(context.property) | ||
if (value.startsWith('--') && !isVarException) { | ||
return `var(${value})` | ||
@@ -34,3 +55,3 @@ } | ||
return normalize(part, false) | ||
return normalize(part, context, false) | ||
}) | ||
@@ -67,12 +88,63 @@ .join('') | ||
function normalizeMathOperatorSpacing(value) { | ||
let preventFormattingInFunctions = ['theme'] | ||
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => { | ||
let vars = [] | ||
let result = '' | ||
return match | ||
.replace(/var\((--.+?)[,)]/g, (match, g1) => { | ||
vars.push(g1) | ||
return match.replace(g1, placeholder) | ||
}) | ||
.replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ') | ||
.replace(placeholderRe, () => vars.shift()) | ||
function lastChar() { | ||
let char = result.trimEnd() | ||
return char[char.length - 1] | ||
} | ||
for (let i = 0; i < match.length; i++) { | ||
function peek(word) { | ||
return word.split('').every((char, j) => match[i + j] === char) | ||
} | ||
function consumeUntil(chars) { | ||
let minIndex = Infinity | ||
for (let char of chars) { | ||
let index = match.indexOf(char, i) | ||
if (index !== -1 && index < minIndex) { | ||
minIndex = index | ||
} | ||
} | ||
let result = match.slice(i, minIndex) | ||
i += result.length - 1 | ||
return result | ||
} | ||
let char = match[i] | ||
// Handle `var(--variable)` | ||
if (peek('var')) { | ||
// When we consume until `)`, then we are dealing with this scenario: | ||
// `var(--example)` | ||
// | ||
// When we consume until `,`, then we are dealing with this scenario: | ||
// `var(--example, 1rem)` | ||
// | ||
// In this case we do want to "format", the default value as well | ||
result += consumeUntil([')', ',']) | ||
} | ||
// Skip formatting inside known functions | ||
else if (preventFormattingInFunctions.some((fn) => peek(fn))) { | ||
result += consumeUntil([')']) | ||
} | ||
// Handle operators | ||
else if ( | ||
['+', '-', '*', '/'].includes(char) && | ||
!['(', '+', '-', '*', '/'].includes(lastChar()) | ||
) { | ||
result += ` ${char} ` | ||
} else { | ||
result += char | ||
} | ||
} | ||
// Simplify multiple spaces | ||
return result.replace(/\s+/g, ' ') | ||
}) | ||
@@ -79,0 +151,0 @@ } |
@@ -6,2 +6,3 @@ import selectorParser from 'postcss-selector-parser' | ||
import { movePseudos } from './pseudoElements' | ||
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' | ||
@@ -164,3 +165,3 @@ /** @typedef {import('postcss-selector-parser').Root} Root */ | ||
// | ||
base = base ?? candidate.split(new RegExp(`\\${separator}(?![^[]*\\])`)).pop() | ||
base = base ?? splitAtTopLevelOnly(candidate, separator).pop() | ||
@@ -190,2 +191,9 @@ // Parse the selector into an AST | ||
// If ffter eliminating irrelevant selectors, we end up with nothing | ||
// Then the whole "rule" this is associated with does not need to exist | ||
// We use `null` as a marker value for that case | ||
if (selector.length === 0) { | ||
return null | ||
} | ||
// If there are no formats that means there were no variants added to the candidate | ||
@@ -192,0 +200,0 @@ // so we can just return the selector as-is |
@@ -7,3 +7,3 @@ export default function isPlainObject(value) { | ||
const prototype = Object.getPrototypeOf(value) | ||
return prototype === null || prototype === Object.prototype | ||
return prototype === null || Object.getPrototypeOf(prototype) === null | ||
} |
@@ -49,9 +49,9 @@ import type { CorePluginList } from './generated/corePluginList' | ||
// Safelist related config | ||
type SafelistConfig = (string | { pattern: RegExp; variants?: string[] })[] | ||
type SafelistConfig = string | { pattern: RegExp; variants?: string[] } | ||
// Blocklist related config | ||
type BlocklistConfig = string[] | ||
type BlocklistConfig = string | ||
// Presets related config | ||
type PresetsConfig = Config[] | ||
type PresetsConfig = Partial<Config> | ||
@@ -356,5 +356,5 @@ // Future related config | ||
separator: Partial<SeparatorConfig> | ||
safelist: Partial<SafelistConfig> | ||
blocklist: Partial<BlocklistConfig> | ||
presets: Partial<PresetsConfig> | ||
safelist: Array<SafelistConfig> | ||
blocklist: Array<BlocklistConfig> | ||
presets: Array<PresetsConfig> | ||
future: Partial<FutureConfig> | ||
@@ -361,0 +361,0 @@ experimental: Partial<ExperimentalConfig> |
@@ -1,2 +0,2 @@ | ||
import { PluginCreator } from 'postcss' | ||
import type { PluginCreator } from 'postcss' | ||
import type { Config } from './config.d' | ||
@@ -6,3 +6,7 @@ | ||
export { Config } | ||
export default plugin | ||
declare type _Config = Config | ||
declare namespace plugin { | ||
export type { _Config as Config } | ||
} | ||
export = plugin |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
5565132
275
34159
Updatedfast-glob@^3.3.0
Updatedjiti@^1.19.1