Building Pattern Lab Workshop

Sara Olson, Marketing Analyst
Posted

As an intern fresh out of a local coding bootcamp, I was included in a meeting with Robbie Holmes, Chris Bloom, and Evan Lovely about Pattern Lab. They tasked me with building an internal training module that would explain the concepts of Pattern Lab to people across Phase2 (not just our front-end developers). The other key requirement was that it should be done in the "workshopper" format, popularized by nodeschool.io.Pattern Lab is a fairly complex (not complicated) system with many moving parts, and the workshopper format is very good at one thing -- being a terminal-based test runner for node.js. How to combine the two?Most of the tools Pattern Lab provides are best viewed in a browser, not the terminal. As a rapid prototyping tool, Pattern Lab feels far more concerned with CSS, HTML markup and JS assets than a terminal shell. Fixed terminal output seemed worlds removed from a responsive browser window. This changed the problem from "How to explain Pattern Lab?" to "How to explain atomic design through Pattern Lab's lens (in the terminal)?" Good question. Moreover, workshoppers presented plenty of their own challenges, as I soon discovered.

There are two popular workshopper packages that most of the nodeschool.io curriculum are based on:

As far as I know, stream-adventure is the "original" node.js terminal test-runner. Workshopper builds on the concepts from stream-adventure, adds functionality, and is maintained by a larger group of people, including the authors of stream-adventure, making  workshopper the obvious choice.It wasn't long before I hit my first wall. I found that the verify function -- one of the most critical components for the workshopper format, because it is the command that checks your answer to the test("lesson") it is running -- wouldn't correctly test & display HTML or CSS instead of javascript code. This is a problem because HTML and CSS are the basic units needed to test for atomic design in Pattern Lab.After beating my head against this wall for a while, I started looking for other options. My search led me to a fork of workshopper, workshopper-jlord. This fork had its fair share of changes but the most valuable thing, an improved verify function, far outweighed the annoyances. So I made a new branch and started from scratch with this as my base.

Workshop Components & Resources

The entire Pattern Lab Workshop was essentially built with four things:

  • workshopper-jlord - I'll cover these below.
  • node’s fs/path modules - aka the real stars of the show
  • Nested callbacks - There are so many different ways (notation-wise) to actually write these things, it was hard to keep straight at first.
  • Lin Clark's amazing post on Authoring Node.js workshopper lessons Despite all of the incredibly varied workshoppers present on nodeschool, documentation surrounding is still surprisingly slim. Without this post I would have been dead in the water before I even started.

Workshoppers still seem wonky to me, so the easiest way to explain how they (and by extension, Pattern Lab Workshop) work is to just dive right in. Let's look at the first lesson, found here.

A standard workshopper lesson has 4 files:

  • problem.md - Explanation text loaded at the beginning of every test
  • setup.js - Required by workshopper to make commands work
  • verify.js - Steps needed to run the solution / verify the solution is correct
  • solution.js - What a correct answer should return.

Our lesson has a bonus file:

  • example.html - Result of one of the patches for workshopper-jlord, creating an easy way to display just about anything you need in the terminal window. It's essentially a more literal solution.js.

For our purposes, we can ignore setup.js. Setup.js is the same in every problem and doesn't do anything terribly interesting. Take a quick peek at problem.md for some context to what verify is doing. Example.html is our answer to the steps outlined in problem.md.

That leaves solution.js and verify.js.

Building the Workshop

Pattern Lab Workshop is built around verify.js searching for the contents of a specific file in a specific directory. It does that by using the fs and path modules and several nested callbacks.

Let's see just how that works.

  1. [geshifilter-php]```verify.js
  2. var exec = require('child_process').exec;
  3. var fs = require('fs');
  4. var path = require('path');
  5. var filename = "01-tutorial-button.mustache";
  6.  
  7. //check the directory exists
  8. //check the file exists
  9. //verify the contents of the file
  10.  
  11. findFile();
  12. ```[/geshifilter-php]

The first three lines are specific modules required. There are the fs and path modules we've heard so much about. require('child_process').exec; is the workshopper/terminal window. We also declare a filename to search for-- the user is given the name and path and contents in problem.md.

findFile(); is the starting function of the callbacks. It does this:

  1. [geshifilter-php]```verify.js
  2. function findFile() {
  3. if (process.cwd().match("atoms")) {
  4. check(process.cwd())
  5. } else {
  6. check(path.join(process.cwd(), "/atoms/"))
  7. }
  8.  
  9. function check(userspath) {
  10. fs.readdir(userspath, function(err, files) {
  11. if (err) {
  12. return console.log(err);
  13. }
  14. var allFiles = files.join();
  15. if (allFiles.match(filename)) {
  16. console.log("File in atoms folder!");
  17. checkFile();
  18. }
  19. else {
  20. console.log("File NOT in atoms folder!");
  21. }
  22. })
  23. }
  24. }
  25. ```[/geshifilter-php]

First, this ensures that we're in or immediately above a folder called /atoms/ by calling another function check. The console.log()s are my workaround for workshopper limits. Due to the way workshopper(/-jlord)'s core verify function works, some javascript needs to be output to the terminal. By limiting that javascript to confirmations that something exists as it should, I avoided that problem.

