Repository: tailwindlabs/tailwindcss

Problem statement:
Consider not relying on previously defined mixin in file for "@apply"
Currently `@apply` works by walking through all previously defined "mixins"[1] in the file before substituting the found ones, which constrains you to either only use one file, or to import all of your css in one place [2] before defining your custom utilities.
It of course won't work with popular css-in-js solutions [3] simply because the css fragment using `@apply` has no knowledge of tailwind's utilities, yielding the dreaded `unable to find $SELECTOR` error.
Importing them beforehand using `@tailwind utilities` everywhere is obviously not a solution because of duplication.

An idea would be to:
1. Check if the "mixin" has been defined before in the file/fragment, as usual
2. **If none was found, try to get it from a global map of generated utilities**
3. Throw if both of these steps yield nothing


---
[1] which are actually selectors, making me wonder why the mixin terminology was used
[2] https://github.com/tailwindcss/tailwindcss/issues/53#issuecomment-341413622
[3] such as styled-component, emotionjs or even vue's "single file component" (https://github.com/tailwindcss/tailwindcss/issues/1#issuecomment-341918988)
They all are processed by postcss under the hood


Hints:
I tried using https://github.com/ben-eb/postcss-discard-duplicates to discard the duplicate code created by `@tailwind utilities;` but unfortunately it just did not work for me at all. Has anyone else been able to get it to work?

The only other option would be to have the core include a "reverse parser", something that can take in a css class, and spit out a config key path. Probably not the easiest thing to do, but with it you could avoid including `@tailwind utilities;` all together when using `@apply` in something like a Vue single file component.  
Follow up: I was able to dedupe the rules using https://github.com/ChristianMurphy/postcss-combine-duplicated-selectors, so that is an option.

Patch:
diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js
--- a/src/lib/substituteClassApplyAtRules.js
+++ b/src/lib/substituteClassApplyAtRules.js
@@ -15,40 +15,57 @@ function buildClassTable(css) {
   return classTable
 }
 
+function buildShadowTable(generatedUtilities) {
+  const utilities = postcss.root()
+
+  generatedUtilities.walkAtRules('variants', atRule => {
+    utilities.append(atRule.clone().nodes)
+  })
+
+  return buildClassTable(utilities)
+}
+
 function normalizeClassName(className) {
   return `.${escapeClassName(_.trimStart(className, '.'))}`
 }
 
