Skip to content
Waseem Sadiq edited this page Jul 22, 2022 · 23 revisions

Table of contents

Mason

We believe task runners should give a simple interface for you to do your magic and stay out of the way as much as possible.

While Mason comes with many helpers for common tasks, in the end you write pure Javascript code to perform your tasks depending on your needs through the mason.js file.

A build process is a sequence of tasks, usually automated, that you run each time that you want to deploy a new release of your application. Mason fits into this build process as a tool that should be run on your development CSS, and in turn will create compressed production assets. These are then uploaded to your production server or CDN.

Features

Mason includes the following features:

  1. CSS transformations using PostCSS
  2. Syntactically Awesome Style Sheets with SASS
  3. Automatic processing using Chokidar
  4. Live reload using Browsersync
  5. CSS framework with TailwindCSS

You don't need to use any of these features, if you don’t have a hard requirement, a strong preference, or just want our advice, we recommend giving our default setup a go. Yes, we did research this.

Installation

brew install joomlatools/mason/mason

or

npm i -g @joomlatools/mason

Copy the following into /joomlatools-pages/theme/mason.js

const mason = require('@joomlatools/mason-tools-v1');

async function css() {
  await mason.css.process(`css/input.css`, `css/output.css`, {});
}

async function sync() {
  mason.browserSync({
    watch: true,
    server: {
       baseDir: './joomlatools-pages/theme'
    },
    files: 'css/*.css',
  });
}

module.exports = {
  version: '1.0',
  tasks: {
    css,
    sync,
    watch: {
      path: ['.'],
      callback: async (path) => {
        if (path.endsWith('css/input.css')) {
          await css();
        }
      },
    },
  },
};

Configuration

The most important part is this line:

await mason.css.process(`css/input.css`, `css/output.css`, {});

mason.css.process will run your CSS file (css/input.css) through a PostCSS pipeline and generate the output.css file. By default it runs the file through (in this exact order):

  1. postcss-import
  2. Tailwind
  3. PostCSS Preset Env
  4. CSSnano (for minification)

It generates output.css and output.min.css

You can adjust the configuration for each step by passing the third options parameter. For example you can add a tailwind property with the contents of your tailwind.config.js file or set the stage option for postcss-preset-env to determine which CSS features to polyfill.

await mason.css.process(`css/input.css`, `css/output.css`, {
  tailwind: {
    content: [
      '../**/*.html.php',
    ],
    safelist: [
      'classes',
      'that',
      'you',
      'do',
      'not',
      'want',
      'to',
      'be',
      'purged',
    ],
    theme: {
      screens: {
        sm: '640px',
        md: '768px',
        lg: '1024px',
        xl: '1280px',
      },
      variants: {
        opacity: ['responsive', 'hover']
      },
      plugins: [
        // add your own tailwind plugins here
        require('@tailwindcss/typography'),  
      ],
    }
  },

  postcssPresetEnv: {
    stage: 2, // default is 2 (A Working Draft championed by a W3C Working Group.)
    autoprefixer: { cascade: true },
  }, 

  postcssImport: {
    root: process.cwd() //define the root where to resolve path (eg: place where node_modules are)
  },

  plugins: [
    // add your own postcss plugins here
    // they will run after postcss-import and  before other default plugins
    require('postcss-nested'), 
  ]
}); 

Running Mason

You can run mason from the command line. Run mason inspect to see what options are available.

$ mason inspect 

You can run the default tasks by just running mason. This will run CSS compilation and watch for changes.

$ mason
  ✔ Running css…
  ⠙ Watching for changes…

Upgrading Mason

You can upgrade Mason to the latest version with the following commands:

brew upgrade joomlatools/mason/mason

or

npm update -g @joomlatools/mason

Extending Mason

Mason is not limited to node modules that are included by default. You can install any node modules locally and require them - Mason will check your working directory (~/[path]/[to]/mason.js), then traverse the path tree all the way back up to root, so you can put the modules anywhere in that working dir, or any of its parents.

