Creating a Custom addon for Local by Flywheel

#

Update: This post was originally written about Pressmatic before the app was purchased by Flywheel and had its name changed to “Local”. I have updated the post’s text and most of the code to reflect this change.

If you’ve been developing WordPress sites for more than just a couple of years, you’ve no doubt heard of MAMP or possibly WAMP if you swing that way. I would even venture to say that if you’ve been developing WordPress sites on a Mac for five years or more, you have almost definitely used MAMP for a good chunk of that time.You likely still use it because it works well enough and there hasn’t been much else out there to entice you away.

Vagrant is a bit like voodoo and while it’s easy enough to set up a new VVV install, it’s not as easy as creating a new host in MAMP. DesktopServer is kind of a nice option, it takes away the hassle of having to install WordPress yourself, but it also feels a bit basic, like it’s hiding just a bit too much from you for the sake of being simple.

Until recently, these were your main options for developing WordPress locally – none of them really seem fantastic, none of them really feel like the modern tool that forms the basis of a modern web developer’s stack. I can only guess that it was that feeling that lead Clay Griffiths to build something better, Local.

Modern Tools for the Modern Developer

Local is built on top of Electron, an app framework that allows web developers to create native desktop apps using JavaScript and HTML. If you’ve heard of Electron before, it’s probably through its parent project Atom which bills itself as the “hackable text editor for the 21st century” and I’d argue that Local might just be the “hackable local development environment for the 21st century”.

Docker is doing the heavy lifting behind the scenes for Local, which makes it fast and portable. If you’ve tinkered with Docker much you probably know two things about it: it’s very powerful and it’s very complex. Local proves to be a great abstraction for both aspects, both harnessing its power and hiding the complexities of running Docker containers behind an efficient UI.

While Local isn’t open source like Atom, it does have an addon API which allows developers to extend its functionality by utilizing hooks, similar to WordPress. To get you started writing addons of your own, Flywheel have released two addons under the MIT license as well as some API documentation on Flywheel’s github – so let’s dive in.

Writing a Simple Local Addon

At the time I’m writing this, the addon API documentation is pretty sparse, though I’m told that it will be getting updated soon enough. Personally, I like to skip the documentation whenever possible, so I’ll just start us off with a stripped down version of one of the example addons which I’ve made available on GitHub.

Getting Started

To get started we’ll clone down the repo:

git clone git@github.com:JRGould/simple-pressmatic-addon.git my-pressmatic-addon && cd my-pressmatic-addon

Install the NPM dependencies and run the build script.

npm install
npm run-script build

This will create the lib directory and transpile src/renderer.js to ES2015 and copy it to lib.

Now we can link the plugin directly into Local’s addons folder:

ln -s "$(pwd)" ~/Library/Application\ Support/Local\ by\ Flywheel/addons/

You can restart Local and activate your addon by navigating to Settings > Addons and checking the box next to the aptly named “Addon for Local”.

activating the addon

Local will prompt you to restart, and when it does you should see what appears to be Chrome Developer Tools.

dev tools open

This looks like Chrome Developer Tools, because it is! As I mentioned before, Local is built on Electron which uses Chromium to render and run the front-end of the app. So far, this is all our addon does, it opens the dev tools and adds a function to the window object called reload() which will reload the app without forcing you to quit and re-run Local manually. This should save us a couple of cycles during development. Go ahead and try that out by typing reload() into the dev tools console and hitting enter.

Utilizing Hooks

Now that our addon is running and we can quickly reload the app to test our changes, Let’s see if we can add some custom functionality. Using the Local Stats Addon for reference, we’ll start by adding a menu item under each site’s “More” menu, which is done in that addon’s renderer.js file using the siteInfoMoreMenu filter. This should feel familiar to any seasoned WordPress developer, as it is reminiscent of hooks in WordPress.

We’ll use the siteInfoMoreMenu filter to add a menu item called “Plugins” by adding the following code to our own renderer.js:

hooks.addFilter( 'siteInfoMoreMenu', function( menu, site ) {
    menu.push( {
        label: 'Plugins',
        enabled: !this.context.router.isActive(`/site-info/${site.id}/my-component`),
        click: () => {
            context.events.send('goToRoute', `/site-info/${site.id}/my-component`);
        }
    } );
    return menu;
} );

Now save renderer.js, give babel a minute to do its thing and then reload() Local. Now you can click into a site and expand the “More” menu and you should see that “Plugins” has been added to the list.

added plugins menu item

If you click “Plugins” you should see a warning in the console because the my-component route doesn’t exist and if you reload() Local now you’ll receive the warning and a blank screen so it’s probably a good idea to quit Local and launch it again.

Creating a Component

Let’s give our new menu item somewhere to go by creating a custom component. We’ll start by creating a new file in src/ named MyComponent.js and start with the following boilerplate:

/* src/MyComponent.js */

module.exports = function( context ) {

    const Component = context.React.Component 
    const React = context.React
    const $ = context.jQuery

    return class SiteInfoStats extends Component {
        constructor( props ) {
            super( props )
            // init class vars
        }

        componentDidMount() {
            // set up 
        }

        componentWillUnmount() {
            // tear down
        }

        render() {
            return (
                <div style={{ display: 'flex', flexDirection: 'column', flex: 1, padding: '0 5%' }}>
                    <h3>Active Plugins</h3>
                </div>
            );
        }
    }

}

I’m not very well versed in React, so bear with me and please let me know in the comments if you have any improvements, but that should be enough to render our “Plugins” page and do any setup and teardown that we might need to do later.

Now we just need to make this component show up when we click the “Plugins” menu item. We can do that by adding a route to this component in our renderer.js with Local’s routesSiteInfo content hook like so:

