diff --git a/package-lock.json b/package-lock.json index f031cdaf0..8032aea98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,9 +56,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", diff --git a/package.json b/package.json index 7b96d9579..094a967a6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/utils/wordpress-globals.js b/src/utils/wordpress-globals.js index dfe575f73..d87c16c38 100644 --- a/src/utils/wordpress-globals.js +++ b/src/utils/wordpress-globals.js @@ -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'; @@ -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 ); 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 ); diff --git a/src/utils/wordpress-globals.test.js b/src/utils/wordpress-globals.test.js new file mode 100644 index 000000000..7a9c85112 --- /dev/null +++ b/src/utils/wordpress-globals.test.js @@ -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} 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(), + ] ); + } ) + ); +}