tailwindcss 0.0.0-insiders.da85042 → 0.0.0-insiders.db50bbb

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
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
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
14
  - Add future flag to disable color opacity utility plugins ([#9088](https://github.com/tailwindlabs/tailwindcss/pull/9088))
15
15
  - Add negative value support for `outline-offset` ([#9136](https://github.com/tailwindlabs/tailwindcss/pull/9136))
16
+ - Allow negating utilities using min/max/clamp ([#9237](https://github.com/tailwindlabs/tailwindcss/pull/9237))
16
17
 
17
18
  ### Fixed
18
19
 
@@ -21,7 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
22
  - Sort tags before classes when `@applying` a selector with joined classes ([#9107](https://github.com/tailwindlabs/tailwindcss/pull/9107))
22
23
  - Remove invalid `outline-hidden` utility ([#9147](https://github.com/tailwindlabs/tailwindcss/pull/9147))
23
24
  - 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
+ - Don't stop watching atomically renamed files ([#9173](https://github.com/tailwindlabs/tailwindcss/pull/9173), [#9215](https://github.com/tailwindlabs/tailwindcss/pull/9215))
26
+ - Re-use existing entries in the rule cache ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
27
+ - Don't output duplicate utilities ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
28
+ - Fix `fontFamily` config TypeScript types ([#9214](https://github.com/tailwindlabs/tailwindcss/pull/9214))
29
+ - Handle variants on complex selector utilities ([#9262](https://github.com/tailwindlabs/tailwindcss/pull/9262))
25
30
 
26
31
  ## [3.1.8] - 2022-08-05
27
32
 
package/lib/cli.js CHANGED
@@ -21,6 +21,7 @@ const _getModuleDependencies = /*#__PURE__*/ _interopRequireDefault(require("./l
21
21
  const _log = /*#__PURE__*/ _interopRequireDefault(require("./util/log"));
22
22
  const _packageJson = /*#__PURE__*/ _interopRequireDefault(require("../package.json"));
23
23
  const _normalizePath = /*#__PURE__*/ _interopRequireDefault(require("normalize-path"));
24
+ const _micromatch = /*#__PURE__*/ _interopRequireDefault(require("micromatch"));
24
25
  const _validateConfigJs = require("./util/validateConfig.js");
25
26
  function _interopRequireDefault(obj) {
26
27
  return obj && obj.__esModule ? obj : {
@@ -741,6 +742,25 @@ async function build() {
741
742
  return result;
742
743
  }
743
744
  let config = refreshConfig(configPath);
745
+ let contentPatterns = refreshContentPatterns(config);
746
+ /**
747
+ * @param {import('../types/config.js').RequiredConfig} config
748
+ * @return {{all: string[], dynamic: string[], static: string[]}}
749
+ **/ function refreshContentPatterns(config) {
750
+ let globs = extractFileGlobs(config);
751
+ let tasks = _fastGlob.default.generateTasks(globs, {
752
+ absolute: true
753
+ });
754
+ let dynamicPatterns = tasks.filter((task)=>task.dynamic).flatMap((task)=>task.patterns);
755
+ let staticPatterns = tasks.filter((task)=>!task.dynamic).flatMap((task)=>task.patterns);
756
+ return {
757
+ all: [
758
+ ...staticPatterns,
759
+ ...dynamicPatterns
760
+ ],
761
+ dynamic: dynamicPatterns
762
+ };
763
+ }
744
764
  if (input) {
745
765
  contextDependencies.add(_path.default.resolve(input));
746
766
  }
@@ -767,6 +787,7 @@ async function build() {
767
787
  env.DEBUG && console.time("Resolve config");
768
788
  context = null;
769
789
  config = refreshConfig(configPath);
790
+ contentPatterns = refreshContentPatterns(config);
770
791
  env.DEBUG && console.timeEnd("Resolve config");
771
792
  env.DEBUG && console.time("Watch new files");
772
793
  let globs = extractFileGlobs(config);
@@ -816,7 +837,15 @@ async function build() {
816
837
  // Restore watching any files that are "removed"
817
838
  // 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
839
  // TODO: An an optimization we should allow removal when the config changes
819
- watcher.on("unlink", (file)=>watcher.add(file));
840
+ watcher.on("unlink", (file)=>{
841
+ file = (0, _normalizePath.default)(file);
842
+ // Only re-add the file if it's not covered by a dynamic pattern
843
+ if (!_micromatch.default.some([
844
+ file
845
+ ], contentPatterns.dynamic)) {
846
+ watcher.add(file);
847
+ }
848
+ });
820
849
  // Some applications such as Visual Studio (but not VS Code)
821
850
  // will only fire a rename event for atomic writes and not a change event
822
851
  // This is very likely a chokidar bug but it's one we need to work around
@@ -825,10 +854,16 @@ async function build() {
825
854
  if (evt !== "rename") {
826
855
  return;
827
856
  }
828
- let watchedPath = _path.default.resolve(meta.watchedPath);
857
+ let watchedPath = meta.watchedPath;
829
858
  // Watched path might be the file itself
830
859
  // Or the directory it is in
831
- filePath = watchedPath.endsWith(filePath) ? watchedPath : _path.default.resolve(watchedPath, filePath);
860
+ filePath = watchedPath.endsWith(filePath) ? watchedPath : _path.default.join(watchedPath, filePath);
861
+ // Skip this event since the files it is for does not match any of the registered content globs
862
+ if (!_micromatch.default.some([
863
+ filePath
864
+ ], contentPatterns.all)) {
865
+ return;
866
+ }
832
867
  // Skip since we've already queued a rebuild for this file that hasn't happened yet
833
868
  if (pendingRebuilds.has(filePath)) {
834
869
  return;
@@ -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(),
@@ -45,6 +45,48 @@ function formatVariantSelector(current, ...others) {
45
45
  }
46
46
  return current;
47
47
  }
48
+ /**
49
+ * Given any node in a selector this gets the "simple" selector it's a part of
50
+ * A simple selector is just a list of nodes without any combinators
51
+ * Technically :is(), :not(), :has(), etc… can have combinators but those are nested
52
+ * inside the relevant node and won't be picked up so they're fine to ignore
53
+ *
54
+ * @param {import('postcss-selector-parser').Node} node
55
+ * @returns {import('postcss-selector-parser').Node[]}
56
+ **/ function simpleSelectorForNode(node) {
57
+ /** @type {import('postcss-selector-parser').Node[]} */ let nodes = [];
58
+ // Walk backwards until we hit a combinator node (or the start)
59
+ while(node.prev() && node.prev().type !== "combinator"){
60
+ node = node.prev();
61
+ }
62
+ // Now record all non-combinator nodes until we hit one (or the end)
63
+ while(node && node.type !== "combinator"){
64
+ nodes.push(node);
65
+ node = node.next();
66
+ }
67
+ return nodes;
68
+ }
69
+ /**
70
+ * Resorts the nodes in a selector to ensure they're in the correct order
71
+ * Tags go before classes, and pseudo classes go after classes
72
+ *
73
+ * @param {import('postcss-selector-parser').Selector} sel
74
+ * @returns {import('postcss-selector-parser').Selector}
75
+ **/ function resortSelector(sel) {
76
+ sel.sort((a, b)=>{
77
+ if (a.type === "tag" && b.type === "class") {
78
+ return -1;
79
+ } else if (a.type === "class" && b.type === "tag") {
80
+ return 1;
81
+ } else if (a.type === "class" && b.type === "pseudo" && b.value !== ":merge") {
82
+ return -1;
83
+ } else if (a.type === "pseudo" && a.value !== ":merge" && b.type === "class") {
84
+ return 1;
85
+ }
86
+ return sel.index(a) - sel.index(b);
87
+ });
88
+ return sel;
89
+ }
48
90
  var ref1;
49
91
  function finalizeSelector(format, { selector , candidate , context , isArbitraryVariant , // Split by the separator, but ignore the separator inside square brackets:
50
92
  //
@@ -87,12 +129,40 @@ base =candidate.split(new RegExp(`\\${(ref1 = context === null || context === vo
87
129
  node.raws.value = (0, _escapeClassName.default)((0, _unesc.default)(node.raws.value));
88
130
  }
89
131
  });
132
+ let simpleStart = _postcssSelectorParser.default.comment({
133
+ value: "/*__simple__*/"
134
+ });
135
+ let simpleEnd = _postcssSelectorParser.default.comment({
136
+ value: "/*__simple__*/"
137
+ });
90
138
  // We can safely replace the escaped base now, since the `base` section is
91
139
  // now in a normalized escaped value.
92
140
  ast.walkClasses((node)=>{
93
- if (node.value === base) {
94
- node.replaceWith(...formatAst.nodes);
141
+ if (node.value !== base) {
142
+ return;
143
+ }
144
+ let parent = node.parent;
145
+ let formatNodes = formatAst.nodes[0].nodes;
146
+ // Perf optimization: if the parent is a single class we can just replace it and be done
147
+ if (parent.nodes.length === 1) {
148
+ node.replaceWith(...formatNodes);
149
+ return;
150
+ }
151
+ let simpleSelector = simpleSelectorForNode(node);
152
+ parent.insertBefore(simpleSelector[0], simpleStart);
153
+ parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd);
154
+ for (let child of formatNodes){
155
+ parent.insertBefore(simpleSelector[0], child);
95
156
  }
157
+ node.remove();
158
+ // Re-sort the simple selector to ensure it's in the correct order
159
+ simpleSelector = simpleSelectorForNode(simpleStart);
160
+ let firstNode = parent.index(simpleStart);
161
+ parent.nodes.splice(firstNode, simpleSelector.length, ...resortSelector(_postcssSelectorParser.default.selector({
162
+ nodes: simpleSelector
163
+ })).nodes);
164
+ simpleStart.remove();
165
+ simpleEnd.remove();
96
166
  });
97
167
  // This will make sure to move pseudo's to the correct spot (the end for
98
168
  // pseudo elements) because otherwise the selector will never work
@@ -15,7 +15,20 @@ function _default(value) {
15
15
  if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) {
16
16
  return value.replace(/^[+-]?/, (sign)=>sign === "-" ? "" : "-");
17
17
  }
18
- if (value.includes("var(") || value.includes("calc(")) {
19
- return `calc(${value} * -1)`;
18
+ // What functions we support negating numeric values for
19
+ // var() isn't inherently a numeric function but we support it anyway
20
+ // The trigonometric functions are omitted because you'll need to use calc(…) with them _anyway_
21
+ // to produce generally useful results and that will be covered already
22
+ let numericFunctions = [
23
+ "var",
24
+ "calc",
25
+ "min",
26
+ "max",
27
+ "clamp"
28
+ ];
29
+ for (const fn of numericFunctions){
30
+ if (value.includes(`${fn}(`)) {
31
+ return `calc(${value} * -1)`;
32
+ }
20
33
  }
21
34
  }
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "default", {
7
7
  get: ()=>transformThemeValue
8
8
  });
9
9
  const _postcss = /*#__PURE__*/ _interopRequireDefault(require("postcss"));
10
+ const _isPlainObject = /*#__PURE__*/ _interopRequireDefault(require("./isPlainObject"));
10
11
  function _interopRequireDefault(obj) {
11
12
  return obj && obj.__esModule ? obj : {
12
13
  default: obj
@@ -23,8 +24,14 @@ function transformThemeValue(themeSection) {
23
24
  return value;
24
25
  };
25
26
  }
27
+ if (themeSection === "fontFamily") {
28
+ return (value)=>{
29
+ if (typeof value === "function") value = value({});
30
+ let families = Array.isArray(value) && (0, _isPlainObject.default)(value[1]) ? value[0] : value;
31
+ return Array.isArray(families) ? families.join(", ") : families;
32
+ };
33
+ }
26
34
  if ([
27
- "fontFamily",
28
35
  "boxShadow",
29
36
  "transitionProperty",
30
37
  "transitionDuration",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwindcss",
3
- "version": "0.0.0-insiders.da85042",
3
+ "version": "0.0.0-insiders.db50bbb",
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
50
  "cssnano": "^5.1.13",
51
51
  "esbuild": "^0.14.54",
52
- "eslint": "^8.21.0",
52
+ "eslint": "^8.23.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
@@ -17,6 +17,7 @@ import getModuleDependencies from './lib/getModuleDependencies'
17
17
  import log from './util/log'
18
18
  import packageJson from '../package.json'
19
19
  import normalizePath from 'normalize-path'
20
+ import micromatch from 'micromatch'
20
21
  import { validateConfig } from './util/validateConfig.js'
21
22
 
22
23
  let env = {
@@ -837,6 +838,23 @@ async function build() {
837
838
  }
838
839
 
839
840
  let config = refreshConfig(configPath)
841
+ let contentPatterns = refreshContentPatterns(config)
842
+
843
+ /**
844
+ * @param {import('../types/config.js').RequiredConfig} config
845
+ * @return {{all: string[], dynamic: string[], static: string[]}}
846
+ **/
847
+ function refreshContentPatterns(config) {
848
+ let globs = extractFileGlobs(config)
849
+ let tasks = fastGlob.generateTasks(globs, { absolute: true })
850
+ let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns)
851
+ let staticPatterns = tasks.filter((task) => !task.dynamic).flatMap((task) => task.patterns)
852
+
853
+ return {
854
+ all: [...staticPatterns, ...dynamicPatterns],
855
+ dynamic: dynamicPatterns,
856
+ }
857
+ }
840
858
 
841
859
  if (input) {
842
860
  contextDependencies.add(path.resolve(input))
@@ -867,6 +885,7 @@ async function build() {
867
885
  env.DEBUG && console.time('Resolve config')
868
886
  context = null
869
887
  config = refreshConfig(configPath)
888
+ contentPatterns = refreshContentPatterns(config)
870
889
  env.DEBUG && console.timeEnd('Resolve config')
871
890
 
872
891
  env.DEBUG && console.time('Watch new files')
@@ -924,7 +943,14 @@ async function build() {
924
943
  // Restore watching any files that are "removed"
925
944
  // 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
945
  // TODO: An an optimization we should allow removal when the config changes
927
- watcher.on('unlink', (file) => watcher.add(file))
946
+ watcher.on('unlink', (file) => {
947
+ file = normalizePath(file)
948
+
949
+ // Only re-add the file if it's not covered by a dynamic pattern
950
+ if (!micromatch.some([file], contentPatterns.dynamic)) {
951
+ watcher.add(file)
952
+ }
953
+ })
928
954
 
929
955
  // Some applications such as Visual Studio (but not VS Code)
930
956
  // will only fire a rename event for atomic writes and not a change event
@@ -935,11 +961,16 @@ async function build() {
935
961
  return
936
962
  }
937
963
 
938
- let watchedPath = path.resolve(meta.watchedPath)
964
+ let watchedPath = meta.watchedPath
939
965
 
940
966
  // Watched path might be the file itself
941
967
  // Or the directory it is in
942
- filePath = watchedPath.endsWith(filePath) ? watchedPath : path.resolve(watchedPath, filePath)
968
+ filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
969
+
970
+ // Skip this event since the files it is for does not match any of the registered content globs
971
+ if (!micromatch.some([filePath], contentPatterns.all)) {
972
+ return
973
+ }
943
974
 
944
975
  // Skip since we've already queued a rebuild for this file that hasn't happened yet
945
976
  if (pendingRebuilds.has(filePath)) {
@@ -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(),
@@ -29,6 +29,58 @@ export function formatVariantSelector(current, ...others) {
29
29
  return current
30
30
  }
31
31
 
32
+ /**
33
+ * Given any node in a selector this gets the "simple" selector it's a part of
34
+ * A simple selector is just a list of nodes without any combinators
35
+ * Technically :is(), :not(), :has(), etc… can have combinators but those are nested
36
+ * inside the relevant node and won't be picked up so they're fine to ignore
37
+ *
38
+ * @param {import('postcss-selector-parser').Node} node
39
+ * @returns {import('postcss-selector-parser').Node[]}
40
+ **/
41
+ function simpleSelectorForNode(node) {
42
+ /** @type {import('postcss-selector-parser').Node[]} */
43
+ let nodes = []
44
+
45
+ // Walk backwards until we hit a combinator node (or the start)
46
+ while (node.prev() && node.prev().type !== 'combinator') {
47
+ node = node.prev()
48
+ }
49
+
50
+ // Now record all non-combinator nodes until we hit one (or the end)
51
+ while (node && node.type !== 'combinator') {
52
+ nodes.push(node)
53
+ node = node.next()
54
+ }
55
+
56
+ return nodes
57
+ }
58
+
59
+ /**
60
+ * Resorts the nodes in a selector to ensure they're in the correct order
61
+ * Tags go before classes, and pseudo classes go after classes
62
+ *
63
+ * @param {import('postcss-selector-parser').Selector} sel
64
+ * @returns {import('postcss-selector-parser').Selector}
65
+ **/
66
+ function resortSelector(sel) {
67
+ sel.sort((a, b) => {
68
+ if (a.type === 'tag' && b.type === 'class') {
69
+ return -1
70
+ } else if (a.type === 'class' && b.type === 'tag') {
71
+ return 1
72
+ } else if (a.type === 'class' && b.type === 'pseudo' && b.value !== ':merge') {
73
+ return -1
74
+ } else if (a.type === 'pseudo' && a.value !== ':merge' && b.type === 'class') {
75
+ return 1
76
+ }
77
+
78
+ return sel.index(a) - sel.index(b)
79
+ })
80
+
81
+ return sel
82
+ }
83
+
32
84
  export function finalizeSelector(
33
85
  format,
34
86
  {
@@ -88,12 +140,47 @@ export function finalizeSelector(
88
140
  }
89
141
  })
90
142
 
143
+ let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
144
+ let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
145
+
91
146
  // We can safely replace the escaped base now, since the `base` section is
92
147
  // now in a normalized escaped value.
93
148
  ast.walkClasses((node) => {
94
- if (node.value === base) {
95
- node.replaceWith(...formatAst.nodes)
149
+ if (node.value !== base) {
150
+ return
151
+ }
152
+
153
+ let parent = node.parent
154
+ let formatNodes = formatAst.nodes[0].nodes
155
+
156
+ // Perf optimization: if the parent is a single class we can just replace it and be done
157
+ if (parent.nodes.length === 1) {
158
+ node.replaceWith(...formatNodes)
159
+ return
96
160
  }
161
+
162
+ let simpleSelector = simpleSelectorForNode(node)
163
+ parent.insertBefore(simpleSelector[0], simpleStart)
164
+ parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
165
+
166
+ for (let child of formatNodes) {
167
+ parent.insertBefore(simpleSelector[0], child)
168
+ }
169
+
170
+ node.remove()
171
+
172
+ // Re-sort the simple selector to ensure it's in the correct order
173
+ simpleSelector = simpleSelectorForNode(simpleStart)
174
+ let firstNode = parent.index(simpleStart)
175
+
176
+ parent.nodes.splice(
177
+ firstNode,
178
+ simpleSelector.length,
179
+ ...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
180
+ )
181
+
182
+ simpleStart.remove()
183
+ simpleEnd.remove()
97
184
  })
98
185
 
99
186
  // This will make sure to move pseudo's to the correct spot (the end for
@@ -10,7 +10,15 @@ export default function (value) {
10
10
  return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
11
11
  }
12
12
 
13
- if (value.includes('var(') || value.includes('calc(')) {
14
- return `calc(${value} * -1)`
13
+ // What functions we support negating numeric values for
14
+ // var() isn't inherently a numeric function but we support it anyway
15
+ // The trigonometric functions are omitted because you'll need to use calc(…) with them _anyway_
16
+ // to produce generally useful results and that will be covered already
17
+ let numericFunctions = ['var', 'calc', 'min', 'max', 'clamp']
18
+
19
+ for (const fn of numericFunctions) {
20
+ if (value.includes(`${fn}(`)) {
21
+ return `calc(${value} * -1)`
22
+ }
15
23
  }
16
24
  }
@@ -1,4 +1,5 @@
1
1
  import postcss from 'postcss'
2
+ import isPlainObject from './isPlainObject'
2
3
 
3
4
  export default function transformThemeValue(themeSection) {
4
5
  if (['fontSize', 'outline'].includes(themeSection)) {
@@ -10,9 +11,16 @@ export default function transformThemeValue(themeSection) {
10
11
  }
11
12
  }
12
13
 
14
+ if (themeSection === 'fontFamily') {
15
+ return (value) => {
16
+ if (typeof value === 'function') value = value({})
17
+ let families = Array.isArray(value) && isPlainObject(value[1]) ? value[0] : value
18
+ return Array.isArray(families) ? families.join(', ') : families
19
+ }
20
+ }
21
+
13
22
  if (
14
23
  [
15
- 'fontFamily',
16
24
  'boxShadow',
17
25
  'transitionProperty',
18
26
  'transitionDuration',
package/types/config.d.ts CHANGED
@@ -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,