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 (looking for an in-depth review? Check out our 4 Best Local WordPress Development Environments)- 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 [email protected]: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”.
Local will prompt you to restart, and when it does you should see what appears to be Chrome Developer Tools.
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.
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”.
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.
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
:
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.