此页面需要javascript支持,请在浏览器中启用javascript

How to get the context of tailwindcss at runtime?

tailwindcss
context
runtime
plugin
postcss
共1350个字,阅读时间 7 分钟
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://icebreaker.top/articles/2023/6/30-how-i-get-tailwindcss-context-at-runtime

How to get the context of tailwindcss at runtime?

The core principle of tailwindcss-patch

Preface

When we use tailwindcss for development, we often struggle with the problem of how to redevelop tailwindcss.

Because currently tailwindcss only can extract the corresponding class name from our source code and generate the corresponding css, but can not modify the html/js package result generated by the source code.

What is this meaning?

For example I write a class named: md:text-[20px], some non-h5 platforms may not support characters like :, [, ], so I want to escape it while compiling.

md:text-[20px] => md_text-_20px_

Or maybe we want to obfuscate all the class names generated by tailwindcss

  • md:text-[20px] => tw-a
  • text-[20px] => tw-b
  • text-xs => tw-c

This kind of functionality cannot be done by plugin and preset of tailwindcss alone, and a single postcss plugin can neither. We have to work with a packaging tool (webpack/vite...) to get the currently running tailwindcss context in the packaging tool so that all html/js/css files can be dynamically changed during build time.

tailwindcss context object

Through debugging, we know that tailwindcss context will be an object constructed at runtime, which contains mainly the following fields.

Image

It contains some core methods of tailwindcss and holds some data, etc.

How to get the context outside tailwindcss?

Usually, we want to get the context in the webpack/vite plugin so that we can make changes to our code based on some key information in the context.

However, tailwindcss itself is mostly used as a postcss plugin, so how do we get a postcss plugin to communicate with the webpack/vite/gulp plugin?

When I looked through the source code, I found that tailwindcss itself is great as a plugin. However, the source code was rather closed and could not expose the context for other code to use.

After reading and studying the source code, I came up with a solution to expose the context to other code, i.e. modify the tailwindcss source code to expose the context while keeping all the original functionality: put a patch on the running tailwindcss code.

Patch tailwindcss

As we want to patch tailwindcss, we need to know where exactly tailwindcss is running in our local code.

We install version 3.3.2 of tailwindcss (the latest version as of 20230630) and go to node_modules/tailwindcss/package.json

{
  "name": "tailwindcss",
  "version": "3.3.2",
  "description": "A utility-first CSS framework for rapidly building custom user interfaces",
  "license": "MIT",
  "main": "lib/index.js",
  "types": "types/index.d.ts",
  // ...
}

By the package.json#main field, we know that most of the currently running code is in the lib directory

Reading through the source code and looking through the lib directory, I came to the following conclusion, we need to make a change to the

  • tailwindcss/lib/processTailwindFeatures.js
  • tailwindcss/lib/plugin.js

These 2 files should be modified as follows.

tailwindcss/lib/processTailwindFeatures.js

function processTailwindFeatures (setupContext) {
  return function (root, result) {
    const {
      tailwindDirectives,
      applyDirectives
    } = (0, _normalizeTailwindDirectives.default)(root);
    (0, _detectNesting.default)()(root, result);
    // Partition apply rules that are found in the css
    // itself.
    (0, _partitionApplyAtRules.default)()(root, result)
    const context = setupContext({
      tailwindDirectives,
      applyDirectives,
      registerDependency (dependency) {
        result.messages.push({
          plugin: 'tailwindcss',
          parent: result.opts.from,
          ...dependency
        })
      },
      createContext (tailwindConfig, changedContent) {
        return (0, _setupContextUtils.createContext)(tailwindConfig, changedContent, root)
      }
    })(root, result)
    if (context.tailwindConfig.separator === '-') {
      throw new Error("The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead.")
    }
    (0, _featureFlags.issueFlagNotices)(context.tailwindConfig);
    (0, _expandTailwindAtRules.default)(context)(root, result);
    // Partition apply rules that are generated by
    // addComponents, addUtilities and so on.
    (0, _partitionApplyAtRules.default)()(root, result);
    (0, _expandApplyAtRules.default)(context)(root, result);
    (0, _evaluateTailwindFunctions.default)(context)(root, result);
    (0, _substituteScreenAtRules.default)(context)(root, result);
    (0, _resolveDefaultsAtRules.default)(context)(root, result);
    (0, _collapseAdjacentRules.default)(context)(root, result);
    (0, _collapseDuplicateDeclarations.default)(context)(root, result)
+    return context
  }
}

This file just adds a line at the end of the processTailwindFeatures method to give context to return out.

tailwindcss/lib/plugin.js

'use strict'

Object.defineProperty(exports, '__esModule', {
  value: true
})
const _setupTrackingContext = /* #__PURE__ */_interop_require_default(require('./lib/setupTrackingContext'))
const _processTailwindFeatures = /* #__PURE__ */_interop_require_default(require('./processTailwindFeatures'))
const _sharedState = require('./lib/sharedState')
const _findAtConfigPath = require('./lib/findAtConfigPath')
function _interop_require_default (obj) {
  return obj && obj.__esModule
    ? obj
    : {
        default: obj
      }
}

