• Resolved Gerard Reches

    (@gerardreches)


    I’m developing a plugin that may also be used as a library in themes or plugins.

    I need to enqueue a custom script, but I cannot know the absolute path to the script, as the final location of the library may vary. I don’t want the other developers to have to manually define a constant with the path to the library/script either, as that’s extra work for them and I want the library to be as minimalistic as possible

    I also want to consider that this plugin/library might be symlinked, which means that I may not be able to know if it’s located in the plugins directory or the themes directory.

    This is where it becomes the trickiest, because PHP’s magic constants __FILE__ and __DIR__ resolve the symlinks and I’m unable to figure out where the library is being used.

    This is what I currently have, but is there any better way to do this?

    static function enqueue_admin_scripts( string $hook_suffix ): void {
        if ( self::get_page_hook_suffix() !== $hook_suffix ) {
            return;
        }
    
        if ( str_contains( __DIR__, ABSPATH ) ) {
            // Enqueue script taking into account that MY_PLUGIN may be either a plugin or a library used in another plugin or theme.
            $utils_path = trailingslashit( str_replace( ABSPATH, '/', __DIR__ ) ) . 'utils.js';
        } else {
            // The plugin has been symlinked and the previous enqueue method won't resolve.
            $utils_path = plugin_dir_url( __FILE__ ) . 'utils.js';
    
            // When using the MY_PLUGIN as a library, MY_PLUGIN_DIR must be defined in your plugin or theme using the right path.
            if ( MY_PLUGIN_DIR !== dirname( __DIR__, 2 ) ) {
                $utils_path = trailingslashit( MY_PLUGIN_DIR ) . 'includes/Settings/utils.js';
            } else {
                if ( ! defined( 'MY_PLUGIN_DISABLE_LOG' ) || ! MY_PLUGIN_DISABLE_LOG ) {
                    error_log( "WARNING: MY_PLUGIN has been symlinked. The script utils.js might not be loading correctly. If it is not loading correctly and you are using MY_PLUGIN in your theme or plugin, please define the constant MY_PLUGIN_DIR with the correct path to MY_PLUGIN. To disable this warning, use define( 'MY_PLUGIN_DISABLE_LOG', true ) in your functions.php or your plugin main file." );
                }
            }
        }
        wp_enqueue_script( 'my-plugin-utils', $utils_path );
    }

    I would like to be able to simplify this as well as to offer a bulletproof solution without requiring a constant to be defined.

