Skip to content

Commit 0486628

Browse files
feat: support tailwindcss@3.4.0 (francoismassart#306)
* test(tw3.4.0): Dynamic viewport units * test(tw3.4.0): New :has() variant * test(tw3.4.0): Style children with the * variant * feat: autofix h-* w-* becomes size-* shorthand * 3.14.0-beta.0 * fix: support for contradicting sizes * feat: new text-wrap utilities * feat: Subgrid support * test: Extended min-width, max-width, and min-height scales * test: Extended opacity scale * test: Extended grid-rows-* scale * test: New forced-colors variant * test: Forced Color Adjust * 3.14.0-beta.1
1 parent 3ee4698 commit 0486628

File tree

10 files changed

+260
-99
lines changed

10 files changed

+260
-99
lines changed

lib/config/groups.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,10 @@ module.exports.groups = [
507507
type: 'Max-Height',
508508
members: 'max\\-h\\-(?<value>${maxHeight})',
509509
},
510+
{
511+
type: 'Size',
512+
members: 'size\\-(?<value>${size})',
513+
},
510514
],
511515
},
512516
{
@@ -594,6 +598,10 @@ module.exports.groups = [
594598
members: 'overflow\\-(ellipsis|clip)',
595599
deprecated: true,
596600
},
601+
{
602+
type: 'Text Wrap',
603+
members: 'text\\-(wrap|nowrap|balance|pretty)',
604+
},
597605
{
598606
type: 'Text Indent',
599607
members: '(indent\\-(?<value>${textIndent})|\\-indent\\-(?<negativeValue>${-textIndent}))',
@@ -1341,6 +1349,10 @@ module.exports.groups = [
13411349
type: 'Screen Readers',
13421350
members: '(not\\-)?sr\\-only',
13431351
},
1352+
{
1353+
type: 'Forced Color Adjust',
1354+
members: 'forced\\-color\\-adjust\\-(auto|none)',
1355+
},
13441356
],
13451357
},
13461358
{

lib/rules/enforces-shorthand.js

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,28 @@ module.exports = {
7575

7676
// These are shorthand candidates that do not share the same parent type
7777
const complexEquivalences = [
78-
[["overflow-hidden", "text-ellipsis", "whitespace-nowrap"], "truncate"]
79-
]
78+
{
79+
needles: ['overflow-hidden', 'text-ellipsis', 'whitespace-nowrap'],
80+
shorthand: 'truncate',
81+
mode: 'exact',
82+
},
83+
{
84+
needles: ['w-', 'h-'],
85+
shorthand: 'size-',
86+
mode: 'value',
87+
},
88+
];
8089

8190
// Init assets
8291
const targetProperties = {
8392
Layout: ['Overflow', 'Overscroll Behavior', 'Top / Right / Bottom / Left'],
8493
'Flexbox & Grid': ['Gap'],
8594
Spacing: ['Padding', 'Margin'],
95+
Sizing: ['Width', 'Height'],
8696
Borders: ['Border Radius', 'Border Width', 'Border Color'],
8797
Tables: ['Border Spacing'],
8898
Transforms: ['Scale'],
89-
Typography: ['Text Overflow', 'Whitespace']
99+
Typography: ['Text Overflow', 'Whitespace'],
90100
};
91101

92102
// We don't want to affect other rules by object reference
@@ -219,53 +229,79 @@ module.exports = {
219229
const validated = [];
220230

221231
// Handle sets of classnames with different parent types
222-
let remaining = parsed
223-
for (const [inputSet, outputClassname] of complexEquivalences) {
232+
let remaining = parsed;
233+
for (const { needles: inputSet, shorthand: outputClassname, mode } of complexEquivalences) {
224234
if (remaining.length < inputSet.length) {
225-
continue
226-
}
227-
228-
const parsedElementsInInputSet = remaining.filter(remainingClass => inputSet.some(inputClass => remainingClass.name.includes(inputClass)))
229-
230-
// Make sure all required classes for the shorthand are present
231-
if (parsedElementsInInputSet.length !== inputSet.length) {
232-
continue
235+
continue;
233236
}
234237

235-
// Make sure the classes share all the same variants
236-
if (new Set(parsedElementsInInputSet.map(p => p.variants)).size !== 1) {
237-
continue
238-
}
239-
240-
// Make sure the classes share all the same importance
241-
if (new Set(parsedElementsInInputSet.map(p => p.important)).size !== 1) {
242-
continue
243-
}
238+
// Matching classes
239+
const parsedElementsInInputSet = remaining.filter((remainingClass) => {
240+
if (mode === 'exact') {
241+
// Test if the name contains the target class, eg. 'text-ellipsis' inside 'md:text-ellipsis'...
242+
return inputSet.some((inputClass) => remainingClass.name.includes(inputClass));
243+
}
244+
// Test if the body of the class matches, eg. 'h-' inside 'h-10'
245+
if (mode === 'value') {
246+
return inputSet.some((inputClassPattern) => inputClassPattern === remainingClass.body);
247+
}
248+
});
244249

245-
const index = parsedElementsInInputSet[0].index
246-
const variants = parsedElementsInInputSet[0].variants
247-
const important = parsedElementsInInputSet[0].important ? "!" : ""
250+
const variantGroups = new Map();
251+
parsedElementsInInputSet.forEach((o) => {
252+
const val = mode === 'value' ? o.value : '';
253+
const v = `${o.variants}${o.important ? '!' : ''}${val}`;
254+
if (!variantGroups.has(v)) {
255+
variantGroups.set(
256+
v,
257+
parsedElementsInInputSet.filter(
258+
(c) => c.variants === o.variants && c.important === o.important && (val === '' || c.value === val)
259+
)
260+
);
261+
}
262+
});
263+
const validKeys = new Set();
264+
variantGroups.forEach((classes, key) => {
265+
let skip = false;
266+
// Make sure all required classes for the shorthand are present
267+
if (classes.length < inputSet.length) {
268+
skip = true;
269+
}
270+
// Make sure the classes share all the single/shared/same value
271+
if (mode === 'value' && new Set(classes.map((p) => p.value)).size !== 1) {
272+
skip = true;
273+
}
274+
if (!skip) {
275+
validKeys.add(key);
276+
}
277+
});
278+
validKeys.forEach((k) => {
279+
const candidates = variantGroups.get(k);
280+
const index = candidates[0].index;
281+
const variants = candidates[0].variants;
282+
const important = candidates[0].important ? '!' : '';
283+
const classValue = mode === 'value' ? candidates[0].value : '';
248284

249-
const patchedClassname = `${variants}${important}${mergedConfig.prefix}${outputClassname}`
250-
troubles.push([parsedElementsInInputSet.map((c) => `${c.name}`), patchedClassname]);
285+
const patchedClassname = `${variants}${important}${mergedConfig.prefix}${outputClassname}${classValue}`;
286+
troubles.push([candidates.map((c) => `${c.name}`), patchedClassname]);
251287

252-
const validatedClassname = groupUtil.parseClassname(patchedClassname, targetGroups, mergedConfig, index)
253-
validated.push(validatedClassname);
288+
const validatedClassname = groupUtil.parseClassname(patchedClassname, targetGroups, mergedConfig, index);
289+
validated.push(validatedClassname);
254290

255-
remaining = remaining.filter(p => !parsedElementsInInputSet.includes(p))
291+
remaining = remaining.filter((p) => !candidates.includes(p));
292+
});
256293
}
257294

258295
// Handle sets of classnames with the same parent type
259-
260296
// Each group parentType
261297
const checkedGroups = [];
262-
remaining.forEach((classname) => {
298+
remaining.forEach((classname, idx, arr) => {
263299
// Valid candidate
264300
if (classname.parentType === '') {
265301
validated.push(classname);
266302
} else if (!checkedGroups.includes(classname.parentType)) {
267303
checkedGroups.push(classname.parentType);
268-
const sameType = parsed.filter((cls) => cls.parentType === classname.parentType);
304+
const sameType = remaining.filter((cls) => cls.parentType === classname.parentType);
269305
// Comparing same parentType classnames
270306
const checkedVariantsValue = [];
271307
sameType.forEach((cls) => {
@@ -404,27 +440,22 @@ module.exports = {
404440
}
405441
}
406442

407-
troubles
408-
.filter((trouble) => {
409-
// Only valid issue if there are classes to replace
410-
return trouble[0].length;
411-
})
412-
.forEach((issue) => {
413-
if (originalClassNamesValue !== validatedClassNamesValue) {
414-
validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;
415-
context.report({
416-
node: node,
417-
messageId: 'shorthandCandidateDetected',
418-
data: {
419-
classnames: issue[0].join(', '),
420-
shorthand: issue[1],
421-
},
422-
fix: function (fixer) {
423-
return fixer.replaceTextRange([start, end], validatedClassNamesValue);
424-
},
425-
});
426-
}
427-
});
443+
troubles.forEach((issue) => {
444+
if (originalClassNamesValue !== validatedClassNamesValue) {
445+
validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;
446+
context.report({
447+
node: node,
448+
messageId: 'shorthandCandidateDetected',
449+
data: {
450+
classnames: issue[0].join(', '),
451+
shorthand: issue[1],
452+
},
453+
fix: function (fixer) {
454+
return fixer.replaceTextRange([start, end], validatedClassNamesValue);
455+
},
456+
});
457+
}
458+
});
428459
};
429460

430461
//----------------------------------------------------------------------

lib/util/groupMethods.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ function generateOptions(propName, keys, config, isNegative = false) {
160160
case 'height':
161161
case 'lineHeight':
162162
case 'maxHeight':
163+
case 'size':
163164
case 'maxWidth':
164165
case 'minHeight':
165166
case 'minWidth':

package-lock.json

Lines changed: 26 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)