From 7a8b6513b8b1c76d48161f05b178fe0e7323ff1a Mon Sep 17 00:00:00 2001 From: jacobd91 Date: Thu, 10 Oct 2024 12:47:35 -0400 Subject: [PATCH 01/10] Initial commit. Feature built with exception of partytown library and setup code. --- UserExperience_GeneralPage_View.php | 34 ++++ UserExperience_PartyTown_Extension.php | 245 +++++++++++++++++++++++++ UserExperience_PartyTown_Mutator.php | 137 ++++++++++++++ UserExperience_PartyTown_Page_View.php | 125 +++++++++++++ UserExperience_Plugin_Admin.php | 5 + 5 files changed, 546 insertions(+) create mode 100644 UserExperience_PartyTown_Extension.php create mode 100644 UserExperience_PartyTown_Mutator.php create mode 100644 UserExperience_PartyTown_Page_View.php diff --git a/UserExperience_GeneralPage_View.php b/UserExperience_GeneralPage_View.php index e1d086085..c393ac618 100644 --- a/UserExperience_GeneralPage_View.php +++ b/UserExperience_GeneralPage_View.php @@ -169,6 +169,40 @@ ) ); + Util_Ui::config_item_extension_enabled( + array( + 'extension_id' => 'user-experience-partytown', + 'checkbox_label' => esc_html__( 'Enable PartyTown', 'w3-total-cache' ), + 'description' => __( + 'This feature allows you to optimize third-party scripts by offloading them to web workers using PartyTown.', + 'w3-total-cache' + ) . ( + UserExperience_PartyTown_Extension::is_enabled() + ? wp_kses( + sprintf( + // translators: 1 opening HTML a tag to W3TC User Experience page, 2 closing HTML a tag. + __( + ' Settings can be found on the %1$sUser Experience page%2$s.', + 'w3-total-cache' + ), + '', + '' + ), + array( + 'a' => array( + 'href' => array(), + ), + ) + ) + : '' + ), + 'label_class' => 'w3tc_single_column', + 'pro' => true, + 'disabled' => ! Util_Environment::is_w3tc_pro( $config ) ? true : false, + 'show_learn_more' => true, + ) + ); + Util_Ui::config_item_extension_enabled( array( 'extension_id' => 'user-experience-preload-requests', diff --git a/UserExperience_PartyTown_Extension.php b/UserExperience_PartyTown_Extension.php new file mode 100644 index 000000000..ffc533af7 --- /dev/null +++ b/UserExperience_PartyTown_Extension.php @@ -0,0 +1,245 @@ +config = Dispatcher::config(); + } + + /** + * Runs User Experience PartyTown feature. + * + * @since X.X.X + * + * @return void + */ + public function run() { + add_action( 'w3tc_userexperience_page', array( $this, 'w3tc_userexperience_page' ), 12 ); + + /** + * This filter is documented in Generic_AdminActions_Default.php under the read_request method. + */ + add_filter( 'w3tc_config_key_descriptor', array( $this, 'w3tc_config_key_descriptor' ), 10, 2 ); + + if ( ! Util_Environment::is_w3tc_pro( $this->config ) ) { + $this->config->set_extension_active_frontend( 'user-experience-partytown', false ); + return; + } + + Util_Bus::add_ob_callback( 'partytown', array( $this, 'ob_callback' ) ); + + add_filter( 'w3tc_save_options', array( $this, 'w3tc_save_options' ), 10, 2 ); + } + + /** + * Processes the page content buffer to modify target CSS/JS. + * + * @since X.X.X + * + * @param string $buffer page content buffer. + * + * @return string + */ + public function ob_callback( $buffer ) { + if ( '' === $buffer || ! \W3TC\Util_Content::is_html_xml( $buffer ) ) { + return $buffer; + } + + $can_process = array( + 'enabled' => true, + 'buffer' => $buffer, + 'reason' => null, + ); + + $can_process = $this->can_process( $can_process ); + $can_process = apply_filters( 'w3tc_partytown_can_process', $can_process ); + + // set reject reason in comment. + if ( $can_process['enabled'] ) { + $reject_reason = ''; + } else { + $reject_reason = empty( $can_process['reason'] ) ? ' (not specified)' : ' (' . $can_process['reason'] . ')'; + } + + $buffer = str_replace( + '{w3tc_partytown_reject_reason}', + $reject_reason, + $buffer + ); + + // processing. + if ( ! $can_process['enabled'] ) { + return $buffer; + } + + $this->mutator = new UserExperience_PartyTown_Mutator( $this->config ); + + $buffer = $this->mutator->run( $buffer ); + + return $buffer; + } + + /** + * Checks if the request can be processed for PartyTown. + * + * @since X.X.X + * + * @param boolean $can_process flag representing if PartyTown can be executed. + * + * @return boolean + */ + private function can_process( $can_process ) { + if ( defined( 'WP_ADMIN' ) ) { + $can_process['enabled'] = false; + $can_process['reason'] = 'WP_ADMIN'; + + return $can_process; + } + + if ( defined( 'SHORTINIT' ) && SHORTINIT ) { + $can_process['enabled'] = false; + $can_process['reason'] = 'SHORTINIT'; + + return $can_process; + } + + if ( function_exists( 'is_feed' ) && is_feed() ) { + $can_process['enabled'] = false; + $can_process['reason'] = 'feed'; + + return $can_process; + } + + return $can_process; + } + + /** + * Adds PartyTown message to W3TC footer comment. + * + * @since X.X.X + * + * @param array $strings array of W3TC footer comments. + * + * @return array + */ + public function w3tc_footer_comment( $strings ) { + $strings[] = __( 'PartyTown', 'w3-total-cache' ) . '{w3tc_partytown_reject_reason}'; + return $strings; + } + + /** + * Renders the user experience PartyTown settings page. + * + * @since X.X.X + * + * @return void + */ + public function w3tc_userexperience_page() { + include __DIR__ . '/UserExperience_PartyTown_Page_View.php'; + } + + /** + * Specify config key typing for fields that need it. + * + * @since X.X.X + * + * @param mixed $descriptor Descriptor. + * @param mixed $key Compound key array. + * + * @return array + */ + public function w3tc_config_key_descriptor( $descriptor, $key ) { + if ( is_array( $key ) && 'user-experience-partytown.includes' === implode( '.', $key ) ) { + $descriptor = array( 'type' => 'array' ); + } + + return $descriptor; + } + + /** + * Performs actions on save. + * + * @since X.X.X + * + * @param array $data Array of save data. + * @param array $page String page value. + * + * @return array + */ + public function w3tc_save_options( $data, $page ) { + if ( 'w3tc_userexperience' === $page ) { + $new_config =& $data['new_config']; + $old_config =& $data['old_config']; + + $old_partytown_includes = $old_config->get_array( array( 'user-experience-partytown', 'includes' ) ); + $new_partytown_includes = $new_config->get_array( array( 'user-experience-partytown', 'includes' ) ); + + if ( $new_partytown_includes !== $old_partytown_includes ) { + $minify_enabled = $new_config->get_boolean( 'minify.enabled' ); + $pgcache_enabled = $new_config->get_boolean( 'pgcache.enabled' ); + if ( $minify_enabled || $pgcache_enabled ) { + $state = Dispatcher::config_state(); + if ( $minify_enabled ) { + $state->set( 'minify.show_note.need_flush', true ); + } + if ( $pgcache_enabled ) { + $state->set( 'common.show_note.flush_posts_needed', true ); + } + $state->save(); + } + } + } + + return $data; + } + + /** + * Gets the enabled status of the extension. + * + * @since 2.5.1 + * + * @return bool + */ + public static function is_enabled() { + $config = Dispatcher::config(); + $extensions_active = $config->get_array( 'extensions.active' ); + return Util_Environment::is_w3tc_pro( $config ) && array_key_exists( 'user-experience-partytown', $extensions_active ); + } +} + +$o = new UserExperience_PartyTown_Extension(); +$o->run(); diff --git a/UserExperience_PartyTown_Mutator.php b/UserExperience_PartyTown_Mutator.php new file mode 100644 index 000000000..75351c9a0 --- /dev/null +++ b/UserExperience_PartyTown_Mutator.php @@ -0,0 +1,137 @@ +config = $config; + } + + /** + * Runs User Experience PartyTown Mutator. + * + * @since X.X.X + * + * @param string $buffer Buffer string containing browser output. + * + * @return string + */ + public function run( $buffer ) { + $r = apply_filters( + 'w3tc_partytown_mutator_before', + array( + 'buffer' => $buffer, + ) + ); + + $this->buffer = $r['buffer']; + + // Sets includes whose matches will be stripped site-wide. + $this->includes = $this->config->get_array( + array( + 'user-experience-partytown', + 'includes', + ) + ); + + $this->buffer = preg_replace_callback( + '~(]+href[^>]+>)|(]+src[^>]+>)~is', + array( $this, 'modify_content' ), + $this->buffer + ); + + return $this->buffer; + } + + /** + * Modifies matched link/script tag from HTML content. + * + * @since X.X.X + * + * @param array $matches array of matched CSS/JS entries. + * + * @return string + */ + public function modify_content( $matches ) { + $content = $matches[0]; + + // Early return if not the main query or content not a match. + if ( ! is_main_query() || ! $this->is_content_included( $content ) ) { + return $content; + } + + // Check if it's a script tag and type="text/partytown" is not already present. + if ( strpos( $content, ')/', ')~is', + '~(]+src[^>]+>)~is', array( $this, 'modify_content' ), $this->buffer ); @@ -100,13 +100,18 @@ public function modify_content( $matches ) { $content = $matches[0]; // Early return if not the main query or content not a match. - if ( ! is_main_query() || ! $this->is_content_included( $content ) ) { + if ( ! $this->is_content_included( $content ) ) { return $content; } // Check if it's a script tag and type="text/partytown" is not already present. if ( strpos( $content, ')/', ' + * + * ``` + * + * The `nonce` property should be generated by the server, and it should be unique + * for each request. You can leave a placeholder, as shown in the above example, + * to facilitate replacement through a regular expression on the server side. + * For instance, you can use the following code: + * + * ```js + * html.replace(/THIS_SHOULD_BE_REPLACED/g, nonce); + * ``` + */ + nonce?: string; +} + +/** + * A forward property to patch on `window`. The forward config property is an string, + * representing the call to forward, such as `dataLayer.push` or `fbq`. + * + * https://partytown.builder.io/forwarding-events + * + * @public + */ +export declare type PartytownForwardProperty = string | PartytownForwardPropertyWithSettings; + +/** + * @public + */ +export declare type PartytownForwardPropertySettings = { + preserveBehavior?: boolean; +}; + +/** + * @public + */ +export declare type PartytownForwardPropertyWithSettings = [string, PartytownForwardPropertySettings?]; + +/** + * Function that returns the Partytown snippet as a string, which can be + * used as the innerHTML of the inlined Partytown script in the head. + * + * @public + */ +export declare const partytownSnippet: (config?: PartytownConfig) => string; + +/** + * @public + */ +export declare type ResolveUrlType = 'fetch' | 'xhr' | 'script' | 'iframe' | 'image'; + +/** + * The `type` attribute for Partytown scripts, which does two things: + * + * 1. Prevents the ` + * + * ``` + * + * The `nonce` property should be generated by the server, and it should be unique + * for each request. You can leave a placeholder, as shown in the above example, + * to facilitate replacement through a regular expression on the server side. + * For instance, you can use the following code: + * + * ```js + * html.replace(/THIS_SHOULD_BE_REPLACED/g, nonce); + * ``` + */ + nonce?: string; +} + +/** + * A forward property to patch on `window`. The forward config property is an string, + * representing the call to forward, such as `dataLayer.push` or `fbq`. + * + * https://partytown.builder.io/forwarding-events + * + * @public + */ +declare type PartytownForwardProperty = string | PartytownForwardPropertyWithSettings; + +/** + * @public + */ +declare type PartytownForwardPropertySettings = { + preserveBehavior?: boolean; +}; + +/** + * @public + */ +declare type PartytownForwardPropertyWithSettings = [string, PartytownForwardPropertySettings?]; + +/** + * Props for ``, which extends the Partytown Config. + * + * https://github.com/BuilderIO/partytown#config + * + * @public + */ +export declare interface PartytownProps extends PartytownConfig { +} + +/** + * @public + */ +declare type ResolveUrlType = 'fetch' | 'xhr' | 'script' | 'iframe' | 'image'; + +/** + * @public + */ +declare type SendBeaconParameters = Pick; + +/** + * @public + */ +declare type SetHook = (opts: SetHookOptions) => any; + +/** + * @public + */ +declare interface SetHookOptions extends HookOptions { + value: any; + prevent: Symbol; +} + +declare type WinId = string; + +declare const WinIdKey: unique symbol; + +declare interface WorkerInstance { + [WinIdKey]: WinId; + [InstanceIdKey]: InstanceId; + [ApplyPathKey]: string[]; + [InstanceDataKey]: string | undefined; + [NamespaceKey]: string | undefined; + [InstanceStateKey]: { + [key: string]: any; + }; +} + +export { } diff --git a/lib/PartyTown/react/index.mjs b/lib/PartyTown/react/index.mjs new file mode 100644 index 000000000..0ce18cf96 --- /dev/null +++ b/lib/PartyTown/react/index.mjs @@ -0,0 +1,42 @@ +import React from 'react'; +import { partytownSnippet } from '../integration/index.mjs'; + +/** + * The React `` component should be placed within the `` + * of the document. This component should work for SSR/SSG only HTML + * (static HTML without javascript), clientside javascript only + * (entire React app is build with clientside javascript), + * and both SSR/SSG HTML that's then hydrated by the client. + * + * @public + */ +const Partytown = ({ nonce, ...props } = {}) => { + // purposely not using useState() or useEffect() so this component + // can also work as a React Server Component + // this check is only be done on the client, and skipped over on the server + if (typeof document !== 'undefined' && !document._partytown) { + if (!document.querySelector('script[data-partytown]')) { + // the append script to document code should only run on the client + // and only if the SSR'd script doesn't already exist within the document. + // If the SSR'd script isn't found in the document, then this + // must be a clientside only render. Append the partytown script + // to the . + const scriptElm = document.createElement('script'); + scriptElm.dataset.partytown = ''; + scriptElm.innerHTML = partytownSnippet(props); + scriptElm.nonce = nonce; + document.head.appendChild(scriptElm); + } + // should only append this script once per document, and is not dynamic + document._partytown = true; + } + // `dangerouslySetInnerHTML` only works for scripts rendered as HTML from SSR. + // The added code will set the [type="data-pt-script"] attribute to the SSR rendered + // )~is', + '~(]*>.*?)~is', array( $this, 'modify_content' ), $this->buffer ); From 1c9870dcf84b42cdd443b409b23f41039d72c509 Mon Sep 17 00:00:00 2001 From: jacobd91 Date: Wed, 13 Nov 2024 13:56:49 -0500 Subject: [PATCH 09/10] Fixed issue with partytown.js path generation for sites configured as a sub-directory. --- UserExperience_PartyTown_Extension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UserExperience_PartyTown_Extension.php b/UserExperience_PartyTown_Extension.php index b3fc8f113..70f9e8f04 100644 --- a/UserExperience_PartyTown_Extension.php +++ b/UserExperience_PartyTown_Extension.php @@ -75,11 +75,11 @@ public function run() { * @return void */ public function w3tc_enqueue_partytown() { - $party_path = wp_make_link_relative( plugins_url( 'lib/PartyTown/lib/', __FILE__ ) ); + $party_path = wp_make_link_relative( plugin_dir_url( __FILE__ ) . 'lib/PartyTown/lib/' ); $debug = $this->config->get_boolean( array( 'user-experience-partytown', 'debug' ) ) ? 'true' : 'false'; $timeout = $this->config->get_integer( array( 'user-experience-partytown', 'timeout' ) ) ?? 5000; $worker_concurrency = $this->config->get_integer( array( 'user-experience-partytown', 'workers' ) ) ?? 5; - +Util_Debug::debug('party_path',$party_path); wp_register_script( 'partytown', $party_path . 'partytown.js', array(), W3TC_VERSION, true ); // Preload Partytown if enabled. From 65aafaaad3835edb0969f072eeadadabcd8c5f8d Mon Sep 17 00:00:00 2001 From: jacobd91 Date: Fri, 22 Nov 2024 11:16:54 -0500 Subject: [PATCH 10/10] Fixed subdir path issue. Removed debug code and unused code. --- UserExperience_PartyTown_Extension.php | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/UserExperience_PartyTown_Extension.php b/UserExperience_PartyTown_Extension.php index 70f9e8f04..2eed7d030 100644 --- a/UserExperience_PartyTown_Extension.php +++ b/UserExperience_PartyTown_Extension.php @@ -75,12 +75,13 @@ public function run() { * @return void */ public function w3tc_enqueue_partytown() { - $party_path = wp_make_link_relative( plugin_dir_url( __FILE__ ) . 'lib/PartyTown/lib/' ); + $script_path = plugins_url( 'lib/PartyTown/lib/partytown.js', __FILE__ ); + $party_path = wp_make_link_relative( plugins_url( 'lib/PartyTown/lib/', __FILE__ ) ); $debug = $this->config->get_boolean( array( 'user-experience-partytown', 'debug' ) ) ? 'true' : 'false'; $timeout = $this->config->get_integer( array( 'user-experience-partytown', 'timeout' ) ) ?? 5000; $worker_concurrency = $this->config->get_integer( array( 'user-experience-partytown', 'workers' ) ) ?? 5; -Util_Debug::debug('party_path',$party_path); - wp_register_script( 'partytown', $party_path . 'partytown.js', array(), W3TC_VERSION, true ); + + wp_register_script( 'partytown', $script_path, array(), W3TC_VERSION, true ); // Preload Partytown if enabled. if ( $this->config->get_boolean( array( 'user-experience-partytown', 'preload' ) ) ) { @@ -95,17 +96,6 @@ public function w3tc_enqueue_partytown() { timeout: {$timeout}, workerConcurrency: {$worker_concurrency}, }; - - // This was added due to the fact it's not applying on initial page load. The iframe added by partytown.js is removed afterwards due to no service worker being available. - /* - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('controllerchange', () => { - // This event is triggered when the service worker is installed and takes control - console.log('Service worker installed and controlling the page. Reloading...'); - window.location.reload(); - }); - } - */ "; // Add inline script to configure Partytown.