The directory lookup tree looks as follows:

`paths: [
    '~/[path]/[to]/node_modules',
    '~/[path]/node_modules',
    '~/node_modules',
    '/Users/node_modules',
    '/node_modules',
    '/snapshot/mason/node_modules'
  ]`

Mason will use local version of modules instead of default Mason modules, if installed. A good example would be installing a newer version of Tailwind to the local node_modules then is currently available in Mason.

PostCSS

PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.

By default Mason includes the TailwindCSS, PostCSS Preset Env which also includes the Autoprefixer, and CSSnano plugins. Additionally, Mason allows you to load any other PostCSS plugins that you need for your creative exploits.

Tailwind

Tailwind CSS is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override. Tailwind is more than a CSS framework, it's an engine for creating design systems and is a perfect wing-man for Pages.

Tailwind is written in PostCSS and configured in JavaScript, which means you have the full power of a real programming language at your fingertips, well as real javascript gets at least.

Importing Tailwind plugins

Using Tailwind plugins you can import any Tailwind plugins that are not included in a default Tailwind installation.

You can consume local files, node modules or web_modules... and potentially break your mason file. If you want to reduce your chances of eff-ing things up, see: Importing plugins.

Configuration

Because Tailwind is a framework for building bespoke user interfaces, it has been designed from the ground up with customisation in mind. Configuration can easily be added to your mason.js file to define any customisations.

tailwind: {
  content: [
    '../**/*.html.php',
  ],
  safelist: [
    'classes',
    'that',
    'you',
    'do',
    'not',
    'want',
    'to',
    'be',
    'purged',
  ],
  theme: {
    fontFamily: {
      display: ['Gilroy', 'sans-serif'],
      body: ['Graphik', 'sans-serif'],
    },
    extend: {
      colors: {
        cyan: '#9cdbff',
      },
      margin: {
        '96': '24rem',
        '128': '32rem',
      },
    }
  },
  variants: {
    opacity: ['responsive', 'hover']
  },
  plugins: [
    // add your own tailwind plugins here
    require('@tailwind/typography'),  
  ],
}

You can find all config options in the TailWindCSS docs. Every section of the config file is optional, so you only have to specify what you'd like to change. Any missing sections will fall back to Tailwind's default configuration.

PostCSS Preset Env

PostCSS Preset Env allows developers to use CSS variables in their regular CSS code. Depending on the config, it includes regular values alongside CSS variables as fallback for browsers that do not support CSS variables.

Code

.cta:hover {
  background-color: var(--color__cta--darker);
  color: var(--color__secondary);
}

Result

Configuration

We will only cover the used and most important config options, and those are stage, features and browsers. You can RTFM for more goodies.

Stage

This is similar to ECMAScript proposals used in Babel. The stages represent the steps that each feature (or proposal) in CSS must go through to become a part of the CSS standard. It consists of the following stages:

  • Stage 0: Aspirational - an idea or an early draft. Highly unstable and subject to change
  • Stage 1: Experimental - also highly unstable and subject to change, but the proposal is recognized by the members of W3C.
  • Stage 2: Allowable - also highly unstable and subject to change, but it’s actively being worked on.
  • Stage 3: Embraced - stable and subject to little change. This feature will likely become a standard.
  • Stage 4: Standardized - final working solution. supported by all major browsers.

By setting a stage option, we are choosing groups of CSS features that we can be used when writing CSS:

postcssPresetEnv: {
    stage: 2 // default is 2 (A Working Draft championed by a W3C Working Group.)
  }
}; 

Useful links for keeping track of which CSS features are in what stage:

Features

We can use the feature config to only enable specific CSS features, regardless of which stage option has been set. All feature variables can be found here: https://preset-env.cssdb.org/features

In the following example we use all stage 3+ features and we include nesting-rules, which is a stage 1 feature.

postcssPresetEnv: {
   stage: 3,
      features: {
        "nesting-rules": true,
      }
  }
}; 

Browsers

