Proposal: Server to client data sharing for Script Modules

Abstract

Script Modules were introduced in WordPress 6.5. wp_add_inline_script is often used to initialize or make data available to Scripts. Feedback on Script Modules and explorations suggest this would be a useful feature for Script Modules, but nothing exists at this time. This post will describe the problem in detail and propose a solution. The proposed solution consists of three main points:

  • A new filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. runs for each Script Module that is enqueued or in the dependency graph. This filter allows arbitrary data to be associated with a given Script Module and added to the rendered page.
  • Script Module data is embedded in the page as JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. in a <script type="application/json"> tagtag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.).
  • Script Modules are responsible for reading the data and performing their own initialization.
Read more: Proposal: Server to client data sharing for Script Modules

The feedback period will end 2024-05-24 (Friday, May 24). Please provide any feedback before then.

This post will use “scripts” to refer to WP Scripts and “modules” to refer to WP Script Modules.

Scripts or modules?

Most JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. for WordPress is probably using scripts unless it was specifically compiled as a module. Modern JavaScript is often authored using import apiFetch from '@wordpress/api-fetch', which is module syntax. Until very recently, WordPress build tooling has always removed the module syntax and compiled a script.

Only the most recent WordPress version 6.5 introduced support for modules. WordPress CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. includes exactly two modules to expose functionality at this time: @wordpress/interactivity and @wordpress/interactivity-router. JavaScript that uses the Interactivity APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. is probably a module.

This post will talk about a common script dependency, wp-api-fetch. The @wordpress/api-fetch module mentioned in this post is hypothetical; it does not exist at this time.

A note about modules

It’s important to know a few things about scripts versus modules in the context of this post. A few key differences:

  • Scripts and their dependencies will be executed as the page is parsed even if a dependency is never directly used.
  • Script “exports” are attached to the window object, e.g. wp-api-fetch is available as window.wp.apiFetch.
  • Modules will execute after the page has finished parsing.
  • Module dependencies can be loaded on demand.
  • Modules have true encapsulation, using import and export to share values.
More about scripts and modules…

Scripts that are either enqueued or are dependencies of enqueued scripts will be added to the page as script tags, like <script src="path/to/script.js">. The browser will fetch and execute these scripts before it continues to parse the page. There are attributes like async and defer that can change how the browser executes scripts.

Modules that are enqueued will appear on the page as script tags of type module, like <script src="path/to/module.js" type="module">. Processing of modules is deferred, they will be executed after the whole document has been parsed. The async attribute will cause a module to execute in parallel as the document is parsed. WordPress Script Modules do not use async at this time.

Modules that are in the dependency graph of enqueued modules will appear in an importmap. This is a mapping of names to URLs so that a browser knows what module to fetch when it sees an import. It’s what associates the module used in a statement like import "a-module"; with a URLURL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org { "imports": { "a-module": "path/to/a-module.js" } }.

The problem

Scripts often require some initialization or other data to work correctly in WordPress. The wp-api-fetch script is a good example, it requires a significant amount of configuration. For example, Core uses the following inline script to initialize wp-api-fetch so it can send requests to the appropriate place:

$scripts->add_inline_script(
	'wp-api-fetch',
	sprintf(
		'wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "%s" ) );',
		sanitize_url( get_rest_url() )
	),
	'after'
);

This snippet uses PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher to create a string of JavaScript code with some data from PHP embedded. This will be included in a script tag that appears immediately after the script tag for wp-api-fetch, something like this:

<script src="path/to/wp-api-fetch.js"></script>
<script>
// The JavaScript code is printed here:
wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "…/index.php?rest_route=/" ) );
// More code may follow from other calls to `wp_add_inline_script`…
</script>

Notice that the JavaScript depends on wp-api-fetch, it imperatively calls wp.apiFetch.use(…). That same approach with a hypothetical @wordpress/api-fetch module would look something like this:

<script type="importmap">
{ "imports": { "@wordpress/api-fetch": "…url/to/api-fetch-module.js" } }
</script>
<script type="module">
import apiFetch from '@wordpress/api-fetch';
wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "…/index.php?rest_route=/" ) );
</script>

