Ziggy URL generator and Webpack

I've been wanting to get ziggy in use at $job for a while now because of its developer quality of life improvements around url generation. I don't want our devs to have to remember to update the ziggy generated routes whenever they update a routes file, so I need something automated in our webpack install. But ziggy's artisan command writes out the javascript file whether there are changes or not, which could send webpack in to an infinite loop because the output file is continually being regenerated.

Originally, I'd brought up a PR to make it so that the file was only written if the output from the ziggy command was different to what was on the filesystem already. Unfortunately, the ziggy team opted not to merge that, instead requesting that I find a way to not call ziggy in the first place.

I managed to make that work, but it required a custom webpack hook plugin and a little bit of logic that still feels a little awkward, but does seem to work. The hook plugin code is provided inside the webpack.config.js file, and then called in the plugins option later in the file with a new CustomZiggyHookPlugin(). Additionally, you'll probably want a new WatchExternalFilesPlugin({ files: ['./routes/**/*.php'], }) in the plugins block as well so that webpack knows to attempt runs if those files change.

Here's the plugin:

class CustomZiggyHookPlugin {
    files = [];

    shouldRunZiggy(changedFiles) {
        let shouldChangeZiggy = false;

        changedFiles.forEach((value, key, map) => {
            // If the named file doesn't exist in the files array or it's been
            // the modification timestamp of the file is more than 100ms than
            // what's been logged, we should run ziggy
            if (!this.files[key] || value.timestamp - this.files[key].timestamp > 100) {
                shouldChangeZiggy = true;
            }

            this.files[key] = value;
        });

        return shouldChangeZiggy;
    }

    checkChangedFiles(compiler) {
        const { watchFileSystem } = compiler;
        const watcher = watchFileSystem.watcher || watchFileSystem.wfs.watcher;

        // Check for a directory watcher that exists on the routes path
        const directoryWatcher = watcher.watcherManager.directoryWatchers.get(compiler.options.context + '/routes');

        // and if there are files in that directory watcher, check to see if
        // we should run ziggy against them
        if (directoryWatcher && directoryWatcher.files) {
            return this.shouldRunZiggy(directoryWatcher.files);
        }

        return true;
    }

    checkShouldAndDoRun(compiler) {
        const shouldChange = this.checkChangedFiles(compiler);

        if (shouldChange === false) {
            return;
        }

        let child = execFile('php', ['artisan', 'ziggy:generate'], (error, stdout, stderr) => {
            if (error) {
                console.error('Ziggy Error', error);
                throw error;
            }

            console.log('Ziggy Output', stdout);
        });
    }

    apply(compiler) {
        compiler.hooks.beforeRun.tap('ZiggyBefore', (params) => {
            this.checkShouldAndDoRun(compiler);
        });

        compiler.hooks.beforeCompile.tap('ZiggyGenerate', (params) => {
            this.checkShouldAndDoRun(compiler);
        });
    }
}