// Require component
const MyComponent = require('./MyComponent')(context)
// Get router handle
const Router = context.ReactRouter
// Add Route
hooks.addContent( 'routesSiteInfo', () => {
    return <Router.Route key="site-info-my-component" path="/site-info/:siteID/my-component" component={ MyComponent }/>
} );

Now you can reload() Local once again, click into a site, and select “Plugins” from the “More” menu. At this point you should be presented with a blank screen with the heading “Active Plugins”.

plugins screen

Make It Do Something…

So far this is pretty useless, so let’s see if we can make our addon do something. Once again we’ll look to the Local Stats Addon for guidance. So let’s take a look at the SiteInfoStats.js component to see how we might run a command within a site’s container and grab the output.

Still working in MyComponent.js, we’ll first want to require Node’s child_process module at the top of the file:

const childProcess = require ('child_process' )

This will let us run a terminal command, which we’ll add in a bit, but let’s set up a bit more content in the view. First we’ll add a content property to our component’s state by adding the following to the constructor method:

this.state = {
    content: null
}

Next, we’ll display that content by adding { this.state.content } under the “Active Plugins” heading in our render method:

render() {
        return (
            <div style={{ display: 'flex', flexDirection: 'column', flex: 1, padding: '0 5%' }}>
                <h3>Active Plugins</h3>
                { this.state.content }
            </div>
        );
    }

Now, in componentDidMount we can figure out if the plugin is running. If it isn’t we’ll set state.content to say “Machine not running!” and if it is we’ll call a method called getPluginList() which we’ll create in a moment.

componentDidMount() {
    if ( 'running' === this.props.siteStatus ) {
        this.getPluginList();
    } else {
        this.setState( { content: ( <p>Machine not running!</p> ) } )
    }
}

Now we can create the getPluginList() method. The first thing we’ll do there is set state.content to say “loading…” and then we’ll create a command that will run wp plugin list inside the docker container for the current site:

getPluginList() {
    this.setState( { content: <p>loading...</p> } )

    // get site object using siteID
    let site = this.props.sites[ this.props.params.siteID ]

    // construct command using bundled docker binary to execute 'wp plugin list' inside container
    let command = `${context.environment.dockerPath} exec ${site.container} wp plugin list --path=/app/public --field=name --status=active --allow-root`
}

Don’t let that command scare you, it essentially boils down to docker exec [container id] wp plugin list.... The extra arguments at the end allow us to run the command as the root user, which is the default here when running docker exec, and set constraints on wp plugin list that cause it to only return the names of active plugins.

Now we just need to execute the command using the Node’s child_process module which we have a handle to in our ChildProcess constant. We’ll use ChildProcess.exec() which will make us wait for the output rather than spawning a process in the background that we need to monitor for events. We’ll also pass context.environment.dockerEnv to exec as the env option which will make sure that docker exec runs in the correct docker environment. Here’s how that will look:

// execute command in docker env and run callback when it returns
childProcess.exec( command, { env: context.environment.dockerEnv }, (error, stdout, stderr) => {
    // Display error message if there's an issue
    if (error) {
        this.setState( { content:  ( <p>Error retrieving active plugin list: <pre>{stderr}</pre></p> ) } )
    } else {
        // Display active plugins list
        this.setState( { content: <pre>{ stdout }</pre> } )
    }
} );

In the snippet above, we’re passing a callback as the last argument which checks to see if the command has returned an error, and either prints the error or prints the stdout variable, (what’s printed to your screen if you were to run this command from a terminal). Now you can refresh() Local, click into a running site, and you should see a list of the active plugins under the “Active Plugins” heading.

active plugins list

If you have a lot of sites running, this may take a few seconds to switch from loading... to displaying the list of plugins. Also, if you see any errors in the console, double check to make sure they have to do with our code as they may be coming from a different addon or a different part of Local. I’ve noticed occasional warnings pop up in there due to sites not running even though everything seemed to be running fine in our addon.

To wrap things up, I’ve added a bit of formatting and a message if there are no plugins to display. Here’s the getPluginList() method in its entirety:

getPluginList() {
    this.setState( { content: <p>loading...</p> } )

    // get site object using siteID
    let site = this.props.sites[ this.props.params.siteID ]

    // construct command using bundled docker binary to execute 'wp plugin list' inside container
    let command = `${context.environment.dockerPath} exec ${site.container} wp plugin list --path=/app/public --field=name --status=active --allow-root`

    // execute command in docker env and run callback when it returns
    childProcess.exec( command, { env: context.environment.dockerEnv }, (error, stdout, stderr) => {
        // Display error message if there's an issue
        if (error) {
            this.setState( { content:  <p>Error retrieving active plugin list: <pre>{stderr}</pre></p> } )
        } else {
            // split list into array
            let plugins = stdout.trim().split( "\n" )
            // Only create unordered list if there are plugins to list
            if ( plugins.length && plugins[0].length > 1 ) {
                this.setState( { content: <ul>{ plugins.map( (item) => <li key={ plugins.indexOf(item) }>{ item }</li> ) }</ul> } )
            } else {
                this.setState( { content: <p>No active plugins.</p> } )
            }
        }
    } );
}

And here’s how the final product looks after I remove the lines to show the dev tools window from renderer.js:

the final product

Wrapping Up

You could argue that the addon we’ve created today only adds marginal utility to Local and you’d be right, but the exciting thing is that we were able to add anything at all to an already powerful development tool. The prospect of extensible apps is an exciting one for developers and users alike. Local by Flywheel is one of the first to show us this exciting future, and I hope to see more apps built using the Electron platform that allow for this sort of extensibility.

About the Author

Jeff Gould

Jeff is a problem solver at heart who found his way to the web at an early age and never looked back. Before Delicious Brains, Jeff was a freelance web developer specializing in WordPress and front-end development.