Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 366
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Code_Block
0.00% covered (danger)
0.00%
0 / 365
0.00% covered (danger)
0.00%
0 / 11
4422
0.00% covered (danger)
0.00%
0 / 1
 should_load_block
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 assets_available
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
156
 setup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 register_editor_assets
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
6
 enqueue_view_assets
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 get_module_asset_data
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 override_block_style
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 register_block_type_args
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 1
110
 enqueue_editor_assets
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 render_block
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 1
552
 after_setup_theme
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Code Block
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack;
11
12require_once __DIR__ . '/class-code-block-html-replacer.php';
13
14use WP_Theme_JSON;
15
16/**
17 * Code Block class.
18 *
19 * Contains necessary functionality for the Code Block.
20 */
21abstract class Code_Block {
22    const MODULE_PREFIX = '@a8cCodeBlock/';
23
24    /**
25     * Language names for display.
26     *
27     * @var array<string, string>
28     */
29    public static $language_name_rewrites = array(
30        'Brainfuck' => 'Brainf***',
31    );
32
33    /**
34     * Filterable check for whether the block should be available.
35     *
36     * @return bool
37     */
38    private static function should_load_block(): bool {
39        $filtered_value = apply_filters( 'jetpack_mu_wpcom_should_load_code_block', true );
40        return \is_bool( $filtered_value ) ? $filtered_value : false;
41    }
42
43    /**
44     * Check if the build assets required for the code block are available.
45     *
46     * @return bool
47     */
48    private static function assets_available(): bool {
49        static $result = null;
50        if ( null === $result ) {
51            $block_definition_asset_readable = is_readable( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-block-definition/wpcom-blocks-code-block-definition.asset.php' );
52            $module_asset_readable           = is_readable( Jetpack_Mu_Wpcom::BASE_DIR . 'build-module/assets.php' );
53            $editor_style_asset_readable     = is_readable( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-editor-style/wpcom-blocks-code-editor-style.asset.php' );
54            $style_asset_readable            = is_readable( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-style/wpcom-blocks-code-style.asset.php' );
55
56            $result = $block_definition_asset_readable && $module_asset_readable && $editor_style_asset_readable && $style_asset_readable;
57            if ( ! $result && \defined( 'IS_WPCOM' ) && IS_WPCOM ) {
58                require_once WP_CONTENT_DIR . '/lib/log2logstash/log2logstash.php';
59                $data = array(
60                    'blog_id' => get_current_blog_id(),
61                );
62
63                $message = 'Missing build asset files.';
64                if ( ! $block_definition_asset_readable ) {
65                    $message .= ' Block definition asset file is missing `' . Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-block-definition/wpcom-blocks-code-block-definition.asset.php`.';
66                }
67                if ( ! $module_asset_readable ) {
68                    $message .= ' Module asset file is missing `' . Jetpack_Mu_Wpcom::BASE_DIR . 'build-module/assets.php`.';
69                }
70                if ( ! $editor_style_asset_readable ) {
71                    $message .= ' Editor style asset file is missing `' . Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-editor-style/wpcom-blocks-code-editor-style.asset.php`.';
72                }
73                if ( ! $style_asset_readable ) {
74                    $message .= ' Style asset file is missing `' . Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-style/wpcom-blocks-code-style.asset.php`.';
75                }
76
77                log2logstash(
78                    array(
79                        'feature' => 'jetpack-enhanced-code-block',
80                        'message' => $message,
81                        'extra'   => wp_json_encode( $data, JSON_UNESCAPED_SLASHES ),
82                    )
83                );
84            }
85        }
86        return $result;
87    }
88
89    /**
90     * Set up the block.
91     */
92    public static function setup() {
93        if (
94            ! self::should_load_block() ||
95            ! self::assets_available()
96        ) {
97            return;
98        }
99
100        add_action( 'after_setup_theme', array( __CLASS__, 'after_setup_theme' ), 100 );
101        add_filter( 'register_block_type_args', array( __CLASS__, 'register_block_type_args' ), 150, 2 );
102    }
103
104    /**
105     * Registration of editor scripts, styles, and modules.
106     *
107     * Called lazily when editor assets are needed, not on every request.
108     */
109    private static function register_editor_assets() {
110        static $done = false;
111        if ( $done ) {
112            return;
113        }
114        $done = true;
115
116        $block_definition_asset_file  = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-block-definition/wpcom-blocks-code-block-definition.asset.php';
117        $jetpack_wpcom_modules_assets = self::get_module_asset_data();
118
119        // The block definition must contain the script dependencies that the edit function script module requires.
120        // Append static dependency list here. Some duplicates may appear, that should be harmless.
121        $block_definition_dependencies = array_merge(
122            $block_definition_asset_file['dependencies'],
123            array(
124                'react',
125                'wp-block-editor',
126                'wp-blocks',
127                'wp-components',
128                'wp-data',
129                'wp-editor',
130                'wp-i18n',
131                'wp-keycodes',
132            )
133        );
134
135        wp_register_script(
136            self::MODULE_PREFIX . 'block-definition',
137            plugins_url( 'build/wpcom-blocks-code-block-definition/wpcom-blocks-code-block-definition.js', Jetpack_Mu_Wpcom::BASE_FILE ),
138            $block_definition_dependencies,
139            $block_definition_asset_file['version'],
140            array( 'in_footer' => true )
141        );
142
143        wp_register_script_module(
144            self::MODULE_PREFIX . 'block-edit-function',
145            plugins_url( 'build-module/wpcom-blocks-code-edit-function/wpcom-blocks-code-edit-function.js', Jetpack_Mu_Wpcom::BASE_FILE ),
146            $jetpack_wpcom_modules_assets['wpcom-blocks-code-edit-function/wpcom-blocks-code-edit-function.js']['dependencies'],
147            $jetpack_wpcom_modules_assets['wpcom-blocks-code-edit-function/wpcom-blocks-code-edit-function.js']['version']
148        );
149
150        $editor_style_asset_file = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-editor-style/wpcom-blocks-code-editor-style.asset.php';
151        wp_register_style(
152            self::MODULE_PREFIX . 'editor',
153            plugins_url( 'build/wpcom-blocks-code-editor-style/wpcom-blocks-code-editor-style.css', Jetpack_Mu_Wpcom::BASE_FILE ),
154            array(),
155            $editor_style_asset_file['version']
156        );
157
158        $block_worker_url     = plugins_url( 'build-module/wpcom-blocks-code-worker/wpcom-blocks-code-worker.js', Jetpack_Mu_Wpcom::BASE_FILE );
159        $block_worker_version = $jetpack_wpcom_modules_assets['wpcom-blocks-code-worker/wpcom-blocks-code-worker.js']['version'];
160        add_filter(
161            'script_module_data_' . self::MODULE_PREFIX . 'block-edit-function',
162            function ( array $data ) use ( $block_worker_url, $block_worker_version ): array {
163                $data['workerUrl']     = $block_worker_url;
164                $data['workerVersion'] = $block_worker_version;
165                return $data;
166            }
167        );
168    }
169
170    /**
171     * Enqueue view script module.
172     */
173    private static function enqueue_view_assets() {
174        static $done = false;
175        if ( $done ) {
176            return;
177        }
178        $done = true;
179
180        $jetpack_wpcom_modules_assets = self::get_module_asset_data();
181        wp_enqueue_script_module(
182            self::MODULE_PREFIX . 'block-front',
183            plugins_url( 'build-module/wpcom-blocks-code-block-front/wpcom-blocks-code-block-front.js', Jetpack_Mu_Wpcom::BASE_FILE ),
184            $jetpack_wpcom_modules_assets['wpcom-blocks-code-block-front/wpcom-blocks-code-block-front.js']['dependencies'],
185            $jetpack_wpcom_modules_assets['wpcom-blocks-code-block-front/wpcom-blocks-code-block-front.js']['version']
186        );
187    }
188
189    /**
190     * Get the module asset data.
191     *
192     * @return array
193     */
194    private static function get_module_asset_data() {
195        static $jetpack_wpcom_modules_assets = null;
196        if ( null === $jetpack_wpcom_modules_assets ) {
197            $jetpack_wpcom_modules_assets = include Jetpack_Mu_Wpcom::BASE_DIR . 'build-module/assets.php';
198        }
199        return $jetpack_wpcom_modules_assets;
200    }
201
202    /**
203     * Set up the block view styles.
204     *
205     * Core's `wp-block-code` handle must be used in order to work with the global styles system.
206     * It relies on checking whether this style is enqueued to add the associated global styles to the page.
207     *
208     * Instead of using a different style handle, replace the registered style for `wp-block-code`.
209     *
210     * @see https://core.trac.wordpress.org/browser/tags/6.8.3/src/wp-includes/global-styles-and-settings.php#L322
211     *
212     * @global \WP_Styles $wp_styles
213     */
214    public static function override_block_style() {
215        global $wp_styles;
216
217        $src = plugins_url( 'build/wpcom-blocks-code-style/wpcom-blocks-code-style.css', Jetpack_Mu_Wpcom::BASE_FILE );
218        // Skip work if style is registered as desired.
219        if ( isset( $wp_styles->registered['wp-block-code'] ) && $wp_styles->registered['wp-block-code']->src === $src ) {
220            return;
221        }
222
223        $was_enqueued = wp_style_is( 'wp-block-code', 'enqueued' );
224        wp_deregister_style( 'wp-block-code' );
225
226        $style_asset_file = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-blocks-code-style/wpcom-blocks-code-style.asset.php';
227        $version          = $style_asset_file['version'];
228
229        wp_register_style(
230            'wp-block-code',
231            $src,
232            array(),
233            $version
234        );
235        if ( $was_enqueued ) {
236            wp_enqueue_style( 'wp-block-code' );
237        }
238    }
239
240    /**
241     * Filter for block registration to modify the core/code block.
242     *
243     * @param array  $args The block type arguments.
244     * @param string $block_type The block type name.
245     *
246     * @return array The modified block type arguments.
247     */
248    public static function register_block_type_args( array $args, string $block_type ): array {
249        if (
250            'core/code' !== $block_type
251
252            // In some cases the block may not include the content attribute.
253            // Only perform enhancement on the _full_, expected block.
254            || ! isset( $args['attributes']['content'] )
255
256            // Skip if the block is already processed.
257            || $args['render_callback'] === array( __CLASS__, 'render_block' )
258        ) {
259            return $args;
260        }
261
262        // Register assets and hooks only when overriding the block.
263        self::register_editor_assets();
264        self::override_block_style();
265
266        static $hooks_registered = false;
267        if ( ! $hooks_registered ) {
268            $hooks_registered = true;
269            add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_editor_assets' ) );
270            add_action(
271                'wp_enqueue_scripts',
272                function () {
273                    if ( wp_should_load_block_editor_scripts_and_styles() ) {
274                        self::enqueue_editor_assets();
275                    }
276
277                    /*
278                     * Core should handle this, but Script Module assets are not currently handled.
279                     */
280                    if (
281                        ! wp_should_load_block_assets_on_demand()
282                        && has_block( 'core/code' )
283                    ) {
284                        self::enqueue_view_assets();
285                    }
286                }
287            );
288        }
289
290        $args['render_callback']       = array( __CLASS__, 'render_block' );
291        $args['editor_script_handles'] = array_merge( array( self::MODULE_PREFIX . 'block-definition' ), $args['editor_script_handles'] ?? array() );
292
293        $args['editor_style_handles'] = array( self::MODULE_PREFIX . 'editor' );
294        $args['style_handles']        = array( 'wp-block-code' );
295        unset( $args['view_style_handles'] );
296
297        /*
298         * Add selectors for typography targetting problematic elements.
299         *
300         * - The descendent PRE element needs font-family styling like this to ensure it receives
301         *   user agent default styling like monospace, as well as PRE element styling from themes,
302         *   and can also be styled by global styles and theme.json.
303         */
304        $args['selectors'] = array(
305            'root'       => '.wp-block-code',
306            'typography' => array(
307
308                /*
309                 * These are experimental at the moment. The camelCase form appears to be used, but
310                 * it's possible the kebab-case currently used in documentation may be used when
311                 * they're stabilized.
312                 */
313                'fontFamily'  => '.wp-block-code, .wp-block-code pre',
314                'font-family' => '.wp-block-code, .wp-block-code pre',
315            ),
316        );
317
318        /**
319         * Typography support:
320         *
321         * Line height and letter spacing may be problematic for rendering in the editor,
322         * line numbers, etc. Disable them.
323         *
324         * Text decoration is problematic with additional UI elements like buttons and
325         * line numbers. Disable.
326         */
327        if ( isset( $args['supports']['typography'] ) && \is_array( $args['supports']['typography'] ) ) {
328            $args['supports']['typography']['lineHeight']                   = false;
329            $args['supports']['typography']['__experimentalLetterSpacing']  = false;
330            $args['supports']['typography']['letterSpacing']                = false;
331            $args['supports']['typography']['__experimentalTextDecoration'] = false;
332            $args['supports']['typography']['textDecoration']               = false;
333        } else {
334            $args['supports']['typography'] = array(
335                'fontSize'                      => true,
336                'lineHeight'                    => false,
337
338                // Currently experimental, but include likely stable forms as well.
339                '__experimentalFontFamily'      => true,
340                '__experimentalFontWeight'      => true,
341                '__experimentalFontStyle'       => true,
342                '__experimentalTextTransform'   => true,
343                'fontFamily'                    => true,
344                'fontWeight'                    => true,
345                'fontStyle'                     => true,
346                'textTransform'                 => true,
347
348                '__experimentalDefaultControls' => array(
349                    'fontSize' => true,
350                ),
351                'defaultControls'               => array(
352                    'fontSize' => true,
353                ),
354            );
355        }
356
357        $args['attributes'] = array(
358            // Content attribute is preserved for compatibility with the core/code block and transforms.
359            'content'                 => $args['attributes']['content'],
360            'tokenizedLines'          => array(
361                'type'    => 'array',
362                'default' =>
363                array(),
364            ),
365            'language'                => array(
366                'type'    => 'string',
367                'default' => '',
368            ),
369            'languageConfidence'      => array(
370                'type'    => 'string',
371                'default' => 'unknown',
372            ),
373            'triggerCodeUpdate'       => array(
374                'type'    => 'boolean',
375                'default' => false,
376            ),
377            'showCopyButton'          => array(
378                'type'    => 'boolean',
379                'default' => false,
380            ),
381            'showLanguageName'        => array(
382                'type'    => 'boolean',
383                'default' => false,
384            ),
385            'showLineNumbers'         => array(
386                'type'    => 'boolean',
387                'default' => false,
388            ),
389            'lineNumbersStartAt'      => array(
390                'type'    => 'number',
391                'default' => 1,
392            ),
393            'filename'                => array(
394                'type'    => 'string',
395                'default' => '',
396            ),
397            'colorComment'            => array(
398                'type' => 'string',
399            ),
400            'colorKeyword'            => array(
401                'type' => 'string',
402            ),
403            'colorBoolean'            => array(
404                'type' => 'string',
405            ),
406            'colorLiteral'            => array(
407                'type' => 'string',
408            ),
409            'colorString'             => array(
410                'type' => 'string',
411            ),
412            'colorSpecialString'      => array(
413                'type' => 'string',
414            ),
415            'colorMacroName'          => array(
416                'type' => 'string',
417            ),
418            'colorVariableDefinition' => array(
419                'type' => 'string',
420            ),
421            'colorTypeName'           => array(
422                'type' => 'string',
423            ),
424            'colorClassName'          => array(
425                'type' => 'string',
426            ),
427            'colorInvalid'            => array(
428                'type' => 'string',
429            ),
430        );
431        $args['textdomain'] = 'jetpack-mu-wpcom';
432
433        return $args;
434    }
435
436    /**
437     * Enqueue plugin assets necessary for the block editor.
438     */
439    public static function enqueue_editor_assets() {
440        static $done = false;
441        if ( $done ) {
442            return;
443        }
444        $done = true;
445
446        /*
447         * The code block registration script depends on some script modules.
448         * This "dummy" module ensures those dependencies are available.
449         */
450        wp_enqueue_script_module(
451            self::MODULE_PREFIX . 'dummy',
452            plugins_url( 'empty.js', __FILE__ ),
453            array(
454                array(
455                    'import' => 'dynamic',
456                    'id'     => self::MODULE_PREFIX . 'block-edit-function',
457                ),
458            ),
459            '0.0.0' // This script never needs to be cache busted. It will never change.
460        );
461    }
462
463    /**
464     * Render the block.
465     *
466     * @param array  $attributes The block attributes.
467     * @param string $content The block content.
468     */
469    public static function render_block( array $attributes, string $content ): string {
470        if ( empty( $attributes['tokenizedLines'] ) || ! \is_array( $attributes['tokenizedLines'] ) ) {
471            return $content;
472        }
473
474        $processed_content = Code_Block_HTML_Replacer::get_updated_html_with_replaced_content( $content, $attributes['tokenizedLines'], $attributes['language'] );
475        if ( null === $processed_content ) {
476            return $content;
477        }
478        list( $code_string, $replaced_content ) = $processed_content;
479
480        $extra_attrs      = array();
481        $style_properties = array();
482
483        if ( $attributes['showCopyButton'] ?? false ) {
484            self::enqueue_view_assets();
485        }
486
487        $show_line_numbers = $attributes['showLineNumbers'] ?? false;
488        if ( $show_line_numbers ) {
489            $extra_attrs['class']  = 'show-line-numbers';
490            $line_numbers_start_at = isset( $attributes['lineNumbersStartAt'] )
491                ? max( 0, min( 10000, (int) $attributes['lineNumbersStartAt'] ) )
492                : 1;
493
494            $max_line_number_width = floor(
495                log10( $line_numbers_start_at + \count( $attributes['tokenizedLines'] ) - 1 )
496            ) + 1;
497
498            if ( $line_numbers_start_at !== 1 ) {
499                $style_properties[] = '--line-numbers-start-at: ' . $line_numbers_start_at;
500            }
501            $style_properties[] = '--line-number-gutter-width: ' . $max_line_number_width . 'ch';
502        }
503
504        $color_attributes = array(
505            'colorComment',
506            'colorKeyword',
507            'colorBoolean',
508            'colorLiteral',
509            'colorString',
510            'colorSpecialString',
511            'colorMacroName',
512            'colorVariableDefinition',
513            'colorTypeName',
514            'colorClassName',
515            'colorInvalid',
516        );
517        foreach ( $color_attributes as $color_attr ) {
518            if ( ! empty( $attributes[ $color_attr ] ) ) {
519                $style_properties[] = "--{$color_attr}{$attributes[ $color_attr ]}";
520            }
521        }
522
523        if ( isset( $attributes['backgroundColor'] ) ) {
524            $style_properties[] = "--colorBackground: var( --wp--preset--color--{$attributes['backgroundColor']} )";
525        } elseif ( isset( $attributes['style']['color']['background'] ) ) {
526            $style_properties[] = "--colorBackground: {$attributes['style']['color']['background']}";
527        }
528
529        if ( isset( $attributes['textColor'] ) ) {
530            $style_properties[] = "--colorText: var( --wp--preset--color--{$attributes['textColor']} )";
531        } elseif ( isset( $attributes['style']['color']['text'] ) ) {
532            $style_properties[] = "--colorText: {$attributes['style']['color']['text']}";
533        }
534
535        if ( ! empty( $style_properties ) ) {
536            $extra_attrs['style'] = implode( '; ', $style_properties ) . ';';
537        }
538
539        $attrs = get_block_wrapper_attributes( $extra_attrs );
540
541        $filename_html = ( ! empty( $attributes['filename'] ) )
542            ? \sprintf( '<span class="a8c/code__filename">%s</span>', esc_html( $attributes['filename'] ) )
543            : '';
544
545        $copy_html = ( $attributes['showCopyButton'] ?? false )
546            ? \sprintf(
547                '<button class="%s element-button a8c/code__btn-copy" type="button" data-copy-text="%s" hidden>%s</button>',
548                WP_Theme_JSON::get_element_class_name( 'button' ),
549                esc_attr( $code_string ),
550                esc_html__( 'Copy', 'jetpack-mu-wpcom' )
551            )
552            : '';
553
554        $language_html = '';
555        if ( $attributes['showLanguageName'] ?? false ) {
556            $language_text = empty( $attributes['language'] )
557                ? __( 'Plain text', 'jetpack-mu-wpcom' )
558                : $attributes['language'];
559            $language_text = self::$language_name_rewrites[ $language_text ] ?? $language_text;
560            $language_html = \sprintf(
561                '<span>%s</span>',
562                esc_html( $language_text )
563            );
564        }
565
566        $header_right_html = ( $copy_html || $language_html )
567            ? "<div class=\"a8c/code__header-right\">{$copy_html}{$language_html}</div>"
568            : '';
569        $header_html       = ( $filename_html || $header_right_html )
570            ? "\n\t<div class=\"a8c/code__header\">{$filename_html}{$header_right_html}</div>"
571            : '';
572
573        $output = <<<HTML
574<div {$attrs}>{$header_html}
575    <div class="cm-editor">
576        <div class="cm-scroller">
577            {$replaced_content}
578        </div>
579    </div>
580</div>
581HTML;
582
583        return $output;
584    }
585
586    /**
587     * Hook to allow the dummy script module to inject its dependencies into the importmap.
588     *
589     * Create an opportunity between printing the importmap and printing modules
590     * in order to prevent printing the dummy module.
591     *
592     * This is not essential, but does save some HTML on the page and a network request.
593     * The dummy module is only used to signal that some additional modules
594     * should be included in the importmap.
595     */
596    public static function after_setup_theme() {
597        foreach ( array( 'wp_head', 'wp_footer', 'admin_print_footer_scripts' ) as $hook ) {
598            /*
599             * Script module actions are expected in this order:
600             *
601             * - WP_Script_Modules::print_import_map
602             * - WP_Script_Modules::print_script_module_preloads
603             * - WP_Script_Modules::print_enqueued_script_modules
604             *
605             * Attempt to remove actions starting from the end to that if a removal fails,
606             * the action can be restored to the expected position by adding it again.
607             */
608            if ( ! remove_action( $hook, array( wp_script_modules(), 'print_script_module_preloads' ) ) ) {
609                continue;
610            }
611            if ( ! remove_action( $hook, array( wp_script_modules(), 'print_enqueued_script_modules' ) ) ) {
612                add_action( $hook, array( wp_script_modules(), 'print_script_module_preloads' ) );
613                continue;
614            }
615
616            add_action(
617                $hook,
618                function () {
619                    wp_script_modules()->dequeue( self::MODULE_PREFIX . 'dummy' );
620                },
621                15
622            );
623            add_action( $hook, array( wp_script_modules(), 'print_enqueued_script_modules' ), 20 );
624            add_action( $hook, array( wp_script_modules(), 'print_script_module_preloads' ), 20 );
625            add_action(
626                $hook,
627                function () {
628                    wp_script_modules()->enqueue( self::MODULE_PREFIX . 'dummy' );
629                },
630                25
631            );
632        }
633    }
634}