Developing a Local addon – 03

Making the code React. I’m done with React puns.

This is the third part

This post is the third one, following the first and the second, in a series where I’m chronicling my attempt at a Local addon.
The high-level idea is to be able to control the XDebug configuration of the PHP version used to serve the site allowing me to activate and deactivate XDebug and setting some relevant fields in the process; in the previous post I’ve exposed my understanding of Local technology stack and how to use it to make it do, through “raw” terminal commands, what I want it to do.
As any one of my projects it’s a discovery process while trying to “scratch an itch”; along the way, I will make amateur mistakes in all the technological fields involved (Docker and React in this case) and I know it.
Getting it “done right” has more value for me, in this context, than “getting it done”.
I’ve put a repository online that contains the code in its current state on the master branch; it has more the release state of a leak than that of an alpha though.

Local add-on boilerplate

I’ve started working, for this add-on, from the code provided in the simple-pressmatic-addon repository by Jeff Gould; from the same source came the Delicious Brains article that started it.
After I’ve cloned the plugin, removed the .git folder to “make it mine”, installed the dependencies and removed the code specific to the example from the article I’m left with the src/renderer.js file that I’ve updated to add the “XDebug Control” tab:

// file src/renderer.js

'use strict'

const path = require( 'path' )

module.exports = function ( context ) {
    const hooks = context.hooks
    const React = context.React
    const remote = context.electron.remote
    const Router = context.ReactRouter

    // Development Helpers
    remote.getCurrentWindow().openDevTools()
    window.reload = remote.getCurrentWebContents().reloadIgnoringCache

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

    const XDebugControl = require( './Component/XDebugControl' )( context )

    // Add Route
    hooks.addContent( 'routesSiteInfo', () => {
        return <Router.Route key="site-info-my-component" path="/site-info/:siteID/xdebug-control" component={ XDebugControl }/>
    } )
}

Now comes my “PHP guy” explanation of things; a much better one is found in Delicious Brains article.
In the package.json file I tell Local, an Electron app, that the entry point for the add-on is the lib/renderer.js file; this seems to be a non-standard entry in the package.json file; while I’m working on the src/renderer.js file npm will build and compile the distribution version to the lib folder.
At the top of the file I initialize the context getting hold of some global variables and defining the function that will be returned bt the module I’m writing:

module.exports = function(){
    // ...
}

This, quite literally, means “this module provides, exports, a function of the context”.
From that context, I initialize some variables I need like React library itself, the router and the Electron remote command.
With the latter, to speed up the development process, I open Chrome dev tools to be able to debug the application; this will be removed once the application is ready to ship.
Local, sticking to a WordPress custom, allows developers to extend its functionalities using “hooks”; in this case, my add-on is adding a menu entry, the “XDebug Control” tab, to the “More” menu.
The menu entry will be enabled if the route is active and, when clicked, will try to resolve the /site-info/:siteID/xdebug-control path; the way I do that is by hooking once again to addContent at the end of the file.
The route itself is a new instance of the ReactRouter component handling requests on the /site-info/:siteID/xdebug-control path building and using an instance of the XdebugControl component.

The XDebugControl component

The structure of each file becomes soon familiar when working with modules and the src/Components/XDebugControl.js file makes no exception.
Digesting React flow bit by bit it’s important to understand how React components work: each component [has a lifecycle] it goes through when constructed and when updated.
In the src/renderer.js file I’m constructing an instance of the XdebugControl class with this line:

const XDebugControl = require( './Component/XDebugControl' )( context )

This means that the following methods will be called, as soon as the Local application starts, on hte XDebugControl component:

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

The tab could not be visible and the site could not be running yet all the methods above will be called.
This means none of those is a good place to do any container-related operation like trying to discern if XDebug is currently active or not.
The constructor method proves useful to set the initial state and the site object and, for the time being, I leave the other methods alone.
Looking again at a React component lifecycle when updating it I find out the method that will be called when the Component becomes visible is the componentWillReceiveProps one and set up a first version of the add-on accordingly:

// file src/Components/XdebugControl.js

module.exports = function ( context ) {

    const Component = context.React.Component
    const React = context.React

    return class XDebugControl extends Component {
        constructor( props ) {
            super( props )
            this.state = {
                machineStatus: 'off',
                xdebugStatus: 'n/a',
            }
            this.site = this.props.sites[this.props.params.siteID]
        }

        componentWillMount() {}

        componentWillReceiveProps( nextProps ) {
            let newState = null

            if ( nextProps.siteStatus === 'running' ) {
                newState = {
                    siteStatus: 'running',
                    xdebugStatus: 'active',
                }
            } else {
                newState = {
                    siteStatus: 'stopped',
                    xdebugStatus: 'n/a',
                }
            }

            this.setState( newState )
        }

        componentDidMount() {}

        render() {
            let statusString = null

            if ( this.site.environment !== 'custom' ) {
                statusString = 'Only available on custom installations!'
            } else {
                if ( this.props.siteStatus === 'running' ) {
                    statusString = `Xdebug status: ${this.state.xdebugStatus}`
                } else {

                    statusString = 'Machine non running!'
                }
            }

            return (
                <div style={{display: 'flex', flexDirection: 'column', flex: 1, padding: '0 5%'}}>
                    <h3>XDebug Controls</h3>
                    <h4><strong>{statusString}</strong></h4>
                </div>
            )       
        }
    }
}

