diff --git a/package-lock.json b/package-lock.json index 86c64d697bf18..3266fd7ff209e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26917,7 +26917,16 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.6.2.tgz", "integrity": "sha512-AV33EzqiFJ3fj+mPlKABN59YFPReLkDxQnj067Z3uEOeRQf3g05WprL0RDuqM7UBhSRo9W1rMSC2KvZmjE5UOA==", - "dev": true + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } }, "node_modules/prettier-linter-helpers": { "version": "1.0.0", diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 9bf6ce33e7420..9a09d5e8697a0 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -72,7 +72,10 @@ public function register( string $id, string $src, array $deps = array(), $versi } $dependencies[] = array( 'id' => $dependency['id'], - 'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static', + 'import' => isset( $dependency['import'] ) && + 'dynamic' === $dependency['import'] || + 'wp-script' === $dependency['import'] + ? $dependency['import'] : 'static', ); } elseif ( is_string( $dependency ) ) { $dependencies[] = array( @@ -195,6 +198,13 @@ public function print_enqueued_script_modules() { 'id' => $id . '-js-module', ) ); + + foreach ( $script_module['dependencies'] as $dependency ) { + if ( 'wp-script' !== $dependency['import'] ) { + continue; + } + wp_enqueue_script( $dependency['id'] ); + } } } @@ -250,6 +260,15 @@ public function print_import_map() { 'id' => 'wp-importmap', ) ); + + foreach ( $import_map['imports'] as $id => $_ ) { + foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) { + if ( 'wp-script' !== $dependency['import'] ) { + continue; + } + wp_enqueue_script( $dependency['id'] ); + } + } } } diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 0aceb51f62050..a760e226367f2 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -123,3 +123,62 @@ function wp_dequeue_script_module( string $id ) { function wp_deregister_script_module( string $id ) { wp_script_modules()->deregister( $id ); } + +/** + * Registers all the WordPress packages scripts proxy modules. + * + * @since 6.6.0 + * + * @param WP_Scripts $scripts WP_Scripts object. + */ +function wp_register_package_scripts_proxy_modules() { + $suffix = defined( 'WP_RUN_CORE_TESTS' ) ? '.min' : wp_scripts_get_suffix(); + + /* + * Expects multidimensional array like: + * + * 'a11y-esm.js' => array('dependencies' => array(...), 'version' => '...'), + * 'annotations-esm.js' => array('dependencies' => array(...), 'version' => '...'), + * 'api-fetch-esm.js' => array(... + */ + $assets = include ABSPATH . WPINC . "/assets/script-loader-packages-proxy-modules{$suffix}.php"; + + foreach ( $assets as $file_name => $package_data ) { + $basename = str_replace( $suffix . '-esm-proxy.js', '', basename( $file_name ) ); + $id = '@wordpress/' . $basename; + $path = "/wp-includes/js/dist/{$file_name}"; + + if ( ! empty( $package_data['dependencies'] ) ) { + $dependencies = $package_data['dependencies']; + } else { + $dependencies = array(); + } + + wp_register_script_module( $id, $path, $dependencies, $package_data['version'] ); + } + + /* + * Expects multidimensional array like: + * + * 'a11y-esm.js' => array('dependencies' => array(...), 'version' => '...'), + * 'annotations-esm.js' => array('dependencies' => array(...), 'version' => '...'), + * 'api-fetch-esm.js' => array(... + */ + $assets = include ABSPATH . WPINC . "/assets/script-loader-packages-esm{$suffix}.php"; + + foreach ( $assets as $file_name => $package_data ) { + $basename = str_replace( $suffix . '-esm.js', '', basename( $file_name ) ); + $id = '@wordpress-esm/' . $basename; + $path = "/wp-includes/js/dist/{$file_name}"; + + if ( ! empty( $package_data['dependencies'] ) ) { + $dependencies = $package_data['dependencies']; + } else { + $dependencies = array(); + } + + wp_register_script_module( $id, $path, $dependencies, $package_data['version'] ); + } +} + +add_action( 'init', 'wp_register_package_scripts_proxy_modules' ); diff --git a/tools/webpack/packages-proxy-module-gen-plugin.js b/tools/webpack/packages-proxy-module-gen-plugin.js new file mode 100644 index 0000000000000..050aa02aeec05 --- /dev/null +++ b/tools/webpack/packages-proxy-module-gen-plugin.js @@ -0,0 +1,205 @@ +/** + * External dependencies + */ +const path = require( 'path' ); +const webpack = require( 'webpack' ); +const json2php = require( 'json2php' ); + +const { createHash } = webpack.util; +const { RawSource } = webpack.sources; + +class WpScriptsPackageProxyModuleWebpackPlugin { + /** + * @param {{ combinedOutputFile: string }} options + */ + constructor( options ) { + if ( ! options || ! Object.hasOwn( options, 'combinedOutputFile' ) ) { + throw new Error( 'Must provide combinedOutputFile option' ); + } + + this.options = options; + } + + /** + * @param {any} asset Asset Data + * @return {string} Stringified asset data suitable for output + */ + stringify( asset ) { + return ` { + compilation.hooks.processAssets.tap( + { + name: this.constructor.name, + stage: compiler.webpack.Compilation + .PROCESS_ASSETS_STAGE_ANALYSE, + }, + () => this.addAssets( compilation ) + ); + } + ); + } + + /** @param {webpack.Compilation} compilation */ + addAssets( compilation ) { + const { combinedOutputFile } = this.options; + + const combinedAssetsData = {}; + + // Accumulate all entrypoint chunks, some of them shared + const entrypointChunks = new Set(); + + /** + * @type Map, libOpts: {name:string[];type:string}}> + */ + const entrypointProxyInfo = new Map(); + + for ( const entrypoint of compilation.entrypoints.values() ) { + for ( const chunk of entrypoint.chunks ) { + entrypointChunks.add( chunk ); + } + + const library = entrypoint.options.library; + if ( ! library || ! Array.isArray( library.name ) ) { + continue; + } + + if ( + 1 !== + compilation.chunkGraph.getNumberOfEntryModules( + entrypoint.getEntrypointChunk() + ) + ) { + continue; + } + + /** @type {webpack.Module} */ + const entryModule = compilation.chunkGraph + .getChunkEntryModulesIterable( entrypoint.getEntrypointChunk() ) + .next().value; + + const exportsInfo = + compilation.moduleGraph.getExportsInfo( entryModule ); + + if ( Array.isArray( exportsInfo.getProvidedExports() ) ) { + entrypointProxyInfo.set( entrypoint.options.name, { + exportsInfo, + libOpts: library, + } ); + } + } + + // Process each entrypoint chunk independently + for ( const chunk of entrypointChunks ) { + const chunkFiles = Array.from( chunk.files ); + + const jsExtensionRegExp = this.useModules ? /\.m?js$/i : /\.js$/i; + + const chunkJSFile = chunkFiles.find( ( f ) => + jsExtensionRegExp.test( f ) + ); + if ( ! chunkJSFile ) { + // There's no JS file in this chunk, no work for us. Typically a `style.css` from cache group. + continue; + } + + // Go through the assets and hash the sources. We can't just use + // `chunk.contentHash` because that's not updated when + // assets are minified. In practice the hash is updated by + // `RealContentHashPlugin` after minification, but it only modifies + // already-produced asset filenames and the updated hash is not + // available to plugins. + const { hashFunction, hashDigest, hashDigestLength } = + compilation.outputOptions; + + if ( entrypointProxyInfo.has( chunk.name ) ) { + const { exportsInfo, libOpts } = entrypointProxyInfo.get( + chunk.name + ); + const generatedProxyModuleFilename = compilation + .getPath( '[file]', { + filename: chunkJSFile, + } ) + .replace( /\.m?js$/i, '-esm-proxy.js' ); + const libraryPath = libOpts.name.map( + ( n ) => `[${ JSON.stringify( n ) }]` + ); + + // let sourceString = `if ( 'undefined' === typeof ${ + // libOpts.type + // }?.${ libraryPath.join( + // '?.' + // ) } ) {\n\tthrow new Error( 'Undefined dependency: ${ + // libOpts.type + // }${ libraryPath.join( '' ) }' );\n}\n`; + + let sourceString = `const __library__ = ${ + libOpts.type + }?.${ libraryPath.join( + '?.' + ) } ?? await import( '@wordpress-esm/${ chunk.name }' )${ + '' //libOpts.export === 'default' ? '.default' : '' + };\n`; + + console.log( { + n: chunk.name, + es: exportsInfo.getProvidedExports(), + } ); + for ( const exportName of exportsInfo.getProvidedExports() ) { + if ( exportName === 'default' ) { + sourceString += `export default __library__.default;\n`; + } else { + sourceString += `export const ${ exportName } = __library__.${ exportName };\n`; + } + } + + const contentHash = createHash( hashFunction ); + contentHash.update( sourceString ); + + compilation.assets[ generatedProxyModuleFilename ] = + new RawSource( sourceString ); + + chunk.files.add( generatedProxyModuleFilename ); + + const assetData = { + dependencies: [ + // { id: `wp-${ chunk.name }`, import: 'wp-script' }, + { + id: `@wordpress-esm/${ chunk.name }`, + import: 'dynamic', + }, + ], + version: contentHash + .digest( hashDigest ) + .slice( 0, hashDigestLength ), + }; + combinedAssetsData[ generatedProxyModuleFilename ] = assetData; + } + } + + const outputFolder = compilation.outputOptions.path; + + const assetsFilePath = path.resolve( outputFolder, combinedOutputFile ); + const assetsFilename = path.relative( outputFolder, assetsFilePath ); + + // Add source into compilation for webpack to output. + compilation.assets[ assetsFilename ] = new RawSource( + this.stringify( combinedAssetsData ) + ); + } +} + +module.exports = WpScriptsPackageProxyModuleWebpackPlugin; diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 2b02852d206de..e9aeccd10e2e9 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -4,6 +4,7 @@ const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); const LiveReloadPlugin = require( 'webpack-livereload-plugin' ); const UglifyJS = require( 'uglify-js' ); +const WpScriptsPackageProxyModuleWebpackPlugin = require( './packages-proxy-module-gen-plugin' ); /** * WordPress dependencies @@ -93,7 +94,8 @@ module.exports = function ( 'wp-polyfill-object-fit.js': 'objectFitPolyfill/src/objectFitPolyfill.js', 'wp-polyfill-inert.js': 'wicg-inert/dist/inert.js', - 'wp-polyfill-importmap.js': 'es-module-shims/dist/es-module-shims.wasm.js', + 'wp-polyfill-importmap.js': + 'es-module-shims/dist/es-module-shims.wasm.js', 'moment.js': 'moment/moment.js', 'react.js': 'react/umd/react.development.js', 'react-dom.js': 'react-dom/umd/react-dom.development.js', @@ -122,7 +124,8 @@ module.exports = function ( 'polyfill-library/polyfills/__dist/Node.prototype.contains/raw.js', 'wp-polyfill-dom-rect.min.js': 'polyfill-library/polyfills/__dist/DOMRect/raw.js', - 'wp-polyfill-importmap.min.js': 'es-module-shims/dist/es-module-shims.wasm.js', + 'wp-polyfill-importmap.min.js': + 'es-module-shims/dist/es-module-shims.wasm.js', }; const phpFiles = { @@ -171,42 +174,96 @@ module.exports = function ( } ) ); const baseConfig = getBaseConfig( env ); - const config = { - ...baseConfig, - entry: packages.reduce( ( memo, packageName ) => { - memo[ packageName ] = { - import: normalizeJoin( - baseDir, - `node_modules/@wordpress/${ packageName }` - ), + const config = [ + { + ...baseConfig, + entry: packages.reduce( ( memo, packageName ) => { + memo[ packageName ] = { + import: normalizeJoin( + baseDir, + `node_modules/@wordpress/${ packageName }` + ), + library: { + name: [ 'wp', camelCaseDash( packageName ) ], + type: 'window', + export: exportDefaultPackages.includes( packageName ) + ? 'default' + : undefined, + }, + }; + + return memo; + }, {} ), + output: { + devtoolNamespace: 'wp', + filename: `[name]${ suffix }.js`, + path: normalizeJoin( baseDir, `${ buildTarget }/js/dist` ), + }, + plugins: [ + ...baseConfig?.plugins, + new DependencyExtractionPlugin( { + injectPolyfill: true, + combineAssets: true, + combinedOutputFile: `../../assets/script-loader-packages${ suffix }.php`, + } ), + new WpScriptsPackageProxyModuleWebpackPlugin( { + combinedOutputFile: `../../assets/script-loader-packages-proxy-modules${ suffix }.php`, + } ), + new CopyWebpackPlugin( { + patterns: [ ...vendorCopies, ...cssCopies, ...phpCopies ], + } ), + ], + }, + { + ...baseConfig, + experiments: { + ...baseConfig?.experiments, + outputModule: true, + }, + entry: packages.reduce( ( memo, packageName ) => { + memo[ packageName ] = { + import: normalizeJoin( + baseDir, + `node_modules/@wordpress/${ packageName }` + ), + library: { + type: 'module', + // export: exportDefaultPackages.includes( packageName ) + // ? 'default' + // : undefined, + }, + }; + + return memo; + }, {} ), + output: { + devtoolNamespace: '@wordpress', + filename: `[name]-esm${ suffix }.js`, + path: normalizeJoin( baseDir, `${ buildTarget }/js/dist` ), + module: true, + chunkFormat: 'module', + environment: { + ...baseConfig?.output?.environment, + module: true, + }, library: { - name: [ 'wp', camelCaseDash( packageName ) ], - type: 'window', - export: exportDefaultPackages.includes( packageName ) - ? 'default' - : undefined, + ...baseConfig?.output?.library, + type: 'module', }, - }; - - return memo; - }, {} ), - output: { - devtoolNamespace: 'wp', - filename: `[name]${ suffix }.js`, - path: normalizeJoin( baseDir, `${ buildTarget }/js/dist` ), + }, + plugins: [ + ...baseConfig?.plugins, + new DependencyExtractionPlugin( { + injectPolyfill: true, + combineAssets: true, + combinedOutputFile: `../../assets/script-loader-packages-esm${ suffix }.php`, + } ), + new CopyWebpackPlugin( { + patterns: [ ...vendorCopies, ...cssCopies, ...phpCopies ], + } ), + ], }, - plugins: [ - ...baseConfig.plugins, - new DependencyExtractionPlugin( { - injectPolyfill: true, - combineAssets: true, - combinedOutputFile: `../../assets/script-loader-packages${ suffix }.php`, - } ), - new CopyWebpackPlugin( { - patterns: [ ...vendorCopies, ...cssCopies, ...phpCopies ], - } ), - ], - }; + ]; if ( config.mode === 'development' ) { config.plugins.push( diff --git a/tools/webpack/shared.js b/tools/webpack/shared.js index 6c1397db40f09..a246253659027 100644 --- a/tools/webpack/shared.js +++ b/tools/webpack/shared.js @@ -19,6 +19,7 @@ const getBaseConfig = ( env ) => { minimizer: [ new TerserPlugin( { extractComments: false, + exclude: /-esm-proxy(?:\.min)?\.js$/, } ), ], }, diff --git a/webpack.config.js b/webpack.config.js index 963117a7a52de..cb13e2fc820e3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,20 +4,22 @@ const mediaConfig = require( './tools/webpack/media' ); const packagesConfig = require( './tools/webpack/packages' ); const modulesConfig = require( './tools/webpack/modules' ); -module.exports = function( env = { environment: "production", watch: false, buildTarget: false } ) { +module.exports = function ( + env = { environment: 'production', watch: false, buildTarget: false } +) { if ( ! env.watch ) { env.watch = false; } if ( ! env.buildTarget ) { - env.buildTarget = ( env.mode === 'production' ? 'build/' : 'src/' ); + env.buildTarget = env.mode === 'production' ? 'build/' : 'src/'; } const config = [ blocksConfig( env ), ...developmentConfig( env ), mediaConfig( env ), - packagesConfig( env ), + ...packagesConfig( env ), modulesConfig( env ), ];