In this approach, the initialization module would fetch and execute the @wordpress/api-fetch module just to initialize it! That eliminates one of the important advantages of modules, their ability to load on-demand. 🤔 It looks like this imperative initialization isn’t a good fit for modules. Let’s see if there’s a better solution…

The proposal

Here are some requirements that arise from the naive implementation:

  • Modules should be fetched and initialized on demand.
  • Modules should be responsible for their own initialization when they’re executed.
  • Variables should be scoped to modules and not pollute the global namespace.
  • The data should introduce minimal overhead.

Add server data via filters

Filters provide a nice method to collect the data needed by modules. The WP_Script_Modules class introduces a new filter that runs for each module that is enqueued or present in the dependency graph. Adding or modifying data for a module looks like this:

add_filter(
	'scriptmoduledata_@wordpress/api-fetch',
	function ( $data ) {
		$data['rootURL'] =  sanitize_url( get_rest_url() )
		return $data;
	}
);

Multiple filters can be added to add or modify the data exposed to the script, and if no data is added, nothing will be serialized on the page for the client. It’s also worth mentioning that no JavaScript code is written in PHP, a nice improvement over wp_add_inline_script which requires valid JavaScript to be added.

A drawback to this approach is that all the data passed must pass through JSON. Data without a valid JSON representation is not supported by default.

Use an inert script tag to expose data on the client

The data is embedded it in the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. in a script tag:

<script id="scriptmoduledata_@wordpress/api-fetch" type="application/json">
{ "theData": "JSON Serializable data can be shared" }
</script>

This script tag is effectively ignored by the browser because of its type attribute. This approach is an example on MDN of how to embed data in HTML and it’s already used in WordPress to pass Interactivity API data to the client.

Because modules are always deferred, it should be safe to print these script tags at the bottom of the page. They should have limited impact on page load because the browser will not parse or execute the contents.

Modules read data from the script tag

This script tag doesn’t do anything on its own. The module is responsible for getting the data and performing its initialization when it executes:

if ( typeof document !== 'undefined' ) {
	const serializedData = document.getElementById(
		'scriptmoduledata_@wordpress/api-fetch'
	)?.textContent;
	if ( serializedData ) {
		let config = null;
		try {
			config = JSON.parse( serializedData );
		} catch {
			// there was a problem parsing the serialized data
		}
		performInitialization( config );
	}
}
function performInitialization( config ) {
	if ( config?.rootURL ) {
		registerMiddleware( createRootURLMiddleware( config.rootURL ) );
	}
	// etc.
}

This approach is used by @wordpress/interactivity to retrieve store data on the client.

Try it!

I’ve prototyped this proposal in the following PRs:

  • WordPress-develop PR 6433 applies the filters and adds the necessary filters to expose data to @wordpress/api-fetch. The proposal in this post is contained in this PR.
  • Gutenberg PR 60952 builds and registers the @wordpress/api-fetch module. This PR is helpful for testing, but is beyond the scope of this post.

Try it in the WordPress Playground here. If you run the following JavaScript in the inspector console —make sure you pick the JavaScript context, something like wp (scope:abc123)— you’ll see the @wordpress/api-fetch module log some initialization when it’s imported and then work as expected:

const { apiFetch } = await import( '@wordpress/api-fetch' );
await apiFetch( { path: '/wp/v2/block-types' } )
Screenshot of browser console showing the `@wordpress/api-fetch` module initializing on demand and being used to make a REST request.

Relevant links

Props @youknowriad, @cbravobernal, @bph, and @andronocean for review.

#javascript, #script-loader

Attributes for Resource Hints in 4.7

WordPress 4.6 added support for Resource Hints, a W3CW3C The World Wide Web Consortium (W3C) is an international community where Member organizations, a full-time staff, and the public work together to develop Web standards.https://www.w3.org/. specification that “defines the dns-prefetch, preconnect, prefetch, and prerender relationships of the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. Link Element (<link>)”. These can be used to assist the browser in the decision process of which origins it should connect to, and which resources it should fetch and preprocess to improve page performance.

