Source code analysis of TaiWindCss based on postCss

Posted by readourlines on Sat, 18 Sep 2021 09:13:21 +0200

preface

When I first saw the TaiWindCss library, just from the css point of view, it is similar to the bootstrap I've been in contact with before. It's just a pure css style library. However, after in-depth understanding, I found that it is more convenient and very fine than bootstrap. The only feeling is that each style needs to be assembled by DIV, including a series of hover s, active and other effects. Later, I went deep into TaiWindCss and found that the css style library was actually a css file style library written in js code and compiled. I was shocked at that time. js can still operate like this. A little awesome!!! So I went to understand and view the source code of TaiWindCss in detail. Go deep into the implementation logic of this.

Understanding postCss

What is postCss?

In popular terms: postCss is a development tool, a tool for converting CSS code with JavaScript tools and plug-ins. Support variables, blending, future CSS syntax, inline images, etc.

It has the following features and common functions:

  1. Enhance the readability of the code: Autoprefixer automatically obtains the popularity and supported properties of the browser, and helps you automatically prefix CSS rules according to these data.
  2. Bring future CSS features to today!: Help you convert the latest CSS syntax into syntax that most browsers can understand, and determine the polyfills you need according to your target browser or runtime environment
  3. Ending global CSS: CSS module can let you never worry about conflicts caused by too popular naming, just use the most meaningful name.
  4. Avoid errors in CSS code: enforce consistency constraints and avoid errors in style sheets by using stylelint. Stylelint is a modern CSS code checking tool. It supports the latest CSS syntax, including CSS like syntax, such as SCSS
  5. It can be used as a preprocessor, such as Sass, Less and Stylus. However, PostCSS is a modular tool, which is 3-30 times faster and more powerful than those before. And evolved a series of plug-ins to use.

Core principles / workflow of postCss

PostCSS includes CSS parser, CSS node tree API, a source mapping generator and a node tree stringer.

The main principles and core workflows of PostCSS are as follows:

  1. Read CSS file through fs
  2. Parse CSS into abstract syntax tree (AST tree) through parser
  3. Pass the AST tree to any number of plug-ins for processing
  4. Many plug-ins process data. The data passed between plug-ins is the AST tree
  5. Convert the processed AST tree into a string again through the stringifier

This series of workflows, to put it simply, is a series of operations on data. For this purpose, PostCSS provides a series of data operation APIs. For example, walkAtRules, walkComments, walkdecks, walkRules and other related APIs. See the following documents:
Official API document of postcss

TaiWindCss source code analysis

What is TaiWindCss?

From the perspective of the usage scenario of TaiWindCss time, we install TaiWindCss in the form of PostCSS plug-in. In essence, TaiWindCss is a PostCSS plug-in. From the perspective of actual project development, we have generally used PostCSS. At present, most popular / mainstream frameworks in the environment basically use PostCSS by default, For example, autoprefixer. We will basically use this.

For the use of PostCSS plug-ins, we generally need the following steps in the process of reuse:

  1. The PostCSS configuration file, postcss.config.js, adds the tailwindcss plug-in.
  2. The TaiWindCss plug-in requires a configuration file, such as tailwind.config.js.
  3. Inject @ tailwind ID into the less, sass and CSS files introduced by the project, and introduce base, components and utilities. Whether to import all depends on yourself.

After the relevant configuration is completed, in the process of project packaging or hot update, execute a series of PostCSS plug-ins to automatically package the relevant css referenced on our page into the css file we need, and then load it into our page.

Because we use a fragmented style file layout, such as the code on the page

class="col-start-1 row-start-1 flex sm:col-start-2 sm:row-span-3"

These class types are highly granular and reusable, and TailwindCss will automatically delete all unused CSS when building production files, which means that your last CSS file may be the smallest.

Implementation rationale

First, we clearly understand the workflow of postCss:

General steps:

  • Parsing CSS into an abstract syntax tree (AST tree)
  • Pass the AST tree to any number of plug-ins for processing
  • Convert the processed AST tree back to a string

There are several key processing mechanisms in PostCSS:

Source string → Tokenizer → Parser → AST → Processor → Stringifier

TaiWindCss follows a similar workflow:

Basic steps:

  • Parsing CSS into an abstract syntax tree (AST tree)
  • Read the plug-in configuration and generate a new abstract syntax tree according to the configuration file
  • "Pass" the AST tree to a series of data conversion operations (variable data cycle generation, nested class name cycle, etc.)
  • Clear the data traces left by a series of operations
  • Convert the processed AST tree back to a string