The above code block calls one more function -- named checkFile(); -- that introduced another set of problems.

  1. [geshifilter-php]```verify.js
  2. function checkFile() {
  3. fs.readFile(path.join(process.cwd(), "/atoms/" + filename), 'utf8', function (err,data) {
  4. if (err) {
  5. console.log("Try running the verify command from the top-level folder of this project.");
  6. return console.log(err);
  7. }
  8. else {
  9. if (data.indexOf('<btn>') > -1 || data.indexOf('<button>') > -1) {
  10. console.log('There\'s a button!');
  11. }
  12. if (data.indexOf('{{ btnText }}') > -1) {
  13. console.log('Mustache partial present!');
  14. }
  15. }
  16. });
  17. }
  18. ```[/geshifilter-php]

Using the fs module, the function tries to get the data from /this-project/atoms/01-tutorial-button.mustache. If it can't it provides a message to check your directory. If the file's found, it performs simple string searches on the contents, console logging away if it likes what it finds.

These console.log()s are compared with the correct sequence located in solution.js. If they match, congratulations, you've passed!

All of the lessons' verify.js behave similarly, some checking multiple files, others for CSS or JSON. The files are read in the same way.

Minor Tweaks and Patches

The “last” thing to do was to make some updates to the workshopper-jlord package in order to fix minor issues I couldn't affect in the pattern-lab-workshop package itself.

The reasoning: workshopper from workshopper-jlord would print out two or three lines about git-it after every command was run, which is great if you’re doing the git-it workshop, but not so hot for anything else. This also meant I had to create another new repo to go with my fork of workshopper-jlord: a fork of the workshopper package jlord was based on, which was an older version of workshopper she had forked herself. After sorting that out, I tweaked it a bit so the lines it printed out were fully customizable at the custom-workshop level, not the core workshopper level.

I did this using another node module called lodash and a new json file. I probably could have put that json in the existing package.json, but I wanted it to be very clear where it was editable. It's called strings.json.

The other update I made was to allow for an example file to be printed after each lesson. Workshopper and workshopper-jlord both allowed for this already, but they only worked if it was javascript, because it came from solution.js. And this conflicted with the way I needed the verify function to behave (comparing console logs instead of returned values).

My answer was to have it call a an entirely new file so there could be no conflicts with solution.js. This way the workshopper can still do everything it wants with solution.js, and if it detects an example.html file, it'll spit that out too. If not, continues as normal.

That all happens in workshopper.js on my fork of workshopper.

  1. [geshifilter-php]```workshopper.js
  2. function onpass (setup, dir, current) {
  3. console.log(bold(green('# PASS')))
  4. console.log(green(bold('\nYour solution to ' + current + ' passed!')))
  5. ...
  6.  
  7. else {
  8. // trying to show our example
  9. console.log(repeat('-', this.width))
  10. console.log('Here\'s what our solution looks like:' + '\n')
  11.  
  12. var example = fs.readdirSync(dir).filter(function (file) {
  13. return (/^example.*\.html/).test(file)
  14. }).map(function (file) {
  15. shell.echo(fs.readFileSync(path.join(dir, file), 'utf8'))
  16. // .toString()
  17. // .replace(/^/gm, ' ')
  18. }
  19. )
  20.  
  21. console.log(repeat('-', this.width) + '\n')
  22. console.log(
  23. 'You have '
  24. + remaining
  25. + ' challenge'
  26. + (remaining != 1 ? 's' : '')
  27. + ' left.'
  28. )
  29. console.log('Type `' + this.name + '` to show the menu.\n')
  30. console.log(repeat('-', this.width) + '\n')
  31. }
  32. ```[/geshifilter-php]

Not the most readable thing in the world, but it's pretty much wholesale the solution.js output earlier in the function. The main differences are this file outputs using the shell module, because it echos to the terminal perfectly, keeping spacing and tabs in place. I thought that was a great benefit when outputting our example markup, keeping things nice and readable. The final step of these tweaks was opening pull requests back to their respective modules -- workshopper-jlord and jlord’s fork of the base workshopper module. Unfortunately, I don’t believe either of the pull requests will be implemented/accepted, as jlord appears to be moving away from the workshopper format towards a cleaner, app-based approach opposed to terminal windows.

Open Source and Closing Thoughts

I really enjoyed creating this workshop and am pretty thrilled my first contribution to the open source community went so well. It was incredibly valuable for me as a new developer to be given the time and trust to just figure it out. There were some helpful nudges (and more explanations of nested callbacks), but for the most part I was given the opportunity to really poke around on my own and explore two new technologies I had never touched before: node.js and Pattern Lab.Pattern Lab Workshop was developed from August 5th to September 28th, about two months from that first meeting to release. It was published to npm and then added to Nodeschool's curriculum. Brad Frost (one creator of Pattern Lab) sent out a tweet that even got retweeted by Smashing Magazine. That was pretty wild to me.When I was building this, the node version of Pattern Lab was still under heavy development. At some point, I'd like to revisit this workshopper and include the node version of Pattern Lab as originally conceived in that first meeting. It was implausible then but now seems like a real possibility. That would come with a whole host of bonuses including:

  • Clearer folder structure & examples
  • The ability to see how Pattern Lab works locally in the browser while still running the workshop.

Plus, what better way to learn than by touching all the buttons?

Sara Olson

Marketing Analyst