Viewing 11 replies - 1 through 11 (of 11 total)
  • Moderator bcworkz

    (@bcworkz)

    I think the best approach is to make your plugin mandatory for anyone wanting to use its resources. You can then register a specific handle for your script and anyone wanting to use it can enqueue it or specify it as a dependency. As long as the registered handle is used, redundant enqueue code will get resolved by WP. Then you know exactly where your code is 🙂

    If a mandatory plugin is undesirable, you could require any devs using your code to always use the correct handle when enqueuing. Thus any redundant enqueues will be resolved by WP. Redundancy verification is by handle alone, where the script file actually resides is irrelevant.

    At the very least, require the script’s filename remain unchanged. You could then check the global $wp_scripts object’s data for the filename in one of its enqueued paths to verify your script had been enqueued by something. But this must be done very late to ensure all code has finished with its enqueues. Do so by using PHP_INT_MAX as the $priority arg for your add_action() call.

    Thread Starter Gerard Reches

    (@gerardreches)

    @bcworkz I seem to remember to have read before that it’s against the rules for plugins in the official repository to require the installation of other plugins. I may be wrong though.

    In any case, even though it will be a standalone plugin, I still want other devs to be able to include it as a library as part of their plugin/theme. Not everyone wants to have an additional plugin cluttering their plugins list, and I get it, neither do I most of the time. On the other hand, being able to use the plugin as library inside themes/plugins means that they can use the version they want and I can continue developing without worrying much about backward compatibility.

    Furthermore, all I want to require the devs to do is to require_once() the main plugin file. Everything else should work out of the box from there on. That’s why I don’t like the idea of requiring them to define a constant if they are using symlinks, so making them enqueue the script themselves is not an option unfortunately.

    Right now I am thinking of using wp_add_inline_script() to add the contents of the script to the end of one of the default WordPress scripts that I’m enqueueing inside this same method (wp-color-picker). I’m not sure if this is considered a bad practice.

    Moderator bcworkz

    (@bcworkz)

    it’s against the rules for plugins in the official repository to require the installation of other plugins

    I’m pretty sure you’re right, but I don’t have anything to do with plugin review, so don’t take my word for it 🙂

    If you will ask devs to to only require_once() your code library and do nothing else, then your code should register and enqueue your own scripts. Then the handle will always be correct where ever your code is used. Even if multiple plugins require_once() your code, it’ll always be the same handle so WP will only reference it once even if it’s enqueued several times.

    If someone should prefer your code not be referenced on specific pages they can always conditionally dequeue your script where appropriate.

    wp_add_inline_script() AFAIK is intended for including dynamically generated data related to other scripts you’ve already enqueued. Of course the added inline code can be any sort of JS, it needn’t be limited to data declarations. Could you add inline code to WP core scripts instead? You can. Is it bad practice? Maybe, IDK. You’re in a sense modifying someone else’s code (not literally, but I hope you can see what I’m saying) which IMO is not a good practice.

    But if the script you’re adding is entirely unrelated and independent from the core script, instead of wp_add_inline_script(), I think it makes more sense to output your script from the “wp_print_scripts” action hook. This too might be seen as poor practice but I think it’d be better than wp_add_inline_script() when the added script is entirely unrelated to the $handle you’re adding it to.

    People commonly output inline scripts from the “wp_head” action hook, but IMO script output from “wp_print_scripts” is better positioned in the data flow for the browser’s parser to work with.

    Thread Starter Gerard Reches

    (@gerardreches)

    Thanks for your reply @bcworkz .

    Right now I’ve changed the code to use wp_add_inline_script(), since what I had was quite messy and difficult to understand for anyone looking at the code.

    final static function enqueue_admin_scripts( string $hook_suffix ): void {
        if ( self::get_page_hook_suffix() !== $hook_suffix ) {
            return;
        }
    
        wp_enqueue_media();
    
        wp_enqueue_style( 'wp-color-picker' );
        wp_enqueue_script( handle: 'wp-color-picker', deps: array( 'jquery' ) );
        wp_add_inline_script( 'wp-color-picker', 'jQuery(".color-picker").wpColorPicker();', 'after' );
    
        /**
         * Implement the admin media model for any defined file fields.
         * @see https://codex.wordpress.org/Javascript_Reference/wp.media
         *
         * @note Script is being added inline to avoid issues when the directory is a symlink. PHP doesn't have methods to retrieve the unresolved path.
         * @see https://bugs.php.net/bug.php?id=42516
         */
        wp_add_inline_script( 'jquery', file_get_contents( trailingslashit( dirname( __FILE__ ) ) . 'wp-media-frame.js' ), 'after' );
    }

    This is part of an abstract class which is to be extended by other developers. My scripts are basically for handling the WP Color Picker (just a short line of code) and the Media Library modal.

    When it comes to the first script it’s not a bad idea to use the wp_add_inline_script() as it really is kind of expanding the native script and it’s very short as well.

    Then the WP Media script, as much as I would like to give it a handle, I don’t have a clean way to deal with edge cases where the plugin is being used as a library in an unknown location and symlinks are also in use. I’m using wp_add_inline_script() as it seems to be my best bet for now.

    The wp_print_scripts() action hook you are mentioning requires a handle, and I guess that would mean that I would still have to create that handle by giving a path to the file, which is where my issue arises.

    Now, I am wondering… Would it be possible to enqueue a script with an empty path and then use wp_add_inline_script() on that handle? And if it is possible to enqueue a script with empty path, does dequeueing a script remove the wp_add_inline_script() calls referencing to it? Because if that was possible, it would be a great solution. I wouldn’t require an absolute path and developers would still be able to manage the script handle.

    Moderator bcworkz

    (@bcworkz)

    Sorry, I meant the “wp_print_scripts” action hook, not the function of the same name. No handle required, just add a callback that outputs your script as an inline <script> block.

    As for the empty path idea, using the above action hook renders the question moot, but for the sake of discussion…
    I think passing an empty string might cause an error. You could pass a properly formed path that goes nowhere. But that would needlessly cause extra HTTP traffic. You’d be better off enqueuing an actual file that’s useful.

    Thread Starter Gerard Reches

    (@gerardreches)

    I’ve tried what I said before just for curiosity:

    Now, I am wondering… Would it be possible to enqueue a script with an empty path and then use wp_add_inline_script() on that handle? And if it is possible to enqueue a script with empty path, does dequeueing a script remove the wp_add_inline_script() calls referencing to it? Because if that was possible, it would be a great solution. I wouldn’t require an absolute path and developers would still be able to manage the script handle.

    Results are:

    • wp_enqueue_script( ’empty-handle’ ) doesn’t register the handle in $wp_scripts->queue, which means a path is required.
    • wp_enqueue_script( ’empty-handle’, ‘/fake-path’ ) registers the handle and generates a 404 HTTP request.
    • wp_enqueue_script( ’empty-handle’, ‘/’ ) registers the handle and generates a 200 HTTP Request. 10.5kB and 152ms, much bigger than most WordPress scripts.
    • wp_add_inline_script(’empty-handle’, $contents) does work and adds the script to the page, even if the enqueued path wasn’t real.
    • wp_dequeue_script(’empty-handle’) does effectively remove the added inline script attached to the handle.

    Conclusion:

    Is it possible? Yes, by adding any fake or real path to wp_enqueue_script() the $handle will be registered, and dequeueing such $handle will remove the inline scripts added to it.

    Should it be done? Not really, as it generates an extra HTTP request, which may be either a 404 or dodgy.

    Thread Starter Gerard Reches

    (@gerardreches)

    @bcworkz The hook you are mentioning

    do_action( ‘wp_print_scripts’ )

    Fires before scripts in the $handles queue are printed.

    https://developer.wordpress.org/reference/hooks/wp_print_scripts/

    fires before the enqueued scripts have been printed, causing javascript errors if the manually printed script has some enqueued dependency, in this case jquery. Just tried it.

    Using wp_add_inline_script( $handle, $data ), with jquery as handle since it’s a dependency for the script being added, seems to still be the best option, and now it’s starting to sound like it wasn’t a bad idea after all.

    Next thing that comes to my mind is: “Ok, but we are still unable to dequeue this inline script. Not because I need to, but just in case.”

    And suddenly I have this brilliant idea: “Got it! I can dequeue the script my inline script is attached to (jquery), thus removing my inline script on the process, and then just enqueue the script (jquery) again if still required!”

    And that should work! I’ve tested before that dequeueing a script effectively removes its attached inline scripts.

    Oh boy… Beautiful theory, but why isn’t it working?

    At first I thought: “It must be because jquery is still a dependency for the other script I’m enqueueing.”

    So I removed the dependency and just enqueued both scripts separately: “Still doesn’t work! Maybe there are other script that WordPress is loading that depend on jquery. Let’s change the priority of my admin_enqueue_scripts hook action to 1 so that I do these actions before anyone else.”

    And no, that made no difference.

    Anyway, my script shouldn’t need to be dequeued in any case, so I’ll leave it like this. All I wanted was to have that possibility, out of greed.

    I wish any of this little research can help someone else in the future!

    Moderator bcworkz

    (@bcworkz)

    Apologies for not being more clear that script output from “wp_print_scripts” cannot have any dependencies. The alternative is to use “wp_head” action, assuming any dependencies are linked in the head section.

    Regardless of which action hook you use, it’s outside of the WP_Dependencies mechanism so it’s difficult to be sure dependencies will be resolved for any given situation.

    I agree that adding to another handle in already use such as “jquery” is a reasonable approach. Even if dequeuing your particular script is not feasible. But you could add your own filter hook prior to your wp_add_inline_script() call that could allow other devs to conditionally change the filter’s return value which in turn could bypass your add script call. For example, the filter could normally return true which is used by a conditional statement to call wp_add_inline_script(). If an added filter callback instead returns false, the function will not be called at all.

    IMO this is preferable to later dequeuing anyway. Better to prevent something than to undo it later.

    Thread Starter Gerard Reches

    (@gerardreches)

    Hi @bcworkz, that’s actually a cool idea

    /**
     * Implement the admin media model for any defined file fields.
     * @see https://codex.wordpress.org/Javascript_Reference/wp.media
     *
     * @note Script is being added inline to avoid issues when the directory is a symlink. PHP doesn't have methods to retrieve the unresolved path.
     * @see https://bugs.php.net/bug.php?id=42516
     */
    if ( apply_filters( 'append_media_script', true ) ){
        wp_add_inline_script( 'jquery', file_get_contents( trailingslashit( dirname( __FILE__ ) ) . 'wp-media-frame.js' ), 'after' );
    }

    This is what you meant, right?

    Moderator bcworkz

    (@bcworkz)

    Exactly! Then if someone doesn’t want your script loaded for any reason they can do
    add_filter('append_media_script', '__return_false');
    __return_false() is a built-in WP function in case you were unaware. There are similar functions for null, empty string, zero, etc.

    FYI, it’s recommended you prefix public function and hook names with some unique chars, typically your plugin name’s initials. Much like how a lot of WP functions all start with “wp_”. I often use “bcw_” in examples, so my filter hook might be
    apply_filters( 'bcw_append_media_script', true )

    Helps avoid name conflicts with other plugins and themes.

    Thread Starter Gerard Reches

    (@gerardreches)

    Thanks @bcworkz, very useful information!

Viewing 11 replies - 1 through 11 (of 11 total)
  • You must be logged in to reply to this topic.