Using Chrome, Grunt and Git to automate inline CSS editing

Chrome's Workspaces makes it fairly easy to persist changes you make in the Inspector tool:

Setting up a workspace in Chrome

Start by adding your local directory as a 'workspace' in Chrome. To do this, open the Inspector Tool (ALT+CMD+I on Mac) and go to 'Sources'. Here, right/middle click in the pane and select "Add Folder to Workspace". Chrome will ask for permission to access this directory on your file system.

When that's done, find the remote file you want to map to a local workspace file (I'm using www.4chan.org in this example) and right/middle click on it. Next, select "Map to File System Resource".

This will bring up a search field that will allow you to search for the local file. Click on the file you wish to map.

Voila - that's it. Now to try it out. Go back to the Elements view, select an element and change it using the Styles tab. In this trivial example, I changed the background colour to black.

Navigating to the file itself (global.50.css in our case), you'll see that the CSS rule itself has been updated. Pretty nifty.

Adding in Grunt and Git

Making local changes are fine, but we can automate 90% of the workflow. The idea is to make changes in the Inspector and have these changes end up in version control.

To work around this, I created a very simple-minded Grunt task. The idea is for two things to happen concurrently (hence the use o:

  • The specified directory needs to be watched for changes
  • Git needs to pull every n seconds, to ensure that the local repo is in sync with remote (in case changes are made elsewhere, multiple people working on the project etc)

Here's a dump of the Gruntfile:

//Relative path to the dir being maintained/watched
var SINKER_PATH = '../../Personal/test1';  
var PULL_INTERVAL = 30; //seconds  
var COMMIT_MESSAGE = "Auto-commit from Grunt"

module.exports = function (grunt) {

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        concurrent: {
            target: {
                tasks: ['task-interval', 'watch'],
                options: {
                    logConcurrentOutput: true
                }
            }
        },

        'task-interval': {
            your_target: {
                options: {
                    taskIntervals: [
                        {interval: 1000 * PULL_INTERVAL, tasks: ['gitpull']}
                    ]
                }
            }
        },

        gitadd: {
            your_target: {
                options: {
                    cwd: SINKER_PATH,
                    verbose: true,
                    all: true,
                    force: true
                }
            }
        },

        gitcommit: {
            your_target: {
                options: {
                    cwd: SINKER_PATH,
                    verbose: true,
                    message: COMMIT_MESSAGE,
                    noVerify: true,
                    noStatus: false
                }
            }
        },

        gitpush: {
            your_target: {
                options: {
                    cwd: SINKER_PATH
                }
            }
        },

        gitpull: {
            your_target: {
                options: {
                    cwd: SINKER_PATH
                }
            }
        },

        watch: {
            files: [SINKER_PATH + '/*'],
            tasks: ['gitadd', 'gitcommit', 'gitpush']
        }

    });

    grunt.loadNpmTasks('grunt-git');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-task-interval');
    grunt.loadNpmTasks('grunt-concurrent');

    grunt.registerTask('default', ['concurrent:target']);
};

I've uploaded the project here.

Some concluding thoughts:

Event-driven behaviour is almost always better than time-based operations, but fortunately the pull task is lightweight and incremental. The add -> commit -> push Git procedure is also a bit of a sledgehammer and won't work for all workflows.

There is another solution that involves Git web hooks and a Chrome App that utilises Native Messaging, but that's another post for another day.