Publishing a React Component Demo to Github Pages

While working on a private project, whenever I write some bit of logic that I think could be useful to others without compromising my project's trade secrets, I like to break it out into a separate component, push it to a new project on GitHub, and then register it on npm to others can find it.  Since my recent work has been mostly front-end, I also like to create a demo of it on GitHub Pages.

My recent projects are all web components built with React.  Since I began using React with Webpack a few months ago, I've fallen into a simple pattern whereby I place all the code for my component in a src/ directory, then I write a simple demo.html and demo.jsx and place those in the top directory of my project.  demo.jsx contains only the logic to bootstrap a React app that contains only my component.

In development mode, webpack will bundle demo.jsx into dist/bundle.js.  demo.html links to bundle.js and has a placeholder where the bootstrap code in demo.jsx will inject the top-level React component into the DOM, as per this simple Hello World example.  Then I fire up webpack-dev-server in order to view and troubleshoot my component in a browser.

Here is a recent React component project I'm still working on.

So far so good.  But when the component is finished, I also want to create a demo page that I can host on GitHub Pages.  I'd like to be able to do this on all my React component projects, with as little fuss as possible.  This means I want to automate it with scripts.

I couldn't find any docs stating it, but GitHub Pages seems to want to serve only a file named index.html.  Everything else depends on index.html being in the top level of the project and pulling in whatever scripts and assets it will need to render the page and showcase my component.  Since I prepare my demo web app with Webpack, almost everything it needs will be in either dist/bundle.js or in a subdirectory under dist/ that my app will already know how to resolve.  If GitHub Pages doesn't find index.html, it just renders a traversable directory listing of your project.  Not cool.

If my demo page were already named index.html, I wouldn't need to do anything other than include dist/ in my Git repository and push it to GitHub.  But that's not nice.  Including any files that get generated into a GitHub repo is an anti-pattern.  Every time I rebuild the dist/ dir, it will be out of sync and Git and I will need to commit it again.  Browsing or searching previous commits becomes tedious when my screen fills up with thousands of lines from bundle.js.

Also, I named my top level page demo.html, not index.html.  I name it demo.html as cue that this page is not part of the component, but rather a stand-in for an imaginary page in another project that want to include it.

Finally, when a browser in the wild loads lsiden.GitHub.io/my-project-name, I don't want anything to be reachable but index.html and the dist/ folder tree.  I'm not sure it would matter if my component is already open-source and only I or those I designate can push to it, but I want the demo site to be clean of cruft.

Every GitHub project has a Settings page with a GitHub Pages section near the bottom.

There, you can tell it to serve your assets from a special branch named "gh-pages".  This branch should have exactly the directory image that you would copy to your own server to host it.  You could create the gh-pages branch in your local repository, go there and remove everything but the files you want to deploy to GitHub Pages, then push that branch to your GitHub repo.  But this could get tedious and confusing to keep a local branch that is not ever in sync with your master or dev branch.

The utility gh-pages automates this for you.  From the command-line, you can call "gh-pages -d staging-dir" and the utility will make a copy of your repo, create a new branch called "gh-pages", copy everything in staging-dir/ to your top level directory and delete everything else, and will then push that to your GitHub repo.  The gh-pages branch is for staging to GitHub Pages only.  You never need to fetch the gh-pages branch or track it.  Likewise, you do not need to add the contents staging-dir/ to your Git repo.  Just add "staging-dir/" to your .gitigore file.  Once gh-pages has run, I can safely delete staging-dir/.

That still leaves me with the problem of how to automate the preparation of staging-dir/ each time I want to update GitHub pages.  For this I had to write a little code:

const path=require('path')
const fs=require('fs-extra')
const {exec} = require('child_process')
const ghPagesPath = './gh-pages'

function toPromise(func) {
    return function() {
        const args = [].slice.call(arguments)
        return new Promise(function(resolve, reject) {
            args.push(function(err) {
                err && reject() || resolve()
            })
            func.apply(null, args)
        })
    }
}

(function(dest) {
    fs.remove(dest)
    .then(function() {
        return fs.mkdir(dest)
    }).then(function() {
        return Promise.all([
            fs.copy('dist', path.join(dest, 'dist')),
            fs.copy('./demo.html', path.join(dest, 'index.html')),
            toPromise(exec)('yarn babel demo.jsx -o ' + path.join(dest, 'index.js')),
        ])
    }).catch(function(err) {
        console.error(err)
    })
})(ghPagesPath)


I would have preferred to do it with bash, but I remembered that there are still those who use Microsoft Windows as their development platforms, and I don't want them to be dependent on Cygwin or some other Unix shell emulator on a platform I don't have, so instead I did it with NodeJs, since anyone working on this must already have that installed.

NodeJs is great for writing highly streamlined file transforms, but for simple file copies and system commands, it can be a pain because the vanilla utility methods force you to use callbacks everywhere and we know what that can lead to.

The standard package fs has FileCopy() and FileCopySync(), but they are available only in Node >= 8.5, and I still had 8.4.x installed when I was getting started.  I've upgraded to v.8.6.0 since, but I found a better way that won't require another developer to upgrade as a dependency: fs-extra.

fs-extra also has a copy() function, but it returns an instance of Promise by default if you don't provide a callback function.  I still had to wrap the call to child_process.exec in my own Promise to avoid having to use a callback. 

To help, I wrote the toPromise() function that takes a function that takes a callback and returns an instance of a function that returns Promise(), although I'm sure there must already be many others out there who had the same idea and inspiration.

I chose to call my staging directory "staging-dir/" I call it "gh-pages", but I could have called it anything.  I thought that naming it "staging-dir/" in this post would be less confusing.

Hope this helps someone.  Happy coding!

Comments

Popular Posts