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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5565132
275
34159
12
Updatedfast-glob@^3.3.0
Updatedjiti@^1.19.1