-function findMixin(classTable, mixin, onError) {
-  const matches = _.get(classTable, mixin, [])
+function findClass(classToApply, classTable, shadowLookup, onError) {
+  const matches = _.get(classTable, classToApply, [])
 
   if (_.isEmpty(matches)) {
-    // prettier-ignore
-    onError(`\`@apply\` cannot be used with \`${mixin}\` because \`${mixin}\` either cannot be found, or it's actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that \`${mixin}\` exists, make sure that any \`@import\` statements are being properly processed *before* Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`)
+    if (_.isEmpty(shadowLookup)) {
+      // prettier-ignore
+      throw onError(`\`@apply\` cannot be used with \`${classToApply}\` because \`${classToApply}\` either cannot be found, or it's actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that \`${classToApply}\` exists, make sure that any \`@import\` statements are being properly processed *before* Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`)
+    }
+
+    return findClass(classToApply, shadowLookup, {}, onError)
   }
 
   if (matches.length > 1) {
     // prettier-ignore
-    onError(`\`@apply\` cannot be used with ${mixin} because ${mixin} is included in multiple rulesets.`)
+    throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is included in multiple rulesets.`)
   }
 
   const [match] = matches
 
   if (match.parent.type !== 'root') {
     // prettier-ignore
-    onError(`\`@apply\` cannot be used with ${mixin} because ${mixin} is nested inside of an at-rule (@${match.parent.name}).`)
+    throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is nested inside of an at-rule (@${match.parent.name}).`)
   }
 
   return match.clone().nodes
 }
 
-export default function() {
+export default function(config, generatedUtilities) {
   return function(css) {
     const classLookup = buildClassTable(css)
+    const shadowLookup = _.get(config, 'experiments.shadowLookup', false)
+      ? buildShadowTable(generatedUtilities)
+      : {}
 
     css.walkRules(rule => {
       rule.walkAtRules('apply', atRule => {
-        const mixins = postcss.list.space(atRule.params)
+        const classesAndProperties = postcss.list.space(atRule.params)
 
         /*
          * Don't wreck CSSNext-style @apply rules:
@@ -57,20 +74,20 @@ export default function() {
          * These are deprecated in CSSNext but still playing it safe for now.
          * We might consider renaming this at-rule.
          */
-        const [customProperties, classes] = _.partition(mixins, mixin => {
-          return _.startsWith(mixin, '--')
+        const [customProperties, classes] = _.partition(classesAndProperties, classOrProperty => {
+          return _.startsWith(classOrProperty, '--')
         })
 
         const decls = _(classes)
-          .reject(mixin => mixin === '!important')
-          .flatMap(mixin => {
-            return findMixin(classLookup, normalizeClassName(mixin), message => {
-              throw atRule.error(message)
+          .reject(cssClass => cssClass === '!important')
+          .flatMap(cssClass => {
+            return findClass(normalizeClassName(cssClass), classLookup, shadowLookup, message => {
+              return atRule.error(message)
             })
           })
           .value()
 
-        _.tap(_.last(mixins) === '!important', important => {
+        _.tap(_.last(classesAndProperties) === '!important', important => {
           decls.forEach(decl => (decl.important = important))
         })
 
diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js
--- a/src/lib/substituteTailwindAtRules.js
+++ b/src/lib/substituteTailwindAtRules.js
@@ -1,10 +1,7 @@
 import fs from 'fs'
 import postcss from 'postcss'
-import utilityModules from '../utilityModules'
-import prefixTree from '../util/prefixTree'
-import generateModules from '../util/generateModules'
 
-export default function(config, { components: pluginComponents, utilities: pluginUtilities }) {
+export default function(config, { components: pluginComponents }, generatedUtilities) {
   return function(css) {
     css.walkAtRules('tailwind', atRule => {
       if (atRule.params === 'preflight') {
@@ -30,27 +27,8 @@ export default function(config, { components: pluginComponents, utilities: plugi
       }
 
       if (atRule.params === 'utilities') {
-        const utilities = generateModules(utilityModules, config.modules, config)
-
-        if (config.options.important) {
-          utilities.walkDecls(decl => (decl.important = true))
-        }
-
-        const tailwindUtilityTree = postcss.root({
-          nodes: utilities.nodes,
-        })
-
-        const pluginUtilityTree = postcss.root({
-          nodes: pluginUtilities,
-        })
-
-        prefixTree(tailwindUtilityTree, config.options.prefix)
-
-        tailwindUtilityTree.walk(node => (node.source = atRule.source))
-        pluginUtilityTree.walk(node => (node.source = atRule.source))
-
-        atRule.before(tailwindUtilityTree)
-        atRule.before(pluginUtilityTree)
+        generatedUtilities.walk(node => (node.source = atRule.source))
+        atRule.before(generatedUtilities)
         atRule.remove()
       }
     })
diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js
--- a/src/processTailwindFeatures.js
+++ b/src/processTailwindFeatures.js
@@ -6,18 +6,21 @@ import substituteVariantsAtRules from './lib/substituteVariantsAtRules'
 import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules'
 import substituteScreenAtRules from './lib/substituteScreenAtRules'
 import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules'
+
+import generateUtilities from './util/generateUtilities'
 import processPlugins from './util/processPlugins'
 
 export default function(lazyConfig) {
   const config = lazyConfig()
-  const plugins = processPlugins(config)
+  const processedPlugins = processPlugins(config)
+  const utilities = generateUtilities(config, processedPlugins.utilities)
 
   return postcss([
-    substituteTailwindAtRules(config, plugins),
+    substituteTailwindAtRules(config, processedPlugins, utilities.clone()),
     evaluateTailwindFunctions(config),
-    substituteVariantsAtRules(config, plugins),
+    substituteVariantsAtRules(config, processedPlugins),
     substituteResponsiveAtRules(config),
     substituteScreenAtRules(config),
-    substituteClassApplyAtRules(config),
+    substituteClassApplyAtRules(config, utilities.clone()),
   ])
 }
diff --git a/src/util/generateUtilities.js b/src/util/generateUtilities.js
new file mode 100644
--- /dev/null
+++ b/src/util/generateUtilities.js
@@ -0,0 +1,24 @@
+import _ from 'lodash'
+import postcss from 'postcss'
+import utilityModules from '../utilityModules'
+import prefixTree from '../util/prefixTree'
+import generateModules from '../util/generateModules'
+
+export default function(config, pluginUtilities) {
+  const utilities = generateModules(utilityModules, config.modules, config)
+
+  if (config.options.important) {
+    utilities.walkDecls(decl => (decl.important = true))
+  }
+
+  const tailwindUtilityTree = postcss.root({
+    nodes: utilities.nodes,
+  })
+
+  prefixTree(tailwindUtilityTree, config.options.prefix)
+
+  return _.tap(postcss.root(), root => {
+    root.append(tailwindUtilityTree.nodes)
+    root.append(pluginUtilities)
+  })
+}


Test patch:
diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js
--- a/__tests__/applyAtRule.test.js
+++ b/__tests__/applyAtRule.test.js
@@ -1,8 +1,12 @@
 import postcss from 'postcss'
 import plugin from '../src/lib/substituteClassApplyAtRules'
+import generateUtilities from '../src/util/generateUtilities'
+import defaultConfig from '../defaultConfig.stub.js'
 
-function run(input, opts = {}) {
-  return postcss([plugin(opts)]).process(input, { from: undefined })
+const defaultUtilities = generateUtilities(defaultConfig, [])
+
+function run(input, config = defaultConfig, utilities = defaultUtilities) {
+  return postcss([plugin(config, utilities)]).process(input, { from: undefined })
 }
 
 test("it copies a class's declarations into itself", () => {
@@ -168,3 +172,23 @@ test('it does not match classes that have multiple rules', () => {
     expect(e).toMatchObject({ name: 'CssSyntaxError' })
   })
 })
+
+test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => {
+  const input = `
+    .foo { @apply .mt-4; }
+  `
+
+  const expected = `
+    .foo { margin-top: 1rem; }
+  `
+
+  const config = {
+    ...defaultConfig,
+    experiments: { shadowLookup: true },
+  }
+
+  return run(input, config).then(result => {
+    expect(result.css).toEqual(expected)
+    expect(result.warnings().length).toBe(0)
+  })
+})