With WordPress 4.7, you’re now able to pass specific HTML attributes to these resource hints to make even better use of them. Namely, the as, crossorigin, pr, and type attributes can now be defined when using the wp_resource_hints filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output.. See #38121 for more information.

Here’s an example of how one can use this new feature:

function makewp_example_resource_hints_attributes( $hints, $relation_type ) {
	if ( 'prefetch' === $relation_type ) {
		$hints[] = array(
			'crossorigin' => 'use-credentials',
			'as'          => 'style',
			'pr'          => 0.5,
			'href'        => 'https://example.com/foo.css',
		);
	}

	return $hints;
}

add_filter( 'wp_resource_hints', 'makewp_example_resource_hints_attributes', 10, 2 );

While crossorigin can be used to set the CORS settings for a resource, pr indicates the expected probability that the specified resource hint will be used. The official W3C specification has more information about these attributes.

Note: preload is not yet supported by wp_resource_hints(), mainly because of a lack of browser support and benefit for coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress.. This will continue to be reevaluated as browser support evolves for these emerging features.

#4-7, #dev-notes, #script-loader

Introducing admin_print_footer_scripts-$hook_suffix in 4.6

The admin_print_footer_scripts action hook is the last chance to localize adminadmin (and super admin) scripts; but it is a generic one. If you ever wanted to do something for specific admin pages only, you would either have to hook into another dynamic action, or hook into admin_print_footer_scripts and check the $hook_suffix (which is not even passed, but that’s not an issue) yourself. The problem with the former is that there might be happening a lot between the chosen action and when your script gets enqueued (admin_print_footer_scripts); and maybe you need to be aware of what has happened. The problem with the latter is that you would have to register possibly a lot of functions, maybe check the current admin page inside several of these, and eventually bail most of the times. That’s why WordPress 4.6 brings the dynamic footer action admin_print_footer_scripts-$hook_suffix. See [37279].

The following pre-4.6 code

add_action( 'admin_print_footer_scripts', function() {
    global $hook_suffix;

    if ( 'some_admin_page' !== $hook_suffix ) {
        return;
    }

    // Whatever it is that you want to do...
} );

can now be simplified like this:

add_action( 'admin_print_footer_scripts-some_admin_page', function() {
    // Whatever it is that you want to do...
} );

This change brings more consistency between wp-admin/admin-footer.php and wp-admin/admin-header.php, which already fires the generic admin_print_scripts and the dynamic admin_print_scripts-$hook_suffix actions.

For more background on the change, see #34334.

#4-6, #dev-notes, #plugins, #script-loader

Resource Hints in 4.6

Resource Hints is a rather new W3C specification that “defines the dns-prefetch, preconnect, prefetch, and prerender relationships of the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. Link Element (<link>)”. These can be used to assist the browser in the decision process of which origins it should connect to, and which resources it should fetch and preprocess to improve page performance.

Introduced with [37920], WordPress now has a simple APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. to register and use resource hints. The relevant ticketticket Created for both bug reports and feature development on the bug tracker. is #34292