For example:

The current code block is as follows:
@layer components{
  @variants responsive{
    .container{
      width: 100%
    }
  }
}
Converted AST The code block is as follows:
{
  "raws": {
    "semicolon": false,
    "after": "\n\n"
  },
  "type": "root",
  "nodes": [
    {
      "raws": {
        "before": "",
        "between": "",
        "afterName": " ",
        "semicolon": false,
        "after": "\n"
      },
      "type": "atrule",
      "name": "layer",
      "source": {
        "start": {
          "line": 1,
          "column": 1
        },
        "input": {
          "css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n",
          "hasBOM": false,
          "id": "<input css 17>"
        },
        "end": {
          "line": 7,
          "column": 1
        }
      },
      "params": "components",
      "nodes": [
        {
          "raws": {
            "before": "\n  ",
            "between": "",
            "afterName": " ",
            "semicolon": false,
            "after": "\n  "
          },
          "type": "atrule",
          "name": "variants",
          "source": {
            "start": {
              "line": 2,
              "column": 3
            },
            "input": {
              "css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n",
              "hasBOM": false,
              "id": "<input css 17>"
            },
            "end": {
              "line": 6,
              "column": 3
            }
          },
          "params": "responsive",
          "nodes": [
            {
              "raws": {
                "before": "\n    ",
                "between": "",
                "semicolon": false,
                "after": "\n    "
              },
              "type": "rule",
              "nodes": [
                {
                  "raws": {
                    "before": "\n      ",
                    "between": ": "
                  },
                  "type": "decl",
                  "source": {
                    "start": {
                      "line": 4,
                      "column": 7
                    },
                    "input": {
                      "css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n",
                      "hasBOM": false,
                      "id": "<input css 17>"
                    },
                    "end": {
                      "line": 4,
                      "column": 17
                    }
                  },
                  "prop": "width",
                  "value": "100%"
                }
              ],
              "source": {
                "start": {
                  "line": 3,
                  "column": 5
                },
                "input": {
                  "css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n",
                  "hasBOM": false,
                  "id": "<input css 17>"
                },
                "end": {
                  "line": 5,
                  "column": 5
                }
              },
              "selector": ".container"
            }
          ]
        }
      ]
    }
  ],
  "source": {
    "input": {
      "css": "@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n  }\n}\n\n",
      "hasBOM": false,
      "id": "<input css 17>"
    },
    "start": {
      "line": 1,
      "column": 1
    }
  }
}

Tree structure diagram:
Repeated data operations, such as adding, deleting and adding, are to add data to the root - > nodes - > atrule / rule / comment / container / declaration - > nodes array. This series of operations uses a series of methods provided by the postCss document.

If we want to change the Css file into the following code:

@layer components{
  @variants responsive{
    .container{
      width: 100%
    }
    .c {
    	color: red
    }
  }
}

Then we just need to put the following data:

{
    "raws":{
        "before":"\n    ",
        "between":" ",
        "semicolon":false,
        "after":"\n    "
    },
    "type":"rule",
    "nodes":[
        {
            "raws":{
                "before":"\n    \t",
                "between":": "
            },
            "type":"decl",
            "source":{
                "start":{
                    "line":7,
                    "column":6
                },
                "input":{
                    "css":"@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n    .c {\n    \tcolor: red\n    }\n      \n  }\n}\n\n",
                    "hasBOM":false,
                    "id":"<input css 37>"
                },
                "end":{
                    "line":7,
                    "column":15
                }
            },
            "prop":"color",
            "value":"red"
        }
    ],
    "source":{
        "start":{
            "line":6,
            "column":5
        },
        "input":{
            "css":"@layer components{\n  @variants responsive{\n    .container{\n      width: 100%\n    }\n    .c {\n    \tcolor: red\n    }\n      \n  }\n}\n\n",
            "hasBOM":false,
            "id":"<input css 37>"
        },
        "end":{
            "line":8,
            "column":5
        }
    },
    "selector":".c"
}

Insert the data under root - > nodes - > atrule / rule / comment / container / declaration - > nodes.
The new tree is as follows:
In the figure, the new target is the AST tree chart mechanism fed back after we access the data last time.

From the above, we basically understand the basic principle of the implementation of TaiWindCss. In fact, it is a series of operations on the data flow to get the final CSS module we want, and then eliminate the redundant code and convert it into the CSS file we want.

Source file interpretation

