Taking PatternLab modularity to the next level

Space Shuttle Atlantis takes flight on the STS-27 mission in December 2, 1988 - Nasa Image Archive

As we have learned from previous instalments, various other sources online and our work with our clients, a Design System can mean many things and what it is in practical terms really boils down to what you’re trying to achieve.

Of the many tools available now, PatternLab is one that would provide a good flexible infrastructure and solves a lot of the problems related to the fuzzy definition of a Design System.

I’ve decided to put this to a test and see what can I achieve with it, by leveraging on its internal modularity system, and in the process, trying to eliminate a lot of the initial setup work that is usually needed.

Before we move on, let’s have a quick look at what is PatternLab allowing us to do.

TL;DR: PatternLab modularity system

Since Version 2, PatternLab has improved massively from a user standpoint. It’s been one of the first tools to help us create a pattern library and over time the community behind it grew enough to be able to provide enough feedback and use cases.

PatternLab now provides what it’s called the Edition, which is basically a layered approach to the development of whatever you want to do with it: the Core, the PatternEngine (the templating engine used for the templates), the StyleguideKit (essentially the UI theme for PatternLab itself), and the StarterKit.

[!Pattern Lab Edition diagram]

I’ve decided to focus my attention on the StarterKits and how to make the most out of them. This is what I’m going to cover:

  • understanding StarterKits,
  • dependencies and requirements,
  • validating the development workflow.

DISCLAIMER: Before we start, keep in mind that this whole article gravitates around the Node version of PatternLab. Even then, it should be quite straightforward to port the most of it over to the PHP version, but I haven’t been using it for quite a while now, so I don’t know what’s the amount of effort required.

Understanding StarterKits

As we saw above, StarterKits provide the highest level of abstraction over PatternLab and separate quite nicely the majority of the data you would normally have to deal with on a daily basis within it.

StarterKits are what the name suggests: starter kits. To put it in other words, it provides some sort of scaffolding for some if not all the files that live inside the source/ folder.

