https://andrewbaker.ninja/wp-content/themes/twentysixteen/fonts/merriweather-plus-montserrat-plus-inconsolata.css

πŸ‘6views
How to Make WordPress Plugin Upgrades Clean Up Properly

Most WordPress plugin developers eventually hit the same invisible wall: you ship an update, everything looks correct in the zip, the version number changes, the code is cleaner, and yet users report that the old JavaScript is still running. You check the file. It is updated. They clear cache. Still broken. Here is the uncomfortable truth: WordPress plugin uploads do not reliably overwrite existing files inside subdirectories. That single behaviour is responsible for an enormous amount of ghost bugs.

When WordPress installs or upgrades a plugin via zip upload, it extracts the archive into /wp-content/plugins/plugin-name/, it does not reliably purge old files, it may skip overwriting certain files, and it does not clean up removed subdirectories. If your previous version had assets/admin-v7.js and your new version ships assets/admin-v8.js, WordPress will add the new file but it will not remove the old one. Worse, if you reuse the same filename such as assets/admin.js, WordPress may silently skip replacing it depending on extraction behaviour, file permissions, or caching layers. The result is subtle and destructive: you think v8 is running, but v7 is still executing in production. This is not a caching issue. This is a file lifecycle issue.

The first and most important structural decision is to avoid putting assets inside a subdirectory. Move everything to the plugin root folder. Instead of shipping plugin-name/plugin-name.php and plugin-name/assets/admin.js, ship plugin-name/plugin-name.php, plugin-name/admin.js, plugin-name/admin.css, and plugin-name/README.md. WordPress reliably extracts and overwrites files at the same level as the main plugin PHP file. Subdirectories are where stale files survive. Flattening your structure removes an entire class of upgrade bugs. It is not elegant. It is operationally correct.

Users do not always delete plugins cleanly. Hosting panels fail. Permissions vary. File deletions sometimes partially succeed. So add a safety net. When the plugin is deactivated, wipe asset files manually.

register_deactivation_hook( __FILE__, function() {
    $dir = plugin_dir_path( __FILE__ );    foreach ( glob( $dir . 'admin.{js,css}', GLOB_BRACE ) as $f ) {
        @unlink( $f );
    }    $assets = $dir . 'assets/';
    if ( is_dir( $assets ) ) {
        foreach ( glob( $assets . '*' ) as $f ) {
            if ( is_file( $f ) ) {
                @unlink( $f );
            }
        }
        @rmdir( $assets );
    }
});

This ensures that when someone performs Deactivate, Delete, Upload, Activate, there are no survivors. Even if WordPress fails to delete a subdirectory, the deactivation hook already removed its contents. This is defensive engineering.

Not everyone deactivates before upgrading. Some upload via FTP, replace files manually, use automated deploy scripts, or install updates without deactivation. So add a version change detector. On admin_init, compare a stored version value with the current plugin version constant. If they differ, run cleanup.

add_action( 'admin_init', function() {    $cached = get_option( 'myplugin_loaded_version', '' );    if ( $cached !== MY_PLUGIN_VERSION ) {        if ( function_exists( 'opcache_reset' ) ) {
            opcache_reset();
        }        $assets = plugin_dir_path( __FILE__ ) . 'assets/';
        if ( is_dir( $assets ) ) {
            foreach ( glob( $assets . '*' ) as $f ) {
                if ( is_file( $f ) ) {
                    @unlink( $f );
                }
            }
            @rmdir( $assets );
        }        update_option( 'myplugin_loaded_version', MY_PLUGIN_VERSION );
    }
});

This catches FTP upgrades, manual overwrites, partial deployments, and version mismatches. It also resets OPcache to eliminate stale PHP bytecode. Now your plugin self heals on version change.

Even if the filesystem is clean, browsers are not. When enqueueing scripts or styles, always use the plugin version constant as the ver parameter.

wp_enqueue_script(
    'myplugin-admin',
    plugin_dir_url( __FILE__ ) . 'admin.js',
    array(),
    MY_PLUGIN_VERSION,
    true
);

Every release must increment the version constant.

define( 'MY_PLUGIN_VERSION', '1.2.3' );

If you forget this step, browsers will continue serving cached assets even if the files are correct. This is standard practice and it is also the most commonly forgotten detail.

When you implement all four protections, your user install process becomes simple and reliable: Deactivate, Delete, Upload new zip, Activate. No SSH. No manual file cleanup. No stale JavaScript ghosts. Plugin lifecycle management is not glamorous and it does not sell features, but broken upgrades destroy trust. Most plugin bugs blamed on WordPress being weird are actually poor file hygiene decisions. If your plugin changes asset structure over time, moves files between folders, renames scripts, or leaves old files behind, you are building technical debt into every user’s filesystem. The fix is straightforward: flatten the structure, clean on deactivate, detect version changes, and bust caches correctly. Upgrade reliability is not about clever code. It is about eliminating stale state, because in production the filesystem is part of your architecture.

Leave a Reply

Your email address will not be published. Required fields are marked *