red-perfume / red-perfume-task-runner

Experimental CSS Atomizer
https://red-perfume.github.io
MIT License
26 stars 4 forks source link

Red Perfume

Build Status Coverage Badge Lint Coverage: 100% JSDoc Coverage: 100% Code of Conduct: No Ideologies MIT Licensed

Running the alpha locally

  1. Install Node/npm (lowest supported version not yet known, presumed to work with 12+)
  2. npm install --save-dev red-perfume
  3. Follow API instructions below
  4. Leave feedback or report bugs

Feedback

Leave feedback as an issue or a response on Twitter.

Star and Watch this repo for updates.

Or follow me on Twitter if that's easier.

Contributing

All work for this project is documented and organized in this GitHub Project. Look for "Help Wanted". It is prioritized, top to bottom.

Experimental CSS Atomizer (WIP)

This is a library for a build tool that helps to drastically reduce the total amount of CSS that is shipped for your project. Facebook adopted this atomized CSS approach and it reduced their homepage CSS by 80%. Twitter also atomizes their CSS.

With red-perfume you write your CSS however you like (semantic class names, BEM, utility classes, atomic, whatever). Then reference them in your HTML normally. Then red-perfume atomizes the styling into atomic classes, and replaces the references to them:

Example:

/* Before */
.cow,
.cat {
    font-size: 12px;
    padding: 8px;
}
.dog {
    font-size: 12px;
    background: #F00;
    padding: 8px;
}
/* After */
.rp__font-size__--COLON12px {
  font-size: 12px;
}
.rp__padding__--COLON8px {
  padding: 8px;
}
.rp__background__--COLON__--OCTOTHORPF00 {
  background: #F00;
}
<!-- Before -->
<!DOCTYPE html>
<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <p class="cool cow moo">
      Hi there!
    </p>
    <!--
      <span class="dog">comments are skipped</span>
    -->
    <h1 class="cool cat nice wow">
      Meow
    </h1>
    <h2 class="dog">
      Woof
    </h2>
  </body>
</html>
<!-- After -->
<!DOCTYPE html>
<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <p class="cool moo rp__font-size__--COLON12px rp__padding__--COLON8px">
      Hi there!
    </p>
    <!--
      <span class="dog">comments are skipped</span>
    -->
    <h1 class="cool nice wow rp__font-size__--COLON12px rp__padding__--COLON8px">
      Meow
    </h1>
    <h2 class="rp__font-size__--COLON12px rp__background__--COLON__--OCTOTHORPF00 rp__padding__--COLON8px">
      Woof
    </h2>
  </body>
</html>

This output isn't as pretty to read, but it's a build step, not your source code, so it doesn't really matter. Note: The class names can be uglified as well (.rp__0, .rp__1, etc.).

The alpha version of red-perfume already works for simple CSS, like the above example. However, more work is being done to allow any CSS file to be passed in, no matter how weird or complex. Look at the issues page to see what work is left to be done and how you can help!

Uglified Example:

/* Uglified */
.rp__0 {
  font-size: 12px;
}
.rp__1 {
  padding: 8px;
}
.rp__2 {
  background: #F00;
}
<!-- Uglified -->
<!DOCTYPE html>
<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <p class="cool moo rp__0 rp__1">
      Hi there!
    </p>
    <!--
      <span class="dog">comments are skipped</span>
    -->
    <h1 class="cool nice wow rp__0 rp__1">
      Meow
    </h1>
    <h2 class="rp__0 rp__2 rp__1">
      Woof
    </h2>
  </body>
</html>

API (subject to change before v1.0.0)

API Example

You can point to files or pass strings in directly. Tasks are sequential, the output of one can feed into the input of the next. You can output to file or use lifecycle callback hooks (documented in next section).

const redPerfume = require('red-perfume');

redPerfume.atomize({
  tasks: [
    {
      uglify: false,
      styles: {
        in: [
          './styles/file.css',
          './styles/style.css'
        ],
        // The two above files will be concatenated and atomized then output to this file
        out: './dist/styles/styles.css'
      },
      // The output markup will be a copy of the input but modified to have the class names replaced to match the new atomized styles in this task
      markup: [
        {
          in: './index.html',
          out: './dist/index.html'
        },
        {
          in: './contact.html',
          out: './dist/contact.html'
        }
      ],
      scripts: {
        // Design of this JSON file will change before v1.0.0.
        // The point is to allow your JavaScript to reference a map of the original class name (key) and the atomized classes produced from it (value)
        out: './dist/atomic-styles.json'
      }
    },
    {
      uglify: true,
      styles: {
        // Instead of, or in addition to, using input files, you can also provide a string directly
        data: '.example { padding: 10px; margin: 10px; }',
        // There are many lifecycle hooks that can be used as callbacks at specific points in execution
        // Useful for 3rd party plugins. Fully documented below.
        hooks: {
          afterOutput: function (options, { task, inputCss, atomizedCss, classMap, styleErrors }) {
            console.log({ options, task, inputCss, atomizedCss, classMap, styleErrors });
          }
        }
      },
      markup: [
        {
          data: '<!DOCTYPE html><html><body><div class="example"></div></body></html>',
          hooks: {
            afterOutput: function (options, { task, subTask, classMap, inputHtml, atomizedHtml, markupErrors }) {
              console.log({ options, task, subTask, classMap, inputHtml, atomizedHtml, markupErrors });
            }
          }
        }
      ],
      scripts: {
        hooks: {
          afterOutput: function (options, { task, classMap, scriptErrors }) {
            console.log({ options, task, classMap, scriptErrors });
          }
        }
      }
    }
  ]
});