There are some already-made examples you can start from and have a look how they’re organised. If you’ve used PatternLab before, you should be quite familiar with the [starterkit-mustache-demo](https://github.com/pattern-lab/starterkit-mustache-demo) that contains all the templates and files that are used for the demo website of PatternLab.

PatternLab provides a post-install script that will try to detect any starterkits you have installed via npm. This script is responsible for copying anything that sits under the starterkit dist/ folder over to the source/ folder in PatternLab.

Since we like to have npm packages namespaced, I’ve found that the post-install script won’t work as expected (since it looks for packages starting with starterkit-*) so my best option is to use the CLI command manually, as such:

$ gulp patternlab:loadstarterkit --kit=@buildit/starterkit-global-rules

Loading engines from the core...
Loading engines from the edition or test directory...

mustache: good to go

Done loading engines.

====[ Pattern Lab / Node - v2.9.3 ]====

[13:53:21] Using gulpfile ~/repos/patternlab-base-toolkit/gulpfile.js
[13:53:21] Starting 'patternlab:loadstarterkit'...
Attempting to load starterkit from /Users/peach/repos/patternlab-base-toolkit/node_modules/@buildit/starterkit-global-rules/dist
Overwriting contents of ./source/ during starterkit load.
starterkit @buildit/starterkit-global-rules loaded successfully.
[13:53:21] Finished 'patternlab:loadstarterkit' after 16 ms

You will notice from the output of the command that the content of source/ is getting overwritten. I will come back to this later on.

Ideally, this process should be done only once, as it means that any configuration or improvement done from that will be overwritten and your efforts lost (well, unless you’ve committed your stuff, of course)!

Dependencies and requirements

As usual, with most projects, understanding the main problem and the possible solution(s) is the first and most important step to tackle.

The basic idea I want to achieve is the following:

  1. move all the design system related code into one or more StarterKits,
  2. make sure that PatternLab will work smoothly with them.

The first part is about understanding how to architecture the StarterKits, while the second part, is about enhancing PatternLab with the right tools so that little is left to the developer to deal with, and can concentrate on the important stuff.

Once we will cover all the basic technical aspects, I will dig into the workflow and what that means in practical terms.

Making StarterKits useful

For the purpose of what I’m doing, I’ve decided to leave only templates (Mustache files in my case) in the StarterKits. In my case I will go with at least 3 different StarterKits, very much based on the following architecture:

[!image]
The structure of a Design System, broken down into Building Blocks, UI Patterns, and Rules. — credit goes to UXPin.

This structure is inspired by a UXPin article, and these could be just the first few I’m building:

I want to be able to have different StarterKits that I can mix and match according to what I need to build.

The question that came out as soon as I decided to move into this code organisation is where the whole CSS dependencies should live and how they should be tied together.

In web projects, this is quite important since you might need to create a SASS/CSS library as you go along and build your design system, especially when it comes to the UI Patterns section.

It’s also very important to understand the different CSS layers that are used by PatternLab:

  • /source/styleguide/css/: here lives everything that is usually styling the actual chrome UI of PatternLab (e.g. what would be provided by a StyleGuideKit I mentioned above)
  • /source/css/pattern-scaffolding.css: this contains all the styles that are used to display the patterns within PatternLab. These are the UI Components (if you want) for PatternLab. A very classical example are swatches for colour palettes.
  • /source/css/style.css: this is the actual stylesheet for your own pattern library, that is what you would normally use on a daily basis in your own project. This shouldn’t contain anything that is used specifically in PatternLab.
    The simplest solution is to keep all these stylesheets within the core PatternLab project, and that probably should work fine in most instances.

I’ve instead decided to take it a step further and see if I could decouple these dependencies and provide another layer of abstraction for a better composition of the various StarterKits.

After a lot of tinkering around, the final architecture solution I decided to create is the following:

[!The final architecture organisation of PatternLab in conjunction with 3 different StarterKits]

As you can see, each StarterKit would bring along their own scaffolding CSS., while the “foundation” library (not to be confused with the Zurb Foundation Lib), is composed of basic styles that would be universally available across all the StarterKits. The UI Patterns StarterKit instead would benefit from a more extensible SASS library on top of that. This separation will also provide a clean way to create and export your own SASS library for public distribution and consumption.

How to make PatternLab truly interoperable

PatternLab is fundamentally barebone when it comes to features and automation, for a good reason, as it is its intended scope.

Here are the few changes I had to make in order to achieve what I wanted:

  1. introduce SASS compilation (I’m using Gulp for this)
  2. introduce aggregation of all the scaffolding files, to avoid collisions
  3. add style linting, because, why not?
  4. clean up a bunch of files that are not needed when using StarterKits

Before we even get into any of this, be sure to grab a copy of [gulpfile.js](https://github.com/pattern-lab/edition-node-gulp/blob/master/gulpfile.js) that is available from the edition-node-gulp. Gulp should already be in the dependencies, so be sure to run the npm install before you move on.

Adding SASS compilation

For the SASS compilation there are some instructions available somewhere, probably not particularly up-to-date, and the solution I’m going to present here is very much based on that and it’s also the simplest I could achieve without having to rewrite half of the gulpfile.js, I’m sure something better can be achieved.

Let’s add gulp-sass to our dependencies:

$ npm install -s gulp-sass

If you’re using npm v5+ you don’t need the -s option as it would save it automatically.

At the top of the file, we then need to require the lib, as per usual, in order to use it:

// gulpfile.js
const sass = require('gulp-sass');

Before doing that, let’s add a couple of paths into our configuration file patternlab-config.json:

// patternlab-config.json
{
  "paths": {
    "source": {
      // ...
      "sass": "./source/sass/"
    },
    // ...
  },
  // ...
}

Now the current implementation of the gulpfile.js is just copying /source/css/style.css into /public/css/ using the task pl-copy:css. I won’t be touching this task, and instead add a task pl-sass for compiling the SASS files that would be residing into /source/sass/ with style.scss being the entry point, which means we would then need to remove the style.css file from Git:

// gulpfile.js
gulp.task('pl-sass', function () {
  const sassOptions = {
    includePaths: [paths().source.sass]
  };
  return gulp.src(path.resolve(paths().source.sass, '**/*.scss'))
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(gulp.dest(path.resolve(paths().source.css)))
})

Add the SASS files to the list of watched files:

function watch() {
  const watchers = [
    // ...
    {
      name: 'SASS',
      paths: [normalizePath(paths().source.sass, '**', '*.scss')],
      config: { awaitWriteFinish: true },
      tasks: gulp.series('pl-sass')
    },
    // ...
  ];
  // ...
}

And then add it to the compilation steps in pl-assets:

// gulpfile.js
gulp.task('pl-assets', gulp.series(
    'pl-copy:js',
    'pl-copy:img',
    'pl-copy:favicon',
    'pl-copy:font',
    'pl-sass',
    'pl-copy:css',
    'pl-copy:styleguide',
    'pl-copy:styleguide-css'
  ),
  function (done) {
    done();
  }
);

In the task, we are also adding a call to done() using a callback function to notify the end of the process in case anything is needed to be triggered afterwards (e.g. browser reload) and to handle errors.

We are left with the removal of the compiled CSS file from Git so that every change won’t bother us into committing anything:

// .gitignore
/source/css/style.css

And then the actual removal of the file:

$ git rm --cached source/css/style.css

Don’t forget to commit the changes after this:

$ git add source/css/style.css
$ git commit "removed style.css from tracking"

Avoiding pattern scaffolding clashes

The core idea of pattern scaffolding in the scenario I’ve envisioned here is that you can create the styles used specifically by each set of templates. The problem with this approach is that there’s no immediate way of making it work out of the box, so some further tweaking of the gulpfile.js will be required.

For this we would need to set some basic rules since we won’t be touching the way files are copied over from the StarterKit: each file will have to be named differently, a simple numbered prefix could suffice, as long as pattern-scaffolding is still present in the name. For instance, I have:

  • 00-pattern-scaffolding-rules.css coming from the Rules StarterKit,
  • 01-pattern-scaffolding-base.css coming from the Building Blocks StarterKit,
  • 02-pattern-scaffolding-components.css from the UI Components StarterKit.

All these files will be concatenated in the expected pattern-scaffolding.css file that PatternLab is expecting.

For this to work we would then use gulp-concat for concatenating all the files together:

$ npm install -s gulp-concat

Now, back on the gulpfile.js:

// gulpfile.js
const concat = require('gulp-concat');

and the task will be something along the lines of:

// gulpfile.js
// CSS Copy base style
gulp.task('pl-copy:css:style', function () {
  return gulp.src(normalizePath(paths().source.css) + '/style.css')
    .pipe(gulp.dest(normalizePath(paths().public.css)))
    .pipe(browserSync.stream());
});
// CSS Concat and copy pattern-scaffolding
gulp.task('pl-copy:css:scaffolding', function () {
  return gulp.src(normalizePath(paths().source.css) + '/*pattern-scaffolding*.css')
    .pipe(concat('pattern-scaffolding.css'))
    .pipe(gulp.dest(normalizePath(paths().public.css)))
    .pipe(browserSync.stream());
});

As you can see I’ve modified the original pl-copy:css and renamed into pl-copy:css:style to target style.css specifically, otherwise it would grab everything in the directory.

Lastly, let’s add the tasks to the build process:

// gulpfile.js
gulp.task('pl-assets', gulp.series(
    'pl-copy:js',
    'pl-copy:img',
    'pl-copy:favicon',
    'pl-copy:font',
    'pl-sass',
    'pl-copy:css:style',
    'pl-copy:css:scaffolding',
    'pl-copy:styleguide',
    'pl-copy:styleguide-css'
  ),
  function (done) {
    done();
  }
);

and update the watcher so they’ll be triggered during the update:

// gulpfile.js
function watch() {
  const watchers = [
    // ...
    {
      name: 'CSS',
      paths: [normalizePath(paths().source.css, '**', '*.css')],
      config: { awaitWriteFinish: true },
      tasks: gulp.series(['pl-copy:css:style', 'pl-copy:css:scaffolding'], reloadCSS)
    },
    // ...
  ],
  // ...
}

As before, we can remove the pattern-scaffolding.css file from Git.

SASS libraries and last few bits

I won’t be digging too much into the technical details of this, but for the sake of completeness, it’s important to remember that the SASS libraries, the foundation and the UI components one, would live outside of the repository as external npm dependencies.

The foundation library would provide the basic styles (you can think of them as Atoms, from an Atomic Design point of view), which would make them easy to be applied across all the different StarterKits without too many efforts.

The UI Components library is instead a specific requirement of the UI Patterns StarterKit, which contains all the modules we would need to build any application.

This approach would grant us maximum reusability and composability, especially if we think that the whole purpose of this work is to allow us to create Design Systems that would be almost ready to be used by our clients or projects with the minimum amount of effort.

For this same reason, I’ve also decided to use Eyeglass, a simple npm module that helps load SASS dependencies without having to specify their full path in the gulpfile.js or in the @import statement in your SASS files. On the other hand, it won’t be exposing the SASS files into PatternLab, but I think this is a tradeoff that would be perfect as it almost simulates a real-case scenario.

The result is that in the main PatternLab SASS file, /source/sass/style.scss will contain essentially only:

@include "foundation";
@include "ui-patterns";

Validating the workflow and final notes

At this point, you might be asking yourself if this whole thing is actually sustainable and works as expected, so I think it’s worth taking the whole system for a spin and show how the development and maintenance cycle is supposed to work.

Developing the system

In order to get to a usable situation, there’s some work that needs to be done, it’s not much if you (now) know how to achieve it, this translates in the following tasks:

  • cleanup of PatternLab (i.e. core + engine + StyleguideKit)
  • development of the initial state of the StarterKits (2+, depending on the complexity of the applications you need),
  • development of the foundation and UI components libraries

As I was highlighting at the beginning, if you have a pretty good idea of what you’re trying to achieve, what kind of Design System you or your clients need, and how it’s going to be organised, this step might be quite straightforward.

The “hardcore” development part is around the SASS libraries, which is also very much optional, or might require just some adjustments over something you have already built previously.

While developing the various StarterKits, I’ve found that the whole process of the copy of the files over to the main PatternLab every time the content has changed can be quite annoying, as it’s not a process that can be automated through Gulp. This is normally not needed but could be useful to see the final result or to adjust some of the pattern scaffolding styles.

The good thing is that while working locally, using [npm link](https://docs.npmjs.com/cli/link) saved quite a lot of time (and it’s also suggested in the PatternLab Docs), which means that the files available in node_modules are always up-to-date.

I’ve also found that after the StarterKit file copy, the CSS files needed to be rebuilt every time, probably due to the fact that they are an external dependency. I haven’t got to that yet, and I’m assuming it’s just a question of modifying one of the tasks and trigger the compilation when the files are being updated.

Developing the actual Design System

The boring part is now done, presumably, and you’re only left with the development of the actual Design System.

This would mean that the initial project can be kick started, by copying over the StarterKits and committing all the changes.

The whole development now will gravitate around:

  1. improving the documentation supporting your Design System (if you need that),
  2. developing the actual UI Patterns,
  3. developing the SASS UI library.

This is very much in line with what you would have before, with the added benefit that most of what you need to start from is already there, and you only need to get to work on the juicy bits. ;)

This article originally appeared on Medium.