github address of TaiWindCss: TaiWindCss github address

package.json

First, check the package.json file and find the scripts. The code is as follows:

"scripts": {
    "prebabelify": "rimraf lib",
    "babelify": "babel src --out-dir lib --copy-files",
    "rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js",
    "prepublishOnly": "npm run babelify && babel-node scripts/build.js",
    "style": "eslint .",
    "test": "jest",
    "posttest": "npm run style",
    "compat": "node scripts/compat.js --prepare",
    "compat:restore": "node scripts/compat.js --restore"
  },

Let's look at the line prepublishOnly, which is the entry point for the construction of TaiWindCss source code.

Find the project file scripts/build.js, and the core code is as follows:

import tailwind from '..'
function buildDistFile(filename, config = {}, outFilename = filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(`./${filename}.css`, (err, css) => {
      if (err) throw err

      return postcss([tailwind(config), require('autoprefixer')])
        .process(css, {
          from: `./${filename}.css`,
          to: `./dist/${outFilename}.css`,
        })
        .then((result) => {
          fs.writeFileSync(`./dist/${outFilename}.css`, result.css)
          return result
        })
        .then((result) => {
          const minified = new CleanCSS().minify(result.css)
          fs.writeFileSync(`./dist/${outFilename}.min.css`, minified.styles)
        })
        .then(resolve)
        .catch((error) => {
          console.log(error)
          reject()
        })
    })
  })
}

The core code description is to pass the file name (base), read the CSS file in the project, and convert the CSS file through the postcss plug-in tailwindcss. Then the CSS file is everywhere.

It is worth noting that

import tailwind from '..' 

tailwind here actually corresponds to package.json

 "main": "lib/index.js",

Read related configuration files

File directory SRC - > index.js
The core code is as follows:

const plugin = postcss.plugin('tailwindcss', (config) => {
  const plugins = []
  const resolvedConfigPath = resolveConfigPath(config)
  if (!_.isUndefined(resolvedConfigPath)) {
    plugins.push(registerConfigAsDependency(resolvedConfigPath))
  }
  console.log('plugins:', plugins)
  return postcss([
    ...plugins,
    processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),
    formatCSS,
  ])
})

Define the postCss plug-in named tailwindcss to parse css files.
References on the page:

import getAllConfigs from './util/getAllConfigs'
import { defaultConfigFile } from './constants'
import defaultConfig from '../stubs/defaultConfig.stub.js'

Is a series of configuration files for project initialization. The core read configuration file is tailwind.config.js, which is the file that needs to configure and manage our references when we use tailwindcss again.

Reading the configuration file is mainly to initialize and replace the AST data structure. This file is called as a parameter in subsequent logical processing.

Main entry processing logic

processTailwindFeatures.js under the src file is the entry file for all operation business logic. In addition, due to different references of PostCss versions, the core entry is this file. A series of logic operation codes are as follows:

return postcss([
      substituteTailwindAtRules(config, getProcessedPlugins()),
      evaluateTailwindFunctions(config),
      substituteVariantsAtRules(config, getProcessedPlugins()),
      substituteResponsiveAtRules(config),
      convertLayerAtRulesToControlComments(config),
      substituteScreenAtRules(config),
      substituteClassApplyAtRules(config, getProcessedPlugins, configChanged),
      applyImportantConfiguration(config),
      purgeUnusedStyles(config, configChanged),
    ]).process(css, { from: _.get(css, 'source.input.file') })
  1. substituteTailwindAtRules transform AST data operations
  2. evaluateTailwindFunctions topic configuration actions
  3. Substitutitvariantsatrules variable recursive rule operation
  4. substituteResponsiveAtRules general response rule logic operation
  5. convertLayerAtRulesToControlComments content editing description operation
  6. Substitutionscreen atrules style Screen rule action
  7. Substitutiteclassapplyatrules identifies @ apply logical processing
  8. Apply important configuration parameter whether to add logical processing
  9. purgeUnusedStyles deletes redundant code, adds a purgecss plug-in, reads the configuration, and deletes redundant unreferenced css style code
    10. It is better to export the Css file required for our project development

Core code

The following processTailwindFeatures.js in the src file contains such a line of code:

processedPlugins = processPlugins(
 [...corePlugins(config), ..._.get(config, 'plugins', [])],
  config
)

getProcessedPlugins = function () {
  return {
    // ...jumpUrl,
    base: cloneNodes(processedPlugins.base),
    components: cloneNodes(processedPlugins.components),
    utilities: cloneNodes(processedPlugins.utilities),
  }
}