+ const contextRef = {
+   value: []
+ }
module.exports = function tailwindcss (configOrPath) {
  return {
    postcssPlugin: 'tailwindcss',
    plugins: [_sharedState.env.DEBUG && function (root) {
      console.log('\n')
      console.time('JIT TOTAL')
      return root
    }, function (root, result) {
+      // clear context each time        
+      contextRef.value.length = 0
      let _findAtConfigPath1
      // Use the path for the `@config` directive if it exists, otherwise use the
      // path for the file being processed
      configOrPath = (_findAtConfigPath1 = (0, _findAtConfigPath.findAtConfigPath)(root, result)) !== null && _findAtConfigPath1 !== void 0 ? _findAtConfigPath1 : configOrPath
      const context = (0, _setupTrackingContext.default)(configOrPath)
      if (root.type === 'document') {
        const roots = root.nodes.filter(node => node.type === 'root')
        for (const root of roots) {
          if (root.type === 'root') {
-            (0, _processTailwindFeatures.default)(context)(root, result);
+             contextRef.value.push((0, _processTailwindFeatures.default)(context)(root, result))
          }
        }
        return
      }
-      (0, _processTailwindFeatures.default)(context)(root, result);
+      contextRef.value.push((0, _processTailwindFeatures.default)(context)(root, result))
    }, false && function lightningCssPlugin (_root, result) {
      const postcss = require('postcss')
      const lightningcss = require('lightningcss')
      const browserslist = require('browserslist')
      try {
        const transformed = lightningcss.transform({
          filename: result.opts.from,
          code: Buffer.from(result.root.toString()),
          minify: false,
          sourceMap: !!result.map,
          inputSourceMap: result.map ? result.map.toString() : undefined,
          targets: typeof process !== 'undefined' && process.env.JEST_WORKER_ID
            ? {
                chrome: 106 << 16
              }
            : lightningcss.browserslistToTargets(browserslist(require('../package.json').browserslist)),
          drafts: {
            nesting: true,
            customMedia: true
          }
        })
        let _result_map
        result.map = Object.assign((_result_map = result.map) !== null && _result_map !== void 0 ? _result_map : {}, {
          toJSON () {
            return transformed.map.toJSON()
          },
          toString () {
            return transformed.map.toString()
          }
        })
        result.root = postcss.parse(transformed.code.toString('utf8'))
      } catch (err) {
        if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
          const lines = err.source.split('\n')
          err = new Error(['Error formatting using Lightning CSS:', '', ...['```css', ...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line), ' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(), ...lines.slice(err.loc.line, err.loc.line + 2), '```']].join('\n'))
        }
        if (Error.captureStackTrace) {
          Error.captureStackTrace(err, lightningCssPlugin)
        }
        throw err
      }
    }, _sharedState.env.DEBUG && function (root) {
      console.timeEnd('JIT TOTAL')
      console.log('\n')
      return root
    }].filter(Boolean)
  }
}
module.exports.postcss = true
+ // export contexts
+ module.exports.contextRef = contextRef

In this file, we create a contextRef object, push the context of tailwindcss into contextRef.value, export contextRef in the file, and clean up contextRef.value to avoid memory leaks.

Get the context API

At this point, we'll be able to create a method to get it

function getContexts() {
  const twPath = require.resolve('tailwindcss')

  const distPath = path.dirname(twPath)

  let injectFilePath = path.join(distPath, 'plugin.js')

  const mo = require(injectFilePath)
  if (mo.contextRef) {
    return mo.contextRef.value as any[]
  }
  return []
}

On success, we get the following information, where we want to get the names of all the generated tool classes from this field.

Image

Tips on writing packaged plugins

According to this method above, the tailwindcss context to be fetched is usually executed after postcss/postcss-loader to get the full data object.

So we can get the context at a later lifecycle, like processAssets of webpack compilation, or at the next loader after postcss-loader is executed.

Also vite/rollup can make some changes to the code using hooks that are executed later, such as generateBundle.

Summary

Inspired by ts-patch, I wrote tailwindcss-patch, and summarized all of the above into this package. Its main function is to make changes to the tailwindcss/lib source code, then expose the context and provide some handy tool classes.

It does version checking internally via semver so that different versions of tailwindcss are patched with different strategies.

In this way, I also implemented tailwindcss-mangle, it's an obfuscator tool for tailwindcss.

and also implemented weapp-tailwindcss, a tool that brings tailwindcss to weapp, a non-h5 runtime environment.

However, the last thing I have to say is that I wish the official tailwincss team would provide some way to get the runtime context in the external code. That would be much better than the less stable way I have.

Thanks for reading this far!