API Implementation Status: ALPHA

The documented API is fully implemented and tested. Though there are many edge cases that have not been covered yet (see: issues), and some more advanced parts of the features yet to be implemented (also: issues).

API Documentation

Top level/global settings.

redPerfume.atomize({ verbose, customLogger, tasks, hooks });
Key Type Allowed Default Description
verbose Boolean true, false true If true, consoles out helpful warnings and errors using customLogger or console.error.
customLogger Function Any function console.error Advanced - You can pass in your own custom function to log errors/warnings to. When called the function will receive a message string for the first argument and sometimes an error object for the second argument. This can be useful in scenarios like adding in custom wrappers or colors in a command line/terminal. This function may be called multiple times before all tasks complete. Only called if verbose is true. If not provided and verbose is true, normal console.error messages are called.
tasks Array Array of objects undefined An array of task objects. Each represents the settings for an atomization task to be performed.
hooks Object Array of methods {} Lifecycle callback hooks (documented in next section)

Tasks API:

Tasks are an array of objects with the following API.

redPerfume.atomize({ tasks: [{ uglify, styles, markup, scripts, hooks }] });
Key Type Default Description
uglify Boolean false If false the atomized classes, and all references to them, are long (.rp__padding__--COLOR12px). If true they are short (.rp__b5p).
styles Object undefined CSS settings. API below
markup Array undefined HTML settings. An array of objects with their API defined below
scripts Object undefined JS settings. API below
hooks Object {} Lifecycle callback hooks (documented in next section)

Styles Task API:

redPerfume.atomize({ tasks: [{ styles: { in, data, out, hooks } }] });
Key Type Default Description
in Array undefined An array of strings to valid paths for CSS files. All files will remain untouched. A new atomized string is produced for out and/or hooks.
data String undefined A string of CSS to be atomized. Files provived via in are concatenated with data at the end, then atomized and sent to out and/or hooks.
out String undefined A string file path output. If file exists it will be overwritten with the atomized styles from in and/or data
hooks Object {} Lifecycle callback hooks (documented in next section)

Markup Task API:

redPerfume.atomize({ tasks: [{ markup: [{ in, data, out, hooks }] }] });
Key Type Default Description
in String undefined Path to an HTML file to be processed.
data String undefined A string of markup to be processed. This is appended to the end of the in file contents if both are provided.
out String undefined Path where the modified version of the in file and/or data will be stored. If file already exists, it will be overwritten.
hooks Object {} Lifecycle callback hooks (documented in next section)

Scripts Task API:

redPerfume.atomize({ tasks: [{ scripts: { out, hooks } }] });
Key Type Default Description
out String undefined Path where a JSON object (classMap) will be stored. The object contains keys (selectors) and values (array of strings of atomized class names). If file already exists, it will be overwritten. Output subject to change before v1.0.0.
hooks Object {} Lifecycle callback hooks (documented in next section)

Lifecycle Callback Hooks Example

All the hooks are shown below. Most users will only use the afterOutput hooks as a simple callback to know when something has finished. Perhaps to pass along the atomized string to another plugin (to minify, or generate a report or something). These hooks are primarily for those writing 3rd party plugins. Or for existing 3rd party libraries to add documentation to their repo on how to combine them with Red Perfume.

