Making bsqr

October 26, 2019

Sometimes I have the thought "Damn, someone should make this into an npx script" ... well, today I listened to that voice in my head! In the rest of this post I'll go over what this little utility is & the thought process that went into creating it.

First, bsqr:

It's a little script that combines BrowserSync with qrcode-terminal — I'm definitely standing on the shoulders of giants here, not reinventing the wheel. Still, I think it's a nifty utility because it solves one nagging issue I have with BrowserSync: the external URL should be represented a QR code! I almost always want to use the external URL on my phone as well. Universal Clipboard is nice when I'm working on my personal machines but that won't cut it on my work machine where I'm not signed into iCloud.

So, with that thought in mind, I decided to make a utility that would:

  1. Get the same external URL BrowserSync is using
  2. Display the URL as a QR code
  3. Call BrowserSync like normal

Here's a GIF of bsqr doing exactly that on my latop:

Screen recording of bsqr in action

GIF of bsqr on desktop

& then on my phone:

Screen recording of bsqr being scanned
GIF of bsqr being scanned on an iPhone

So what did it take to create?

How I got the idea

I've known about BrowserSync for a long time but I didn't really appreciate how great it is until I started using 11ty a lot! Whenever I'm working on this very website I would see this:

GIF recording of the terminal output showing BrowserSync URLs

See the BrowserSync output at the end? That's what inspired me!

So my first thought was to go to the BrowserSync API docs — sure enough, it's designed to be scripted as well as run from the command line 🎉!

Writing the actual script

Writing step 1.

Here all I really did was paste & modify a version of the docs API sample:

#!/usr/bin/env node
// require the module as normal
let bs = require("browser-sync").create();

// .init starts the server
bs.init({
server: "." // NOTE: I use the current working directory instead of './app'
});

// Now call methods on bs instead of the
// main browserSync module export
bs.reload("*"); // NOTE: Here I chose to reload everything

This got the basic BrowserSync features running, so yay!

Next, step 2.

The next easiest piece was taking the other off-the-shelf dependency — qrcode-terminal — off the proverbial shelf like so:

#!/usr/bin/env node
// require the module as normal
let bs = require("browser-sync").create();
let qrcode = require('qrcode-terminal');

// .init starts the server
bs.init({
server: "."
});

/*
* NOTE: Here we're just adding a text QR code to prove it's possible
*/

qrcode.generate('lorem ipsum', {small: true});


// Now call methods on bs instead of the
// main browserSync module export
bs.reload("*"); // NOTE: Here I chose to reload everything`

This little script indeed starts a BrowserSync server right after outputting the QR code for lorem ipsum:

A screenshot of the lorem ipsum QR code in the terminal

This QR code is generated by the script above & encodes the words 'lorem ipsum'

Last, but not least, step 3.

Finally, I knew I needed to fill in the QR code text with the actual external URL. I saved it for last because I was pretty sure this would take the most work 😬.

There's a note on the docs page about using "a tool like dev-ip" (italics mine) to find the correct IP address, which I thought was interesting but I ignored when I first read it.

The first thing I actually did was try to figure out where external was coming from. I went clone the Browser Sync repo & do a local ripgrep of the codebase of the browser-sync lib directory.

Running rg external in that directory looked like this:

Output of running rg external in the browser-synclib

& that lead me to look at utils.js. This lead me on a brief detour into xip but once I got past the magic[1] of that I noticed the line above references getHostIp. Finally, it turned out to be this line that shows you where the external IP comes from: the first member of the array that dev-ip returns. Giants standing on the shoulders of giants! If we add that in to our little script we get:

#!/usr/bin/env node
let qrcode = require('qrcode-terminal');
let bs = require("browser-sync").create();
let devIp = require('dev-ip');

// .init starts the server
bs.init({
server: "."
});

/*
* NOTE: We're assuming the port is 3000, this isn't a safe assumption though
*/

qrcode.generate(`http://${devIp()[0]}:3000`, {small: true});


// Now call methods on bs instead of the
// main browserSync module export
bs.reload("*"); // NOTE: Here I chose to reload everything`

Indeed, this is the bones of what bsqr is right now. There's a little more sugar to flags, error checking & such, but it was a relatively straight forward process to get from this script to the first published version.


  1. it's really just a suffix that acts as a very clever DNS resolver ↩︎

◼️ October 26, 2019 @acwervo

← Home