This is a standard Browserlist config that is being used by various tools and plugins like Autoprefixer.

In the following example, we are only targeting the last 2 versions of the browser.

postcssPresetEnv: {
   browsers: "last 2 versions",
   stage: 3,
      features: {
        "nesting-rules": true,
      }
}; 

Further reading

These several options are more than enough to get you started with using postcss-preset-env and write modern CSS syntax that transpiles down to widely-supported CSS syntax. For the full list of config options and features, you can check the following links

Troubleshooting

If when running $ mason you encounter the following error: Error: Expected an opening square bracket, add (and uncomment) the focus-within-pseudo-class feature above.

Autoprefixer

Autoprefixer is a PostCSS plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use.

Write your CSS rules without vendor prefixes (in fact, forget about them entirely):

::placeholder {
  color: gray;
}

.image {
  background-image: url(image@1x.png);
}
@media (min-resolution: 2dppx) {
  .image {
    background-image: url(image@2x.png);
  }
}

Autoprefixer will use the data based on current browser popularity and property support to apply prefixes for you. You can try the interactive demo of Autoprefixer.

::-moz-placeholder {
  color: gray;
}
:-ms-input-placeholder {
  color: gray;
}
::-ms-input-placeholder {
  color: gray;
}
::placeholder {
  color: gray;
}

.image {
  background-image: url(image@1x.png);
}
@media (-webkit-min-device-pixel-ratio: 2),
       (min-resolution: 2dppx) {
  .image {
    background-image: url(image@2x.png);
  }
}

Configuration

PostCSS Preset Env includes Autoprefixer and the browsers option will be passed to it automatically. Specifying the autoprefixer option enables passing additional options into autoprefixer.

postcssPresetEnv({
  autoprefixer: { grid: true }
})

Passing autoprefixer: false disables autoprefixer.

For configuration options: https://github.com/postcss/autoprefixer#options

CSSnano

CSSnano is a PostCSS plugin that minifies your perfectly formatted CSS and ruins it, eugh!, running it through many focused optimisations, to ensure that the final result is as small (and un-readable) as possible for your production environment.

Minification is the process of taking some code and using various methods to reduce its size on disk. Unlike techniques such as gzip, which preserve the original semantics of the CSS file, and are therefore lossless, minification is an inherently lossy process, where values can be replaced with smaller equivalents, or selectors merged together, for example.

before