I have, in terms of rendering, a light initial state set in the constructor method and will re-render using my first dynamic parameter, the site status, when needed.
For the time being these are the two states the addon will go through:

And while this is trivial already understanding when not to perform costly operations took quite some time to me.
The key takeaway here was understanding the React component lifecycle and how it interacts with an Electron application.

Getting XDebug current status

In the current version of the code the addon is not yet interacting with the container in any way.
The first interaction I will put in place is the one to read the current XDebug status from the container; I’ve shown in the previous post how to do that on a bash level interacting with the container directly but it is now time to move that logic to the add-on code.
The first modification I make is to update the componentWillReceiveNewProps method to use a still undefined container dependency to read the current status of XDebug.
The container property is initialized in the constructor method passing it the docker sub-dependency.

// file src/Components/XdebugControl.js

module.exports = function ( context ) {

    const Component = context.React.Component
    const React = context.React
    const Docker = require( './../System/Docker' )( context )
    const Container = require( './../System/Container' )( context )

    return class XDebugControl extends Component {
        constructor( props ) {
            super( props )
            this.state = {
                siteStatus: 'off',
                xdebugStatus: 'n/a',
            }
            this.site = this.props.sites[this.props.params.siteID]
            this.docker = new Docker()
            this.container = new Container( this.docker, this.site )
        }

        componentWillMount() {}

        componentDidMount() {}

        componentWillReceiveProps( nextProps ) {
            let newState = null

            if ( nextProps.siteStatus === 'running' ) {
                newState = {
                    siteStatus: 'running',
                    xdebugStatus: this.container.getXdebugStatus(),
                }
            } else {
                newState = {
                    siteStatus: 'stopped',
                    xdebugStatus: 'n/a',
                }
            }

            this.setState( newState )
        }

        componentWillUnmount() {}

        render() {
            // ...unchanged
        }
    }
}

The Docker class

The Docker class is a general-purpose wrapper around some basic Docker-related operations; I’ve moved its code in the src/System/Docker.js file:

// file src/System/Docker.js

module.exports = function ( context ) {
    const childProcess = require( 'child_process' )

    return class Docker {
        static getDockerPath() {
            return context.environment.dockerPath.replace( / /g, '\\ ' )
        }

        static runCommand( command ) {
            let dockerPath = Docker.getDockerPath()
            let fullCommand = `${dockerPath} ${command}`

            return childProcess.execSync( fullCommand, {env: context.environment.dockerEnv} ).toString().trim()
        }
    }
}

Walking the path of a the Javascript dilettante I’ve made both methods of the class static, it works like PHP and it means those are class methods instead of being instance methods; the reason being that there is really no use for an instance of this class as there is no state change lacking the class any properties.
I guess speed is a factor too. I guess.
Until now I’ve passed the context object around in any require call I’ve made and that’s to allow the classes I define to read some basic and general purpose information from it; in the case of context.environment.dockerPath that value will be a string representing the absolute path, on the host machine, to the Docker binary bundled with Local.
On my machine that value is /Applications/Local by Flywheel.app/Contents/Resources/extraResources/virtual-machine/vendor/docker/osx/docker.
In essence when running a command with the runCommand method that path and the command will be passed to the node.js child process handler; from the library, I then use the synchronous execSync method to run the command and wait for it to finish.
This is probably “PHP mentality” and I should use asynchronous processes and a cascade of callbacks to manage my application: I can only learn so much stuff in a given time.
If the command generates an exception, for any reason, I’m leaving that exception bubble up to the client object method to allow for exception-based logic to be put in place; an example of that in the Container class below.

The Container class

Since Docker is a manager of containers it only made sense to reflect the same dependency in the class structure: the Container class will require, in its constructor method, a docker and a site object; the first I’ve shown above and the second is, essentially, a collection of information about a single site managed by Local.
Where the Docker class made sense in static form the Container class will make sense, having to keep track of a complex object state (not “React state”, just “object state”), in instance form.
The bare-bones code of the current implementation looks like this:

// file src/System/Container.js

module.exports = function ( context ) {
    const childProcess = require( 'child_process' )
    const Docker = require( './Docker' )( context )

    return class Container {
        constructor( docker, site ) {
            this.docker = docker
            this.site = site
        }

        exec( command ) {
            let fullCommand = `exec -i ${this.site.container} sh -c "${command}"`

            return Docker.runCommand( fullCommand )
        }

        getXdebugStatus() {
            try {
                let status = this.exec( `wget -qO- localhost/local-phpinfo.php | grep Xdebug` )
                if ( status.length !== 0 ) {
                    return 'active'
                }
                return 'inactive'
            }
            catch ( e ) {
                return 'inactive'
            }
        }
    }
}

All this yields the initial behavior I’m looking for, correctly displaying the lazily-loaded XDebug status of active custom installation Local sites:

Next

The code I’ve shown in this post is on GitHub tagged post in a not clean, commented-out-blocks-of-code form.
I’m rewriting the first minimum version in light of some more careful pacing and learned notions and that’s the reason the code in the branch looks not pleasant to the eye.
I will delve into more complex code in the next post putting, or rather restore, the functionalities that make the add-on somewhat useful.

Probably related

I appreciate your input