The core function is to traverse some column configuration files under src/plugins:

'preflight',
  'container',
  'space',
  'divideWidth',
  'divideColor',
  'divideStyle',
  'divideOpacity',
  'accessibility',
  'appearance',
  'backgroundAttachment',
  'backgroundClip',
  'backgroundColor',
  'backgroundImage',
  'gradientColorStops',
  'backgroundOpacity',
  'backgroundPosition',
  'backgroundRepeat',
  'backgroundSize',
  'borderCollapse',
  'borderColor',
  'borderOpacity',
  'borderRadius',
  'borderStyle',
  'borderWidth',
  'boxSizing',
  'cursor',
  'display',
  'flexDirection',
  'flexWrap',
  'placeItems',
  'placeContent',
  'placeSelf',
  'alignItems',
  'alignContent',
  'alignSelf',
  'justifyItems',
  'justifyContent',
  .....

The generated code after traversing these configurations is the code in util - > processplugins.js:

handler({
      postcss,
      config: getConfigValue,
      theme: (path, defaultValue) => {
        const [pathRoot, ...subPaths] = _.toPath(path)
        const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)

        return transformThemeValue(pathRoot)(value)
      },
      corePlugins: (path) => {
        if (Array.isArray(config.corePlugins)) {
          return config.corePlugins.includes(path)
        }

        return getConfigValue(`corePlugins.${path}`, true)
      },
      variants: (path, defaultValue) => {
        if (Array.isArray(config.variants)) {
          return config.variants
        }

        return getConfigValue(`variants.${path}`, defaultValue)
      },
      e: escapeClassName,
      prefix: applyConfiguredPrefix,
      addUtilities: (utilities, options) => {
        const defaultOptions = { variants: [], respectPrefix: true, respectImportant: true }

        options = Array.isArray(options)
          ? Object.assign({}, defaultOptions, { variants: options })
          : _.defaults(options, defaultOptions)

        const styles = postcss.root({ nodes: parseStyles(utilities) })

        styles.walkRules((rule) => {
          if (options.respectPrefix && !isKeyframeRule(rule)) {
            rule.selector = applyConfiguredPrefix(rule.selector)
          }

          if (options.respectImportant && config.important) {
            rule.__tailwind = {
              ...rule.__tailwind,
              important: config.important,
            }
          }
        })

        pluginUtilities.push(
          wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'utilities')
        )
      },
      addComponents: (components, options) => {
        const defaultOptions = { variants: [], respectPrefix: true }

        options = Array.isArray(options)
          ? Object.assign({}, defaultOptions, { variants: options })
          : _.defaults(options, defaultOptions)

        const styles = postcss.root({ nodes: parseStyles(components) })

        styles.walkRules((rule) => {
          if (options.respectPrefix && !isKeyframeRule(rule)) {
            rule.selector = applyConfiguredPrefix(rule.selector)
          }
        })

        pluginComponents.push(
          wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'components')
        )
      },
      addBase: (baseStyles) => {
        pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base'))
      },
      addVariant: (name, generator, options = {}) => {
        pluginVariantGenerators[name] = generateVariantFunction(generator, options)
      },
    })
  })

Loop some columns and perform traversal logic operations according to the elements required for configuration, so as to generate base, components and utilities files. Generate raw data for data logical operations.

base core code

The core operation code of base is as follows:

export default function () {
  return function ({ addBase }) {
    const normalizeStyles = postcss.parse(
      fs.readFileSync(require.resolve('modern-normalize'), 'utf8')
    )
    const preflightStyles = postcss.parse(fs.readFileSync(`${__dirname}/css/preflight.css`, 'utf8'))
    addBase([...normalizeStyles.nodes, ...preflightStyles.nodes])
  }
}

Base refers to some basic style configurations, which refer to the basic style library of modern normalize as the basic style library of TaiWindCss. We can also customize the reference, such as the preflightStyles.css file in the code. If you need to extend the base basic library of TaiWindCss later, the writing method is similar to the reference of preflight.css code in the uploaded code.

utilities core code

Core operations of utilities

(1) We have made it clear how css works. For example, we have made it clear that the font style is left, right, center, etc. the code is as follows:

export default function () {
  return function ({ addUtilities, variants }) {
    addUtilities(
      {
        '.text-left': { 'text-align': 'left' },
        '.text-center': { 'text-align': 'center' },
        '.text-right': { 'text-align': 'right' },
        '.text-justify': { 'text-align': 'justify' },
      },
      variants('textAlign')
    )
  }
}