redPerfume.atomize({
  hooks: {
    beforeValidation: function (options) {},
    afterValidation:  function (options) {},
    beforeTasks:      function (options) {},
    afterTasks:       function (options, [{ task, inputCss, atomizedCss, classMap, allInputMarkup, allAtomizedMarkup, styleErrors, markupErrors, scriptErrors }]) {}
  },
  tasks: [
    {
      hooks: {
        beforeTask: function (options, { task }) {},
        afterTask:  function (options, { task, inputCss, atomizedCss, classMap, allInputMarkup, allAtomizedMarkup, styleErrors, markupErrors, scriptErrors }) {}
      },
      styles: {
        hooks: {
          beforeRead:     function (options, { task }) {},
          afterRead:      function (options, { task, inputCss, styleErrors }) {},
          afterProcessed: function (options, { task, inputCss, atomizedCss, classMap, styleErrors }) {},
          afterOutput:    function (options, { task, inputCss, atomizedCss, classMap, styleErrors }) {}
        }
      },
      markup: [
        {
          hooks: {
            beforeRead:     function (options, { task, subTask, classMap }) {},
            afterRead:      function (options, { task, subTask, classMap, inputHtml, markupErrors }) {},
            afterProcessed: function (options, { task, subTask, classMap, inputHtml, atomizedHtml, markupErrors }) {},
            afterOutput:    function (options, { task, subTask, classMap, inputHtml, atomizedHtml, markupErrors }) {}
          }
        }
      ],
      scripts: {
        hooks: {
          beforeOutput: function (options, { task, classMap }) {},
          afterOutput:  function (options, { task, classMap, scriptErrors }) {}
        }
      }
    }
  ]
});

Hook descriptions:

These are always called and in the same order. For example, afterOutput will still be called even if the out setting was undefined, the output is skipped but the hook is still called if provided.

Hook argument definitions:

The arguments defined here will always be the same, in every hook, with the excpection that options will be mutated during validation. However, due to the nature of JavaScript object referencing, it is very possible for 3rd party plugins you use to mutate these object values. This is intentional and allowed. Though we would encourage 3rd party libraries to just add their settings to the options object rather than mutate the data used by Red Perfume when possible, since the validation does not remove undocumented keys.

Argument Type Description
options object The options the user originally passed in (beforeValidation) or a modifed version with all API defaults in place (any point from afterValidation and on)
task object The current task being processed. Looks like { styles, markup, scripts, hooks }, see API above for more info.
inputCss string All of the CSS input files and data combined, but not atomized.
inputHtml string The HTML from the in file and data combined, but not atomized.
atomizedCss string The inputCss string after it is atomized.
atomizedHtml string The atomized HTML for a specific markup subtask.
classMap object An object where the keys are the original class names and the values are the atomized class names made from the original CSS rule. This is the same map we output in the scripts.out. IMPORTANT: How the keys are written (with or without a .) and how the values are stored (as an array or string) are subject to change before v1.0.0.
subTask object The current markup subTask being processed. Looks like { in, out, data, hooks }, see API above for more info.
allInputMarkup array Array of all input strings of HTML for each markup subtask.
allAtomizedMarkup array Array of atomized strings of HTML for each markup subTask.
styleErrors array An array of errors from attempting to read/write/parse/stringify style files.
markupErrors array An array of errors from attempting to read/write/parse/stringify markup files.
scriptErrors array An array of errors from attempting to write JSON files to disk.

The order hooks are called in:

  1. Global: beforeValidation
  2. Global: afterValidation
  3. Global: beforeTasks
  4. Task 1: beforeTask
  5. Task 1 - Styles: beforeRead
  6. Task 1 - Styles: afterRead
  7. Task 1 - Styles: afterProcessed
  8. Task 1 - Styles: afterOutput
  9. Task 1 - Markup: beforeRead
  10. Task 1 - Markup: afterRead
  11. Task 1 - Markup: afterProcessed
  12. Task 1 - Markup: afterOutput
  13. Task 1 - Scripts: beforeOutput
  14. Task 1 - Scripts: afterOutput
  15. Task 1: afterTask
  16. Task 2: beforeTask
  17. Task 2 - Styles: beforeRead
  18. Task 2 - Styles: afterRead
  19. Task 2 - Styles: afterProcessed
  20. Task 2 - Styles: afterOutput
  21. Task 2 - Markup: beforeRead
  22. Task 2 - Markup: afterRead
  23. Task 2 - Markup: afterProcessed
  24. Task 2 - Markup: afterOutput
  25. Task 2 - Scripts: beforeOutput
  26. Task 2 - Scripts: afterOutput
  27. Task 2: afterTask
  28. Global: afterTasks

Running locally to see the proof of concept or contribute

  1. Install Node.js & npm
  2. Download or fork or clone the repo
  3. npm install
  4. npm run manual-test

All work for this project is documented and organized in this GitHub Project. Look for "Help Wanted". It is prioritized, top to bottom.

Why is it called "Red Perfume"

This library takes in any CSS and breaks it down to pure Atomic CSS. This is a process called "CSS Atomization", and libraries that do this process are called "CSS Atomizers".

Outside of our industry jargon, "Atomizer" already exists as a word.

Atomizer (NOUN)

  1. A device for emitting water, perfume, or other liquids as a fine spray.

- Oxford English Dictionary

Though actual atomizers themselves have no consistent size, design, color, or shape. So there is no iconic image that represents them.

Example of several atomizers of differnt size, shape, color and design

And though perfume bottles can also come in many shapes, colors, sizes and designs, they are still recognizable as perfume bottles.