tailwindcss 0.0.0-insiders.bbf2b1b → 0.0.0-insiders.bcf14b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,13 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
- - Add support fo configuring default `font-feature-settings` for a font family ([#9039](https://github.com/tailwindlabs/tailwindcss/pull/9039))
12
+ - Add support for configuring default `font-feature-settings` for a font family ([#9039](https://github.com/tailwindlabs/tailwindcss/pull/9039))
13
13
  - Add a standalone CLI build for 32-bit Linux on ARM (`node16-linux-armv7`) ([#9084](https://github.com/tailwindlabs/tailwindcss/pull/9084))
14
+ - Add future flag to disable color opacity utility plugins ([#9088](https://github.com/tailwindlabs/tailwindcss/pull/9088))
15
+ - Add negative value support for `outline-offset` ([#9136](https://github.com/tailwindlabs/tailwindcss/pull/9136))
14
16
 
15
17
  ### Fixed
16
18
 
17
19
  - Use absolute paths when resolving changed files for resilience against working directory changes ([#9032](https://github.com/tailwindlabs/tailwindcss/pull/9032))
18
20
  - Fix ring color utility generation when using `respectDefaultRingColorOpacity` ([#9070](https://github.com/tailwindlabs/tailwindcss/pull/9070))
21
+ - Sort tags before classes when `@applying` a selector with joined classes ([#9107](https://github.com/tailwindlabs/tailwindcss/pull/9107))
22
+ - Remove invalid `outline-hidden` utility ([#9147](https://github.com/tailwindlabs/tailwindcss/pull/9147))
23
+ - Honor the `hidden` attribute on elements in preflight ([#9174](https://github.com/tailwindlabs/tailwindcss/pull/9174))
24
+ - Don't stop watching atomically renamed files ([#9173](https://github.com/tailwindlabs/tailwindcss/pull/9173))
25
+ - Re-use existing entries in the rule cache ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
26
+ - Don't output duplicate utilities ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
19
27
 
20
28
  ## [3.1.8] - 2022-08-05
21
29
 
package/lib/cli.js CHANGED
@@ -748,6 +748,10 @@ async function build() {
748
748
  ...contextDependencies,
749
749
  ...extractFileGlobs(config)
750
750
  ], {
751
+ // Force checking for atomic writes in all situations
752
+ // This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
753
+ // This only works when watching directories though
754
+ atomic: true,
751
755
  usePolling: shouldPoll,
752
756
  interval: shouldPoll ? pollInterval : undefined,
753
757
  ignoreInitial: true,
@@ -757,6 +761,7 @@ async function build() {
757
761
  } : false
758
762
  });
759
763
  let chain = Promise.resolve();
764
+ let pendingRebuilds = new Set();
760
765
  watcher.on("change", async (file)=>{
761
766
  if (contextDependencies.has(file)) {
762
767
  env.DEBUG && console.time("Resolve config");
@@ -782,6 +787,67 @@ async function build() {
782
787
  });
783
788
  }
784
789
  });
790
+ /**
791
+ * When rapidly saving files atomically a couple of situations can happen:
792
+ * - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
793
+ * - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
794
+ *
795
+ * To work around this we retry reading the file a handful of times with a delay between each attempt
796
+ *
797
+ * @param {string} path
798
+ * @param {number} tries
799
+ * @returns {string}
800
+ * @throws {Error} If the file is still missing or busy after the specified number of tries
801
+ */ async function readFileWithRetries(path, tries = 5) {
802
+ for(let n = 0; n <= tries; n++){
803
+ try {
804
+ return await _fs.default.promises.readFile(path, "utf8");
805
+ } catch (err) {
806
+ if (n !== tries) {
807
+ if (err.code === "ENOENT" || err.code === "EBUSY") {
808
+ await new Promise((resolve)=>setTimeout(resolve, 10));
809
+ continue;
810
+ }
811
+ }
812
+ throw err;
813
+ }
814
+ }
815
+ }
816
+ // Restore watching any files that are "removed"
817
+ // This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
818
+ // TODO: An an optimization we should allow removal when the config changes
819
+ watcher.on("unlink", (file)=>watcher.add(file));
820
+ // Some applications such as Visual Studio (but not VS Code)
821
+ // will only fire a rename event for atomic writes and not a change event
822
+ // This is very likely a chokidar bug but it's one we need to work around
823
+ // We treat this as a change event and rebuild the CSS
824
+ watcher.on("raw", (evt, filePath, meta)=>{
825
+ if (evt !== "rename") {
826
+ return;
827
+ }
828
+ let watchedPath = _path.default.resolve(meta.watchedPath);
829
+ // Watched path might be the file itself
830
+ // Or the directory it is in
831
+ filePath = watchedPath.endsWith(filePath) ? watchedPath : _path.default.resolve(watchedPath, filePath);
832
+ // Skip since we've already queued a rebuild for this file that hasn't happened yet
833
+ if (pendingRebuilds.has(filePath)) {
834
+ return;
835
+ }
836
+ pendingRebuilds.add(filePath);
837
+ chain = chain.then(async ()=>{
838
+ let content;
839
+ try {
840
+ content = await readFileWithRetries(_path.default.resolve(filePath));
841
+ } finally{
842
+ pendingRebuilds.delete(filePath);
843
+ }
844
+ changedContent.push({
845
+ content,
846
+ extension: _path.default.extname(filePath).slice(1)
847
+ });
848
+ await rebuild(config);
849
+ });
850
+ });
785
851
  watcher.on("add", async (file)=>{
786
852
  chain = chain.then(async ()=>{
787
853
  changedContent.push({
@@ -3182,9 +3182,6 @@ let corePlugins = {
3182
3182
  },
3183
3183
  ".outline-double": {
3184
3184
  "outline-style": "double"
3185
- },
3186
- ".outline-hidden": {
3187
- "outline-style": "hidden"
3188
3185
  }
3189
3186
  });
3190
3187
  },
@@ -3214,7 +3211,8 @@ let corePlugins = {
3214
3211
  "length",
3215
3212
  "number",
3216
3213
  "percentage"
3217
- ]
3214
+ ],
3215
+ supportsNegativeValues: true
3218
3216
  }),
3219
3217
  outlineColor: ({ matchUtilities , theme })=>{
3220
3218
  matchUtilities({
@@ -358,3 +358,8 @@ video {
358
358
  max-width: 100%;
359
359
  height: auto;
360
360
  }
361
+
362
+ /* Make elements with the HTML hidden attribute stay hidden by default */
363
+ [hidden] {
364
+ display: none;
365
+ }
@@ -26,7 +26,8 @@ let defaults = {
26
26
  let featureFlags = {
27
27
  future: [
28
28
  "hoverOnlyWhenSupported",
29
- "respectDefaultRingColorOpacity"
29
+ "respectDefaultRingColorOpacity",
30
+ "disableColorOpacityUtilitiesByDefault",
30
31
  ],
31
32
  experimental: [
32
33
  "optimizeUniversalDefaults",
@@ -41,11 +41,11 @@ function _interopRequireDefault(obj) {
41
41
  groups: normalizedGroups
42
42
  });
43
43
  }
44
- let selectorExtractor = (0, _postcssSelectorParser.default)((root)=>root.nodes.map((node)=>node.toString()));
44
+ let selectorExtractor = (0, _postcssSelectorParser.default)();
45
45
  /**
46
46
  * @param {string} ruleSelectors
47
47
  */ function extractSelectors(ruleSelectors) {
48
- return selectorExtractor.transformSync(ruleSelectors);
48
+ return selectorExtractor.astSync(ruleSelectors);
49
49
  }
50
50
  function extractBaseCandidates(candidates, separator) {
51
51
  let baseClasses = new Set();
@@ -268,30 +268,64 @@ function processApply(root, context, localCache) {
268
268
  * What happens in this function is that we prepend a `.` and escape the candidate.
269
269
  * This will result in `.hover\:font-bold`
270
270
  * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
271
- */ // TODO: Should we use postcss-selector-parser for this instead?
272
- function replaceSelector(selector, utilitySelectors, candidate) {
273
- let needle = `.${(0, _escapeClassName.default)(candidate)}`;
274
- let needles = [
275
- ...new Set([
276
- needle,
277
- needle.replace(/\\2c /g, "\\,")
278
- ])
279
- ];
271
+ *
272
+ * @param {string} selector
273
+ * @param {string} utilitySelectors
274
+ * @param {string} candidate
275
+ */ function replaceSelector(selector, utilitySelectors, candidate) {
276
+ let selectorList = extractSelectors(selector);
280
277
  let utilitySelectorsList = extractSelectors(utilitySelectors);
281
- return extractSelectors(selector).map((s)=>{
282
- let replaced = [];
283
- for (let utilitySelector of utilitySelectorsList){
284
- let replacedSelector = utilitySelector;
285
- for (const needle of needles){
286
- replacedSelector = replacedSelector.replace(needle, s);
287
- }
288
- if (replacedSelector === utilitySelector) {
289
- continue;
290
- }
291
- replaced.push(replacedSelector);
278
+ let candidateList = extractSelectors(`.${(0, _escapeClassName.default)(candidate)}`);
279
+ let candidateClass = candidateList.nodes[0].nodes[0];
280
+ selectorList.each((sel)=>{
281
+ /** @type {Set<import('postcss-selector-parser').Selector>} */ let replaced = new Set();
282
+ utilitySelectorsList.each((utilitySelector)=>{
283
+ let hasReplaced = false;
284
+ utilitySelector = utilitySelector.clone();
285
+ utilitySelector.walkClasses((node)=>{
286
+ if (node.value !== candidateClass.value) {
287
+ return;
288
+ }
289
+ // Don't replace multiple instances of the same class
290
+ // This is theoretically correct but only partially
291
+ // We'd need to generate every possible permutation of the replacement
292
+ // For example with `.foo + .foo { … }` and `section { @apply foo; }`
293
+ // We'd need to generate all of these:
294
+ // - `.foo + .foo`
295
+ // - `.foo + section`
296
+ // - `section + .foo`
297
+ // - `section + section`
298
+ if (hasReplaced) {
299
+ return;
300
+ }
301
+ // Since you can only `@apply` class names this is sufficient
302
+ // We want to replace the matched class name with the selector the user is using
303
+ // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)`
304
+ node.replaceWith(...sel.nodes.map((node)=>node.clone()));
305
+ // Record that we did something and we want to use this new selector
306
+ replaced.add(utilitySelector);
307
+ hasReplaced = true;
308
+ });
309
+ });
310
+ // Sort tag names before class names
311
+ // This happens when replacing `.bar` in `.foo.bar` with a tag like `section`
312
+ for (const sel1 of replaced){
313
+ sel1.sort((a, b)=>{
314
+ if (a.type === "tag" && b.type === "class") {
315
+ return -1;
316
+ } else if (a.type === "class" && b.type === "tag") {
317
+ return 1;
318
+ } else if (a.type === "class" && b.type === "pseudo") {
319
+ return -1;
320
+ } else if (a.type === "pseudo" && b.type === "class") {
321
+ return 1;
322
+ }
323
+ return sel1.index(a) - sel1.index(b);
324
+ });
292
325
  }
293
- return replaced.join(", ");
294
- }).join(", ");
326
+ sel.replaceWith(...replaced);
327
+ });
328
+ return selectorList.toString();
295
329
  }
296
330
  let perParentApplies = new Map();
297
331
  // Collect all apply candidates and their rules
@@ -188,14 +188,11 @@ function expandTailwindAtRules(context) {
188
188
  // Generate the actual CSS
189
189
  let classCacheCount = context.classCache.size;
190
190
  env.DEBUG && console.time("Generate rules");
191
- let rules = (0, _generateRules.generateRules)(candidates, context);
191
+ (0, _generateRules.generateRules)(candidates, context);
192
192
  env.DEBUG && console.timeEnd("Generate rules");
193
193
  // We only ever add to the classCache, so if it didn't grow, there is nothing new.
194
194
  env.DEBUG && console.time("Build stylesheet");
195
195
  if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) {
196
- for (let rule of rules){
197
- context.ruleCache.add(rule);
198
- }
199
196
  context.stylesheetCache = buildStylesheet([
200
197
  ...context.ruleCache
201
198
  ], context);
@@ -665,14 +665,39 @@ function* resolveMatches(candidate, context, original = candidate) {
665
665
  function inKeyframes(rule) {
666
666
  return rule.parent && rule.parent.type === "atrule" && rule.parent.name === "keyframes";
667
667
  }
668
+ function getImportantStrategy(important) {
669
+ if (important === true) {
670
+ return (rule)=>{
671
+ if (inKeyframes(rule)) {
672
+ return;
673
+ }
674
+ rule.walkDecls((d)=>{
675
+ if (d.parent.type === "rule" && !inKeyframes(d.parent)) {
676
+ d.important = true;
677
+ }
678
+ });
679
+ };
680
+ }
681
+ if (typeof important === "string") {
682
+ return (rule)=>{
683
+ if (inKeyframes(rule)) {
684
+ return;
685
+ }
686
+ rule.selectors = rule.selectors.map((selector)=>{
687
+ return `${important} ${selector}`;
688
+ });
689
+ };
690
+ }
691
+ }
668
692
  function generateRules(candidates, context) {
669
693
  let allRules = [];
694
+ let strategy = getImportantStrategy(context.tailwindConfig.important);
670
695
  for (let candidate of candidates){
671
696
  if (context.notClassCache.has(candidate)) {
672
697
  continue;
673
698
  }
674
- if (context.classCache.has(candidate)) {
675
- allRules.push(context.classCache.get(candidate));
699
+ if (context.candidateRuleCache.has(candidate)) {
700
+ allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)));
676
701
  continue;
677
702
  }
678
703
  let matches = Array.from(resolveMatches(candidate, context));
@@ -681,49 +706,30 @@ function generateRules(candidates, context) {
681
706
  continue;
682
707
  }
683
708
  context.classCache.set(candidate, matches);
684
- allRules.push(matches);
685
- }
686
- // Strategy based on `tailwindConfig.important`
687
- let strategy = ((important)=>{
688
- if (important === true) {
689
- return (rule)=>{
690
- rule.walkDecls((d)=>{
691
- if (d.parent.type === "rule" && !inKeyframes(d.parent)) {
692
- d.important = true;
693
- }
694
- });
695
- };
696
- }
697
- if (typeof important === "string") {
698
- return (rule)=>{
699
- rule.selectors = rule.selectors.map((selector)=>{
700
- return `${important} ${selector}`;
701
- });
702
- };
703
- }
704
- })(context.tailwindConfig.important);
705
- return allRules.flat(1).map(([{ sort , layer , options }, rule])=>{
706
- if (options.respectImportant) {
707
- if (strategy) {
709
+ var ref;
710
+ let rules = (ref = context.candidateRuleCache.get(candidate)) !== null && ref !== void 0 ? ref : new Set();
711
+ context.candidateRuleCache.set(candidate, rules);
712
+ for (const match of matches){
713
+ let [{ sort , layer , options }, rule] = match;
714
+ if (options.respectImportant && strategy) {
708
715
  let container = _postcss.default.root({
709
716
  nodes: [
710
717
  rule.clone()
711
718
  ]
712
719
  });
713
- container.walkRules((r)=>{
714
- if (inKeyframes(r)) {
715
- return;
716
- }
717
- strategy(r);
718
- });
720
+ container.walkRules(strategy);
719
721
  rule = container.nodes[0];
720
722
  }
723
+ let newEntry = [
724
+ sort | context.layerOrder[layer],
725
+ rule
726
+ ];
727
+ rules.add(newEntry);
728
+ context.ruleCache.add(newEntry);
729
+ allRules.push(newEntry);
721
730
  }
722
- return [
723
- sort | context.layerOrder[layer],
724
- rule
725
- ];
726
- });
731
+ }
732
+ return allRules;
727
733
  }
728
734
  function isArbitraryValue(input) {
729
735
  return input.startsWith("[") && input.endsWith("]");
@@ -858,6 +858,7 @@ function createContext(tailwindConfig, changedContent = [], root = _postcss.defa
858
858
  let context = {
859
859
  disposables: [],
860
860
  ruleCache: new Set(),
861
+ candidateRuleCache: new Map(),
861
862
  classCache: new Map(),
862
863
  applyClassCache: new Map(),
863
864
  notClassCache: new Set(),
@@ -27,6 +27,16 @@ function getAllConfigs(config) {
27
27
  ...theme("colors")
28
28
  })
29
29
  }
30
+ },
31
+ disableColorOpacityUtilitiesByDefault: {
32
+ corePlugins: {
33
+ backgroundOpacity: false,
34
+ borderOpacity: false,
35
+ divideOpacity: false,
36
+ placeholderOpacity: false,
37
+ ringOpacity: false,
38
+ textOpacity: false
39
+ }
30
40
  }
31
41
  };
32
42
  const experimentals = Object.keys(features).filter((feature)=>(0, _featureFlags.flagEnabled)(config, feature)).map((feature)=>features[feature]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwindcss",
3
- "version": "0.0.0-insiders.bbf2b1b",
3
+ "version": "0.0.0-insiders.bcf14b1",
4
4
  "description": "A utility-first CSS framework for rapidly building custom user interfaces.",
5
5
  "license": "MIT",
6
6
  "main": "lib/index.js",
@@ -43,13 +43,13 @@
43
43
  ],
44
44
  "devDependencies": {
45
45
  "@swc/cli": "^0.1.57",
46
- "@swc/core": "^1.2.218",
46
+ "@swc/core": "^1.2.244",
47
47
  "@swc/jest": "^0.2.22",
48
48
  "@swc/register": "^0.1.10",
49
49
  "autoprefixer": "^10.4.8",
50
- "cssnano": "^5.1.12",
50
+ "cssnano": "^5.1.13",
51
51
  "esbuild": "^0.14.54",
52
- "eslint": "^8.20.0",
52
+ "eslint": "^8.21.0",
53
53
  "eslint-config-prettier": "^8.5.0",
54
54
  "eslint-plugin-prettier": "^4.2.1",
55
55
  "jest": "^28.1.3",
package/src/cli.js CHANGED
@@ -843,6 +843,11 @@ async function build() {
843
843
  }
844
844
 
845
845
  watcher = chokidar.watch([...contextDependencies, ...extractFileGlobs(config)], {
846
+ // Force checking for atomic writes in all situations
847
+ // This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
848
+ // This only works when watching directories though
849
+ atomic: true,
850
+
846
851
  usePolling: shouldPoll,
847
852
  interval: shouldPoll ? pollInterval : undefined,
848
853
  ignoreInitial: true,
@@ -855,6 +860,7 @@ async function build() {
855
860
  })
856
861
 
857
862
  let chain = Promise.resolve()
863
+ let pendingRebuilds = new Set()
858
864
 
859
865
  watcher.on('change', async (file) => {
860
866
  if (contextDependencies.has(file)) {
@@ -885,6 +891,81 @@ async function build() {
885
891
  }
886
892
  })
887
893
 
894
+ /**
895
+ * When rapidly saving files atomically a couple of situations can happen:
896
+ * - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
897
+ * - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
898
+ *
899
+ * To work around this we retry reading the file a handful of times with a delay between each attempt
900
+ *
901
+ * @param {string} path
902
+ * @param {number} tries
903
+ * @returns {string}
904
+ * @throws {Error} If the file is still missing or busy after the specified number of tries
905
+ */
906
+ async function readFileWithRetries(path, tries = 5) {
907
+ for (let n = 0; n <= tries; n++) {
908
+ try {
909
+ return await fs.promises.readFile(path, 'utf8')
910
+ } catch (err) {
911
+ if (n !== tries) {
912
+ if (err.code === 'ENOENT' || err.code === 'EBUSY') {
913
+ await new Promise((resolve) => setTimeout(resolve, 10))
914
+
915
+ continue
916
+ }
917
+ }
918
+
919
+ throw err
920
+ }
921
+ }
922
+ }
923
+
924
+ // Restore watching any files that are "removed"
925
+ // This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
926
+ // TODO: An an optimization we should allow removal when the config changes
927
+ watcher.on('unlink', (file) => watcher.add(file))
928
+
929
+ // Some applications such as Visual Studio (but not VS Code)
930
+ // will only fire a rename event for atomic writes and not a change event
931
+ // This is very likely a chokidar bug but it's one we need to work around
932
+ // We treat this as a change event and rebuild the CSS
933
+ watcher.on('raw', (evt, filePath, meta) => {
934
+ if (evt !== 'rename') {
935
+ return
936
+ }
937
+
938
+ let watchedPath = path.resolve(meta.watchedPath)
939
+
940
+ // Watched path might be the file itself
941
+ // Or the directory it is in
942
+ filePath = watchedPath.endsWith(filePath) ? watchedPath : path.resolve(watchedPath, filePath)
943
+
944
+ // Skip since we've already queued a rebuild for this file that hasn't happened yet
945
+ if (pendingRebuilds.has(filePath)) {
946
+ return
947
+ }
948
+
949
+ pendingRebuilds.add(filePath)
950
+
951
+ chain = chain.then(async () => {
952
+ let content
953
+
954
+ try {
955
+ content = await readFileWithRetries(path.resolve(filePath))
956
+ } finally {
957
+ pendingRebuilds.delete(filePath)
958
+ }
959
+
960
+ changedContent.push({
961
+ content,
962
+ extension: path.extname(filePath).slice(1),
963
+ })
964
+
965
+ await rebuild(config)
966
+ })
967
+ })
968
+
888
969
  watcher.on('add', async (file) => {
889
970
  chain = chain.then(async () => {
890
971
  changedContent.push({
@@ -1980,7 +1980,6 @@ export let corePlugins = {
1980
1980
  '.outline-dashed': { 'outline-style': 'dashed' },
1981
1981
  '.outline-dotted': { 'outline-style': 'dotted' },
1982
1982
  '.outline-double': { 'outline-style': 'double' },
1983
- '.outline-hidden': { 'outline-style': 'hidden' },
1984
1983
  })
1985
1984
  },
1986
1985
 
@@ -1990,6 +1989,7 @@ export let corePlugins = {
1990
1989
 
1991
1990
  outlineOffset: createUtilityPlugin('outlineOffset', [['outline-offset', ['outline-offset']]], {
1992
1991
  type: ['length', 'number', 'percentage'],
1992
+ supportsNegativeValues: true,
1993
1993
  }),
1994
1994
 
1995
1995
  outlineColor: ({ matchUtilities, theme }) => {
@@ -358,3 +358,8 @@ video {
358
358
  max-width: 100%;
359
359
  height: auto;
360
360
  }
361
+
362
+ /* Make elements with the HTML hidden attribute stay hidden by default */
363
+ [hidden] {
364
+ display: none;
365
+ }
@@ -6,7 +6,11 @@ let defaults = {
6
6
  }
7
7
 
8
8
  let featureFlags = {
9
- future: ['hoverOnlyWhenSupported', 'respectDefaultRingColorOpacity'],
9
+ future: [
10
+ 'hoverOnlyWhenSupported',
11
+ 'respectDefaultRingColorOpacity',
12
+ 'disableColorOpacityUtilitiesByDefault',
13
+ ],
10
14
  experimental: ['optimizeUniversalDefaults', 'matchVariant' /* , 'variantGrouping' */],
11
15
  }
12
16
 
@@ -34,13 +34,13 @@ function extractClasses(node) {
34
34
  return Object.assign(classes, { groups: normalizedGroups })
35
35
  }
36
36
 
37
- let selectorExtractor = parser((root) => root.nodes.map((node) => node.toString()))
37
+ let selectorExtractor = parser()
38
38
 
39
39
  /**
40
40
  * @param {string} ruleSelectors
41
41
  */
42
42
  function extractSelectors(ruleSelectors) {
43
- return selectorExtractor.transformSync(ruleSelectors)
43
+ return selectorExtractor.astSync(ruleSelectors)
44
44
  }
45
45
 
46
46
  function extractBaseCandidates(candidates, separator) {
@@ -299,30 +299,77 @@ function processApply(root, context, localCache) {
299
299
  * What happens in this function is that we prepend a `.` and escape the candidate.
300
300
  * This will result in `.hover\:font-bold`
301
301
  * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
302
+ *
303
+ * @param {string} selector
304
+ * @param {string} utilitySelectors
305
+ * @param {string} candidate
302
306
  */
303
- // TODO: Should we use postcss-selector-parser for this instead?
304
307
  function replaceSelector(selector, utilitySelectors, candidate) {
305
- let needle = `.${escapeClassName(candidate)}`
306
- let needles = [...new Set([needle, needle.replace(/\\2c /g, '\\,')])]
308
+ let selectorList = extractSelectors(selector)
307
309
  let utilitySelectorsList = extractSelectors(utilitySelectors)
310
+ let candidateList = extractSelectors(`.${escapeClassName(candidate)}`)
311
+ let candidateClass = candidateList.nodes[0].nodes[0]
312
+
313
+ selectorList.each((sel) => {
314
+ /** @type {Set<import('postcss-selector-parser').Selector>} */
315
+ let replaced = new Set()
308
316
 
309
- return extractSelectors(selector)
310
- .map((s) => {
311
- let replaced = []
317
+ utilitySelectorsList.each((utilitySelector) => {
318
+ let hasReplaced = false
319
+ utilitySelector = utilitySelector.clone()
312
320
 
313
- for (let utilitySelector of utilitySelectorsList) {
314
- let replacedSelector = utilitySelector
315
- for (const needle of needles) {
316
- replacedSelector = replacedSelector.replace(needle, s)
321
+ utilitySelector.walkClasses((node) => {
322
+ if (node.value !== candidateClass.value) {
323
+ return
317
324
  }
318
- if (replacedSelector === utilitySelector) {
319
- continue
325
+
326
+ // Don't replace multiple instances of the same class
327
+ // This is theoretically correct but only partially
328
+ // We'd need to generate every possible permutation of the replacement
329
+ // For example with `.foo + .foo { … }` and `section { @apply foo; }`
330
+ // We'd need to generate all of these:
331
+ // - `.foo + .foo`
332
+ // - `.foo + section`
333
+ // - `section + .foo`
334
+ // - `section + section`
335
+ if (hasReplaced) {
336
+ return
320
337
  }
321
- replaced.push(replacedSelector)
322
- }
323
- return replaced.join(', ')
338
+
339
+ // Since you can only `@apply` class names this is sufficient
340
+ // We want to replace the matched class name with the selector the user is using
341
+ // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)`
342
+ node.replaceWith(...sel.nodes.map((node) => node.clone()))
343
+
344
+ // Record that we did something and we want to use this new selector
345
+ replaced.add(utilitySelector)
346
+
347
+ hasReplaced = true
348
+ })
324
349
  })
325
- .join(', ')
350
+
351
+ // Sort tag names before class names
352
+ // This happens when replacing `.bar` in `.foo.bar` with a tag like `section`
353
+ for (const sel of replaced) {
354
+ sel.sort((a, b) => {
355
+ if (a.type === 'tag' && b.type === 'class') {
356
+ return -1
357
+ } else if (a.type === 'class' && b.type === 'tag') {
358
+ return 1
359
+ } else if (a.type === 'class' && b.type === 'pseudo') {
360
+ return -1
361
+ } else if (a.type === 'pseudo' && b.type === 'class') {
362
+ return 1
363
+ }
364
+
365
+ return sel.index(a) - sel.index(b)
366
+ })
367
+ }
368
+
369
+ sel.replaceWith(...replaced)
370
+ })
371
+
372
+ return selectorList.toString()
326
373
  }
327
374
 
328
375
  let perParentApplies = new Map()
@@ -177,16 +177,12 @@ export default function expandTailwindAtRules(context) {
177
177
  let classCacheCount = context.classCache.size
178
178
 
179
179
  env.DEBUG && console.time('Generate rules')
180
- let rules = generateRules(candidates, context)
180
+ generateRules(candidates, context)
181
181
  env.DEBUG && console.timeEnd('Generate rules')
182
182
 
183
183
  // We only ever add to the classCache, so if it didn't grow, there is nothing new.
184
184
  env.DEBUG && console.time('Build stylesheet')
185
185
  if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) {
186
- for (let rule of rules) {
187
- context.ruleCache.add(rule)
188
- }
189
-
190
186
  context.stylesheetCache = buildStylesheet([...context.ruleCache], context)
191
187
  }
192
188
  env.DEBUG && console.timeEnd('Build stylesheet')
@@ -649,16 +649,45 @@ function inKeyframes(rule) {
649
649
  return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
650
650
  }
651
651
 
652
+ function getImportantStrategy(important) {
653
+ if (important === true) {
654
+ return (rule) => {
655
+ if (inKeyframes(rule)) {
656
+ return
657
+ }
658
+
659
+ rule.walkDecls((d) => {
660
+ if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
661
+ d.important = true
662
+ }
663
+ })
664
+ }
665
+ }
666
+
667
+ if (typeof important === 'string') {
668
+ return (rule) => {
669
+ if (inKeyframes(rule)) {
670
+ return
671
+ }
672
+
673
+ rule.selectors = rule.selectors.map((selector) => {
674
+ return `${important} ${selector}`
675
+ })
676
+ }
677
+ }
678
+ }
679
+
652
680
  function generateRules(candidates, context) {
653
681
  let allRules = []
682
+ let strategy = getImportantStrategy(context.tailwindConfig.important)
654
683
 
655
684
  for (let candidate of candidates) {
656
685
  if (context.notClassCache.has(candidate)) {
657
686
  continue
658
687
  }
659
688
 
660
- if (context.classCache.has(candidate)) {
661
- allRules.push(context.classCache.get(candidate))
689
+ if (context.candidateRuleCache.has(candidate)) {
690
+ allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
662
691
  continue
663
692
  }
664
693
 
@@ -670,47 +699,27 @@ function generateRules(candidates, context) {
670
699
  }
671
700
 
672
701
  context.classCache.set(candidate, matches)
673
- allRules.push(matches)
674
- }
675
702
 
676
- // Strategy based on `tailwindConfig.important`
677
- let strategy = ((important) => {
678
- if (important === true) {
679
- return (rule) => {
680
- rule.walkDecls((d) => {
681
- if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
682
- d.important = true
683
- }
684
- })
685
- }
686
- }
703
+ let rules = context.candidateRuleCache.get(candidate) ?? new Set()
704
+ context.candidateRuleCache.set(candidate, rules)
687
705
 
688
- if (typeof important === 'string') {
689
- return (rule) => {
690
- rule.selectors = rule.selectors.map((selector) => {
691
- return `${important} ${selector}`
692
- })
693
- }
694
- }
695
- })(context.tailwindConfig.important)
706
+ for (const match of matches) {
707
+ let [{ sort, layer, options }, rule] = match
696
708
 
697
- return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
698
- if (options.respectImportant) {
699
- if (strategy) {
709
+ if (options.respectImportant && strategy) {
700
710
  let container = postcss.root({ nodes: [rule.clone()] })
701
- container.walkRules((r) => {
702
- if (inKeyframes(r)) {
703
- return
704
- }
705
-
706
- strategy(r)
707
- })
711
+ container.walkRules(strategy)
708
712
  rule = container.nodes[0]
709
713
  }
714
+
715
+ let newEntry = [sort | context.layerOrder[layer], rule]
716
+ rules.add(newEntry)
717
+ context.ruleCache.add(newEntry)
718
+ allRules.push(newEntry)
710
719
  }
720
+ }
711
721
 
712
- return [sort | context.layerOrder[layer], rule]
713
- })
722
+ return allRules
714
723
  }
715
724
 
716
725
  function isArbitraryValue(input) {
@@ -873,6 +873,7 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs
873
873
  let context = {
874
874
  disposables: [],
875
875
  ruleCache: new Set(),
876
+ candidateRuleCache: new Map(),
876
877
  classCache: new Map(),
877
878
  applyClassCache: new Map(),
878
879
  notClassCache: new Set(),
@@ -17,6 +17,17 @@ export default function getAllConfigs(config) {
17
17
  }),
18
18
  },
19
19
  },
20
+
21
+ disableColorOpacityUtilitiesByDefault: {
22
+ corePlugins: {
23
+ backgroundOpacity: false,
24
+ borderOpacity: false,
25
+ divideOpacity: false,
26
+ placeholderOpacity: false,
27
+ ringOpacity: false,
28
+ textOpacity: false,
29
+ },
30
+ },
20
31
  }
21
32
 
22
33
  const experimentals = Object.keys(features)
package/types/config.d.ts CHANGED
@@ -49,7 +49,7 @@ type SafelistConfig =
49
49
  | string[]
50
50
  | {
51
51
  pattern: RegExp
52
- variants: string[]
52
+ variants?: string[]
53
53
  }[]
54
54
 
55
55
  // Presets related config
@@ -153,7 +153,14 @@ interface ThemeConfig {
153
153
  objectPosition: ResolvableTo<KeyValuePair>
154
154
  padding: ThemeConfig['spacing']
155
155
  textIndent: ThemeConfig['spacing']
156
- fontFamily: ResolvableTo<KeyValuePair<string, string[]>>
156
+ fontFamily: ResolvableTo<
157
+ KeyValuePair<
158
+ string,
159
+ | string
160
+ | string[]
161
+ | [fontFamily: string | string[], configuration: Partial<{ fontFeatureSettings: string }>]
162
+ >
163
+ >
157
164
  fontSize: ResolvableTo<
158
165
  KeyValuePair<
159
166
  string,