By default, wp_resource_hints() prints hints for s.w.org (the WordPress.orgWordPress.org The community site where WordPress code is created and shared by the users. This is where you can download the source code for WordPress core, plugins and themes as well as the central location for community conversations and organization. https://wordpress.org/ CDN) and for all scripts and styles which are enqueued from external hosts.

Developers can use the wp_resource_hints filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. to add custom domains and URLs for dns-prefetch, preconnectprefetch or prerender. One needs to be careful to not add too many resource hints as they could quite easily negatively impact performance, especially on mobile, but the filter works like this:

function makewp_example_resource_hints( $hints, $relation_type ) {
	if ( 'dns-prefetch' === $relation_type ) {
		$hints[] = '//make.wordpress.org';
	} else if ( 'prerender' === $relation_type ) {
		$hints[] = 'https://make.wordpress.org/great-again';
	}

	return $hints;
}

add_filter( 'wp_resource_hints', 'makewp_example_resource_hints', 10, 2 );

#4-6, #dev-notes, #script-loader

Enhanced Script Loader in WordPress 4.5

This post summarizes some of the changes to the script loader and script/style dependencies in WordPress 4.5.

Individual stylesheets instead of wp-admin.min.css

Ticketticket Created for both bug reports and feature development on the bug tracker.: #35229

Currently, WordPress generates and ships relatively large 235KB wp-admin.min.css and wp-admin-rtl.min.css files which are created from source files which we also ship.
With WordPress 4.5 we stop generating these files and instead rely upon load-styles.php to combine them. This removes the requirement from shipping for commits such as [35896] 510KB of CSSCSS Cascading Style Sheets.. Instead, we only have to ship the 4 dashboard.css files which are around 72KB.

For pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party authors nothing should change because the script loader takes care of the new dependency for the wp-admin handle. Also, wp-admin.* files are still generated for compatibility purposes, however, they only include the @import() lines.

Breaking Change: If your plugin or theme is still using the deprecated media functionality please note that in [36869] the style handle was changed from media to media-deprecated.

HTTPHTTP HTTP is an acronym for Hyper Text Transfer Protocol. HTTP is the underlying protocol used by the World Wide Web and this protocol defines how messages are formatted and transmitted, and what actions Web servers and browsers should take in response to various commands. ETag headerHeader The header of your site is typically the first thing people will experience. The masthead or header art located across the top of your page is part of the look and feel of your website. It can influence a visitor’s opinion about your content and you/ your organization’s brand. It may also look different on different screen sizes. for load-scripts.php and load-styles.php

Ticket: #28722

Both loaders for script and style concatenation are now sending an ETag header which includes the value of $wp_version. This improves performance since browsers won’t re-download the scripts and styles when they send the HTTP_IF_NONE_MATCH header and there was no change in $wp_version. ⚡️

wp_add_inline_script()

Ticket: #14853

For quite some time wp_add_inline_style() has been available to add extra CSS styles to a registered stylesheet. Now there’s an equivalent function to do the same for inline JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/.. wp_add_inline_script() can be used to add extra scripts either before or after a registered script using the optional third $position argument.

For example, the following code can be used to easily add Typekit’s JavaScript to your theme:

function mytheme_enqueue_typekit() {
   wp_enqueue_script( 'mytheme-typekit', 'https://use.typekit.net/<typekit-id>.js', array(), '1.0' );
   wp_add_inline_script( 'mytheme-typekit', 'try{Typekit.load({ async: true });}catch(e){}' );
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_typekit' );

Which results in:

<script type='text/javascript' src='https://use.typekit.net/<typekit-id>.js?ver=1.0'></script>
<script type='text/javascript'>
try{Typekit.load({ async: true });}catch(e){}
</script>

Scripts/Styles with “alias” handles

Ticket: #35643, #25247, #35229

Alias handles are handles without a $src parameter. Those can be used to group dependencies, like core is doing for jQuery[36550] changes how those handles are loaded, more specifically, they are no longer skipped early in WP_Dependencies.

Now, inline styles and scripts attached to alias handles will do something important — get printed out. This change was required by the switch to an alias handle for wp-admin to provide backwards compatibility for plugins which are adding inline styles to the wp-admin handle.

Support for scripts with dependencies in different groups

Ticket: #35873, #35873

Scripts can be registered in two groups: head or footer. Previously, dependencies of registered scripts were moved to the header, even when the script that depends on them is loaded in the footer. This was fixed in [36871]. The changeset includes some expressive tests to demonstrate how complex dependencies, like “grandchild” dependencies, can be enqueued.

Last, but not least, WP_Dependencies, WP_Styles, and WP_Scripts are now fully documented. 📘

Thanks to @abiralneupane, @atimmer, @dd32, @gitlost, @ocean90, @sebastian.pisula, @sergej.mueller, @stephenharris, and @swissspidy for their contributions!

#4-5, #dev-notes, #script-loader