(2) We need to generate the style file according to the configuration file. For example, the value after z-index is configured. The code is as follows:

import createUtilityPlugin from '../util/createUtilityPlugin'

export default function () {
  return createUtilityPlugin('zIndex', [['z', ['zIndex']]])
}
components core code

The components template, as I understand it, provides a series of code logic similar to componentization. At present, TaiWindCss only does this for the layout style container. The main codes are as follows:

onst atRules = _(minWidths)
 .sortBy((minWidth) => parseInt(minWidth))
 .sortedUniq()
 .map((minWidth) => {
   return {
     [`@media (min-width: ${minWidth})`]: {
       '.container': {
         'max-width': minWidth,
         ...generatePaddingFor(minWidth),
       },
     },
   }
 })
 .value()

addComponents(
 [
   {
     '.container': Object.assign(
       { width: '100%' },
       theme('container.center', false) ? { marginRight: 'auto', marginLeft: 'auto' } : {},
       generatePaddingFor(0)
     ),
   },
   ...atRules,
 ],
 variants('container')
)
}

From the perspective of code logic, read the value of configured screens and traverse the style data of container s under different screens. Then classify and output the style library components. Of course, we can also extend this, such as:

addComponents({
 '.btn-blue': {
   backgroundColor: 'blue',
   color: 'white',
   padding: '.5rem 1rem',
   borderRadius: '.25rem',
 },
 '.btn-blue:hover': {
   backgroundColor: 'darkblue',
 },
})
//perhaps
addComponents(
 {
    '.btn-blue': {
      backgroundColor: 'blue',
    },
  },
  ['responsive', 'hover']
)

If such a plug-in configuration is added, these blocks are packaged into the components library.

The configuration of these three templates to produce CSS files is the main function of TaiWindCss

Plug in configuration

The function of the plug-in has developed an entry function for us to carry out configuration development. The method of the plug-in is as follows:

module.exports = {
  plugins: [
    plugin(function({ addUtilities, addComponents, e, prefix, config }) {
      // Add your custom styles here
    }),
  ]
}

A series of parameters passed are some core code applications in util - > processplugins.js.
Main core codes:
Read the plug-in configuration from the entry file SRC - > index.js:

processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),
  }

The getConfigFunction method reads the plug-ins in the tailwind.config.js configuration, performs data initialization, and then writes these plug-ins in. In principle, the method of JS writing is similar to which plug-in JS files in the plugins folder. It's just called in different ways.

For specific plug-ins, use the document to view the official website TaiWindCss plug-in usage documentation

Scaffolding

In the process of using TaiWindCss again, we need to provide an npm package. Therefore, TaiWindCss also integrates a user manual (scaffold) of TaiWindCss.
View package.json:

 "bin": {
    "tailwind": "lib/cli.js",
    "tailwindcss": "lib/cli.js"
  },

According to the directory, we know that it mainly provides three init (initialization), build (build to generate CSS) and help (help document)

The core code is SRC - > cli - > commands - > init.js, help.js, build.js

The specific code is very simple, that is, simple scaffold configuration js, which is not written here. If you are interested, you can see the source code.

summary

Through the analysis of TaiWindCss source code and reading the source code during this period, I learned some good writing methods of code and the design idea and concept of system architecture. The main learning achievements are as follows:

  • AST structure and conversion principle
  • Be familiar with postCss from 0 to 1. See the specific documents postCss official website
  • Development process of postCss plug-in
  • Design concept of TaiWindCss framework
  • Design and implementation of JS plug-in / configurable architecture

Through reading and viewing relevant codes during this period, I am really familiar with and can skillfully use the tool library of postCss. I think a very important harvest is the system design of codes.

In the past, our code design may be positive. From if else to else, we call when we call and pass parameters when we pass parameters. The design concept is top-down.

After reading the source code of TaiWindCss, I think it is very useful to traverse a series of files under plugins, transfer function methods in function parameters, where to obtain data, and finally get the original data. Such design is the core design concept of this architecture.

Such reverse operation logic, code classification is great, and data collection is done through a loop. Function calls pass parameters, and each different plug-in set is used on demand.

This design gives me access to it by reading the source code. So I feel that reading the source code brings some benefits, which can enrich and increase our development practice and development experience.

Later, I will provide a development practice of mini TaiWindCss.

reference material

Topics: Javascript css postcss