/* normalize selectors */
h1::before, h1:before {
    /* reduce shorthand even further */
    margin: 10px 20px 10px 20px;
    /* reduce color values */
    color: #ff0000;
    /* remove duplicated properties */
    font-weight: 400;
    font-weight: 400;
    /* reduce position values */
    background-position: bottom right;
    /* normalize wrapping quotes */
    quotes: '«' "»";
    /* reduce gradient parameters */
    background: linear-gradient(to bottom, #ffe500 0%, #ffe500 50%, #121 50%, #121 100%);
    /* replace initial values */
    min-width: initial;
}
/* correct invalid placement */
@charset "utf-8";

after

@charset "utf-8";h1:before{margin:10px 20px;color:red;font-weight:400;background-position:100% 100%;quotes:"«" "»";background:linear-gradient(180deg,#ffe500,#ffe500 50%,#121 0,#121);min-width:0}

The semantics of this CSS have been kept the same, but the extraneous white space has been removed, the identifiers compressed, and unnecessary definitions purged from the stylesheet. This gives you a much smaller CSS stylesheet for production use.

But don’t just take our word for it; why not try out css-size, a module especially created to measure CSS size before & after minification.

File Size
Original (gzip) 325 B
Minified (gzip) 177 B
Difference 148 B
Percent 54.46%

Configuration

If you don't want to minify the input file you can pass {minify: false} in the configuration. This will only generate a single output.css file.

Optimisations

An optimisation is a module that performs a transform on some CSS code in order to reduce its size, or failing this, the final gzip size of the CSS. Each optimisation is performed by either a single module or a few modules working together.

Due to the nature of dividing cssnano's responsibilities across several modules, there will be some cases where using a transform standalone will not produce the most optimal output. For example, postcss-colormin will not trim white space inside colour functions as this is handled by postcss-normalize-whitespace.

You can find a complete list of optimisations here: https://cssnano.co/guides/optimisations

Presets

Presets are a way of loading cssnano with different features, depending on your use case. Now, instead of having to opt-out of advanced transformations, you can choose to opt-in instead.

cssnano: {
   preset: [
      'default', 
     {"discardComments": {"removeAll": true}}
   ]
}; 

For more info regarding presents please read: https://cssnano.co/guides/presets

Importing plugins

Using postcssImport you can import any Postcss plugin that is not included in Mason. You can consume local files, node modules or web_modules... and totally break things in the process. You have been warned!

To resolve the path of an @import rule, Mason will look into the root directory (by default process.cwd()), web_modules, node_modules or local modules. When importing a module, it will look for an index.css or file that's referenced in package.json in the style or main fields. You can also manually provide multiple paths where Mason should look.

/* can consume `node_modules`, `web_modules` or local modules */
@import "cssrecipes-defaults"; /* == @import "../node_modules/cssrecipes-defaults/index.css"; */
@import "normalize.css"; /* == @import "../node_modules/normalize.css/normalize.css"; */

@import "foo.css"; /* relative to css/ according to `from` option above */

@import "bar.css" (min-width: 25em);

result

/* ... content of ../node_modules/cssrecipes-defaults/index.css */
/* ... content of ../node_modules/normalize.css/normalize.css */

/* ... content of css/foo.css */

@media (min-width: 25em) {
/* ... content of css/bar.css */
}

body {
  background: black;
}

body {
  background: black;
}

Sass

Sass is an extension for CSS that adds various features to the language like variables, mixins, nested rules and more. Sass is fully backwards compatible with CSS and is a mature technology that has been in development for over 13 years.

Configuration

Mason comes bundled with dart-sass, a Sass compiler for JavaScript.

SASS with PostCSS

You can compile your scss to css as a first step before processing it further with PostCSS. Here is how you would use it in your mason.js file:

const mason = require('@joomlatools/mason-tools-v1');

async function postcss() {
  await mason.css.process('scss', 'css', {
    sass: true,
  });
}

module.exports = {
  version: '1.0',
  tasks: {
    compile,
    watch: {
      path: ['.'],
      callback: async (path) => {
        if (path.endsWith('.scss')) {
          await postcss();
        }
      },
    },
  },
};

SASS only

You can also use Mason to compile your scss to css without further processing by PostCSS. Here is how you would use it in your mason.js file:

const mason = require('@joomlatools/mason-tools-v1');

async function sass() {
  await mason.sass.compile(`input.scss`, `output.css`, {});
  await mason.sass.minify(`input.scss`, `output.min.css`, {});
}

module.exports = {
  version: '1.0',
  tasks: {
    sass,
    watch: {
      path: ['.'],
      callback: async (path) => {
        if (path.endsWith('input.css')) {
          await sass();
        }
      },
    },
  },
};

This compiles input.scss into output.css in the same folder. It also runs the file through autoprefixer for full browser compatibility.

You can use the third parameter to pass options to the Sass compiler. For example:

await mason.sass.compile(`input.scss`, `output.css`, {
    includePaths: [ 'lib/' ], // Include files from the lib/ folder without a prefix
});

For configuration options: https://sass-lang.com/documentation/js-api

Browsersync

Browsersync keeps multiple browsers & devices in sync when building websites, and keeps you as-lazy-as-a-toad. It makes your tweaking and testing faster by synchronising file changes and interactions across multiple devices.

Features

  • Live reloading: This is probably the most important feature of BrowserSync. change your code and the page is auto-reloaded. Live reloading works across many browsers and devices.

  • Interaction synchronization: Means that all your actions are mirrored across every browser. This little feature is useful for testing, especially, when testing across many devices. You can also customise which actions are mirrored across browsers.

  • Simulate slower connections: I believe you are expecting users from all over the world, and some countries aren't fortunate enough to have a fast internet connection.

  • URL history: BrowserSync logs all browsing history so you can push a test URL to all devices.

How does it work?

BrowserSync creates a small server, but if you already have a server set-up, BrowserSync can hook into that server and act as a proxy.

Next, Browsersync works by injecting an asynchronous script tag (<script async>...</script>) right after the <body> tag during initial request. In order for this to work properly the <body> tag must be present. Alternatively you can provide a custom rule for the snippet using snippetOptions.

This file makes use of WebSockets, don't worry everything is safely covered and toddler-proof secured, to communicate between the server and the client to watch changes to your code or browser action. As soon as BrowserSync detects an action it performs a page reload.

Configuration

By default browsersync is configured to watch for css/*.css in the /joomlatools-pages/theme directory. BrowserSync has more options. Take a look at some of the other options: https://www.browsersync.io/docs/options/

Chokidar

Chokidar is a fast file watcher for node.js. You give it a bunch of files, it watches them for changes and notifies you every time an old file is edited; or a new file is created.

How does it work?

Chokidar relies heavily on the Node.js core fs module, but when using fs.watch and fs.watchFile for watching, it normalizes the events it receives, often checking for truth by getting file stats and/or dir contents.

Be advised that chokidar will initiate watchers recursively for everything within scope of the paths that have been specified, so be judicious about not wasting system resources by watching much more than is needed.

Configuration

By default chokidar is configured to watch for . in the /joomlatools-pages/ directory. It skips dotfiles (files starting with .), the callback property accepts a function that has the changed file as the first parameter. You can do custom processing on the changed file in the callback function:

watch: {
      path: ['.'],
      callback: async (path) => {
        if (path.endsWith('input.css')) {
          await css();
        }

        // You can do custom processing on the changed file here
      },
    },

Examples

SCSS & PostCSS

This example is created to run from the theme directory in Pages. It uses SCSS and PostCSS with a couple of PostCSS plugins. The theme/scss/styles.scss compiles to theme/css/styles.css as well as to theme/css/styles.min.css a minified version using CSSNano.

mason.js

const mason = require('@joomlatools/mason-tools-v1');

async function css() {
  await mason.css.process('scss', 'css', {
    sass: true,
    postcssPresetEnv: {
        stage: 3,
        features: {
            "nesting-rules": true
        },
        autoprefixer: { cascade: true, grid: true },
    },
    cssnano: {
       preset: [
          'default',
         {"discardComments": {"removeAll": true}}
       ]
    }
  });
}

async function sync() {
  mason.browserSync({
    watch: true,
    server: {
        baseDir: './joomlatools-pages/theme',
    },
    files: 'css/*.css',
  });
}

module.exports = {
  version: '1.0',
  tasks: {
    css,
    sync,
    watch: {
      path: ['.'],
      callback: async (path) => {
        if (path.endsWith('.scss')) {
          await css();
        }
      },
    },
  },
};

Source

@tailwind base; 

/* Preflight will be injected here */
@tailwind components;
@tailwind utilities;

/* This is autoprefixed for different browsers */
.foo {
  background-color: var(--color__cta--darker);
  user-select: none;
}

/* Nested rules are expanded */
.bar {
  color: red;
  & > .baz {
    color: blue;
  }
}

Step 1: After SASS compilation

The following is the output is the result after sass compilation

@tailwind base;

/* Preflight will be injected here */
@tailwind components;
@tailwind utilities;

/* This is autoprefixed for different browsers */
.foo {
  background-color: var(--color__cta--darker);
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
}
/* Nested rules are expanded */
.bar {
  color: red;
}
.bar > .baz {
  color: blue;
}

Step 2: After PostCSS processing

The resulting output is 60kb of css, too much to provide here. To check the result open following file: themes/css/styles.css

Clone this wiki locally