Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@
"@wordpress/server-side-render": "^6.14.0",
"@wordpress/shortcode": "^4.38.0",
"@wordpress/style-engine": "^2.43.0",
"@wordpress/sync": "^2.0.0",
"@wordpress/theme": "^2.1.0",
"@wordpress/token-list": "^3.38.0",
"@wordpress/undo-manager": "^1.57.0",
"@wordpress/upload-media": "^0.42.0",
"@wordpress/url": "^4.42.0",
"@wordpress/viewport": "^6.57.0",
Expand Down
4 changes: 2 additions & 2 deletions src/utils/wordpress-globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ import * as router from '@wordpress/router';
import * as serverSideRender from '@wordpress/server-side-render';
import * as shortcode from '@wordpress/shortcode';
import * as styleEngine from '@wordpress/style-engine';
import * as sync from '@wordpress/sync';
import * as theme from '@wordpress/theme';
import * as tokenList from '@wordpress/token-list';
import * as undoManager from '@wordpress/undo-manager';
import * as uploadMedia from '@wordpress/upload-media';
import * as url from '@wordpress/url';
import * as viewport from '@wordpress/viewport';
Expand Down Expand Up @@ -124,9 +124,9 @@ export async function initializeWordPressGlobals() {
window.wp.serverSideRender = serverSideRender.default || serverSideRender;
window.wp.shortcode = shortcode.default || shortcode;
window.wp.styleEngine = toCommonJS( styleEngine );
window.wp.sync = toCommonJS( sync );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean packages that are built with older version WordPress packages won't be able to get wp.sync? Would that cause any issue?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the impact to GBK is very narrow and the breaking change was accepted in upstream Gutenberg core in WordPress/gutenberg#81999. It's expected that plugins bundle @wordpress/sync themselves or access the relevant utility via @wordpress/core-data's Y option added to the provider creator.

Details explained by Claude below:

Only for plugins built with @wordpress/dependency-extraction-webpack-plugin 6.40–6.56 that import @wordpress/sync; builds before Feb 2026 and from 6.57 on bundle their own copy. In practice those are real-time collaboration transports (e.g. Jetpack's, VIP's). Those can't work in GBK anyway, since collaboration only runs when window.__experimentalEnableRealTimeCollaboration is set, which we never do. A script that throws on load doesn't stop the remaining assets from loading.

Upstream accepted the same break: WordPress/gutenberg#81999 lists it as breaking, WP 7.0/7.1 never shipped wp.sync, and Gutenberg 24.2 drops it. On our side, wp.sync was only added in #750 and never shipped in a GBK release, so nothing regresses.

window.wp.theme = toCommonJS( theme );
window.wp.tokenList = tokenList.default || tokenList;
window.wp.undoManager = toCommonJS( undoManager );
window.wp.uploadMedia = toCommonJS( uploadMedia );
window.wp.url = toCommonJS( url );
window.wp.viewport = toCommonJS( viewport );
Expand Down
114 changes: 114 additions & 0 deletions src/utils/wordpress-globals.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { camelCaseDash } from '@wordpress/dependency-extraction-webpack-plugin/lib/util';

// Packages flagged `wpScript` that WP Admin's post editor doesn't expose,
// keyed to the reason.
const EXCLUDED_PACKAGES = {
'@wordpress/base-styles':
'Flagged only to build its stylesheet; the editor never loads its empty script.',
};

// Modules that assign `window.wp` globals. Importing them would load every
// `@wordpress` package, which Node can't evaluate, so read their source.
const GLOBALS_MODULES = [ './wordpress-globals.js', './wordpress-i18n.js' ];

const SCRIPT_PACKAGES = getInstalledScriptPackages();

describe( 'WordPress globals', () => {
const assignments = getAssignments();
const scriptNames = SCRIPT_PACKAGES.map( ( { name } ) => name );
const scriptGlobals = SCRIPT_PACKAGES.map( ( { global } ) => global );

// WordPress builds a `window.wp` global for each package flagged `wpScript`,
// so the flag approximates the globals plugins expect. A missing global
// breaks every plugin script that reads it. Core exposes the default export
// of packages flagged `wpScriptDefaultExport`, and the namespace of the rest.
it.each(
SCRIPT_PACKAGES.filter( ( { name } ) => ! EXCLUDED_PACKAGES[ name ] )
)(
'exposes $name in the shape core does',
( { global, defaultExport } ) => {
expect( assignments ).toHaveProperty( global );
expect( assignments[ global ] ).toMatch(
defaultExport
? /^(\w+)\.default \|\| \1$/
: /^toCommonJS\( \w+ \)$|^undefined$/
);
}
);

// A global without a flagged package is one core no longer exposes, like
// `wp.sync` after WordPress/gutenberg#81999.
it.each( Object.keys( assignments ) )(
'exposes window.wp.%s only while a package is flagged `wpScript`',
( global ) => {
expect( scriptGlobals ).toContain( global );
}
);

it.each( Object.keys( EXCLUDED_PACKAGES ) )(
'excludes %s only while it is flagged `wpScript`',
( name ) => {
expect( scriptNames ).toContain( name );
}
);
} );

/**
* List the installed `@wordpress` packages that WordPress builds as scripts.
*
* @return {Array<{name: string, global: string, defaultExport: boolean}>}
* Packages, with the `window.wp` member core assigns each.
*/
function getInstalledScriptPackages() {
const root = join( import.meta.dirname, '../../node_modules/@wordpress' );

return readdirSync( root ).flatMap( ( directory ) => {
const path = join( root, directory, 'package.json' );

// Skip entries that aren't packages, e.g. `.DS_Store` or npm's
// temporary directories from an interrupted install.
if ( ! existsSync( path ) ) {
return [];
}

const { name, wpScript, wpScriptDefaultExport } = JSON.parse(
readFileSync( path )
);

return wpScript
? [
{
name,
global: camelCaseDash( directory ),
defaultExport: !! wpScriptDefaultExport,
},
]
: [];
} );
}

/**
* Map each `window.wp` member the globals modules assign to its value's source.
*
* @return {Object<string, string>} Source keyed by global, e.g.
* `{ blockEditor: 'toCommonJS( blockEditor )' }`.
*/
function getAssignments() {
return Object.fromEntries(
GLOBALS_MODULES.flatMap( ( path ) => {
const source = readFileSync(
join( import.meta.dirname, path ),
'utf8'
);
return [
...source.matchAll( /window\.wp\.(\w+)\s*=\s*([^;]+);/g ),
].map( ( [ , global, value ] ) => [
global,
value.replace( /\s+/g, ' ' ).trim(),
] );
} )
);
}
Loading