From f8a19eb08945a9fd26d5bda49b1e2c94857ea827 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 11 Sep 2024 13:15:29 +0200 Subject: [PATCH] Build: Prepare build for more script modules (#65064) - Rename the "interactivity" webpack build to "script modules". - Output script-modules builds to `build-module` folder (adjacent to the `build` folder currently used for scripts). - Add `wpScriptModulesExports` package.json field to packages with script modules and use it for script module builds. This follows the same basic syntax as [package.json `exports` fields](https://nodejs.org/api/packages.html#exports). Multiple module entrypoints can be exposed per package. However, it remains a custom field, so it is clear that these entrypoints are not intended for general consumption. In the future, module-only packages (interactivity, interactivity-router) can switch to using exports directly and likely add `type: module`. - There are some difficulties with webpack recognizing `wpScriptModulesExports` directly, so packages are inspected programmatically in order to generate webpack script modules entrypoints. - Adjust script module registration accordingly to find the generated script modules. --- Co-authored-by: sirreal Co-authored-by: gziolo Co-authored-by: t-hamano --- .github/workflows/bundle-size.yml | 2 +- bin/build-plugin-zip.sh | 1 + lib/interactivity-api.php | 4 +- package-lock.json | 1 + package.json | 1 + packages/block-library/package.json | 7 + packages/block-library/src/file/index.php | 2 +- packages/block-library/src/image/index.php | 2 +- .../block-library/src/navigation/index.php | 2 +- packages/block-library/src/query/index.php | 2 +- packages/block-library/src/search/index.php | 2 +- packages/interactivity-router/package.json | 1 + packages/interactivity/package.json | 4 + tools/webpack/interactivity.js | 74 ---------- tools/webpack/script-modules.js | 133 ++++++++++++++++++ webpack.config.js | 4 +- 16 files changed, 158 insertions(+), 84 deletions(-) delete mode 100644 tools/webpack/interactivity.js create mode 100644 tools/webpack/script-modules.js diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 6106eee492c32..499a2c020255c 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -52,5 +52,5 @@ jobs: - uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c # v2.6.0 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' - pattern: '{build/**/*.min.js,build/**/*.css}' + pattern: '{build/**/*.min.js,build/**/*.css,build-module/**/*.min.js}' clean-script: 'distclean' diff --git a/bin/build-plugin-zip.sh b/bin/build-plugin-zip.sh index 4ba931c4a4aeb..ad627e05f0c69 100755 --- a/bin/build-plugin-zip.sh +++ b/bin/build-plugin-zip.sh @@ -98,6 +98,7 @@ zip -r gutenberg.zip \ packages/block-serialization-default-parser/*.php \ post-content.php \ $build_files \ + build-module \ readme.txt \ changelog.txt \ README.md diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index 6f04a3ba8fc92..90535f1ebaa42 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -16,14 +16,14 @@ function gutenberg_reregister_interactivity_script_modules() { wp_register_script_module( '@wordpress/interactivity', - gutenberg_url( '/build/interactivity/' . ( SCRIPT_DEBUG ? 'debug.min.js' : 'index.min.js' ) ), + gutenberg_url( '/build-module/' . ( SCRIPT_DEBUG ? 'interactivity/debug.min.js' : 'interactivity/index.min.js' ) ), array(), $default_version ); wp_register_script_module( '@wordpress/interactivity-router', - gutenberg_url( '/build/interactivity/router.min.js' ), + gutenberg_url( '/build-module/interactivity-router/index.min.js' ), array( '@wordpress/interactivity' ), $default_version ); diff --git a/package-lock.json b/package-lock.json index b9e116f15388b..769ef05083420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", "@wordpress/interactivity": "file:packages/interactivity", + "@wordpress/interactivity-router": "file:packages/interactivity-router", "@wordpress/interface": "file:packages/interface", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", diff --git a/package.json b/package.json index 22cdf2ce7acc8..e757aaa0f44b4 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", "@wordpress/interactivity": "file:packages/interactivity", + "@wordpress/interactivity-router": "file:packages/interactivity-router", "@wordpress/interface": "file:packages/interface", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 4d0212490858c..1353ef24c77d8 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -30,6 +30,13 @@ "src/**/*.scss", "{src,build,build-module}/*/init.js" ], + "wpScriptModuleExports": { + "./file/view": "./build-module/file/view.js", + "./image/view": "./build-module/image/view.js", + "./navigation/view": "./build-module/navigation/view.js", + "./query/view": "./build-module/query/view.js", + "./search/view": "./build-module/search/view.js" + }, "dependencies": { "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 87910f0e66a0c..85cc840201da5 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -21,7 +21,7 @@ function render_block_core_file( $attributes, $content ) { if ( ! empty( $attributes['displayPreview'] ) ) { $suffix = wp_scripts_get_suffix(); if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build/interactivity/file.min.js' ); + $module_url = gutenberg_url( '/build-module/block-library/file/view.min.js' ); } wp_register_script_module( diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 75f0d404e4820..abbb03c095245 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -72,7 +72,7 @@ function render_block_core_image( $attributes, $content, $block ) { ) { $suffix = wp_scripts_get_suffix(); if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build/interactivity/image.min.js' ); + $module_url = gutenberg_url( '/build-module/block-library/image/view.min.js' ); } wp_register_script_module( diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index ccadd5c4a222d..ec72b03b6906f 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -624,7 +624,7 @@ private static function handle_view_script_module_loading( $attributes, $block, if ( static::is_interactive( $attributes, $inner_blocks ) ) { $suffix = wp_scripts_get_suffix(); if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build/interactivity/navigation.min.js' ); + $module_url = gutenberg_url( '/build-module/block-library/navigation/view.min.js' ); } wp_register_script_module( diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 6cc57dc08388c..d10db26529854 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -26,7 +26,7 @@ function render_block_core_query( $attributes, $content, $block ) { if ( $is_interactive ) { $suffix = wp_scripts_get_suffix(); if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build/interactivity/query.min.js' ); + $module_url = gutenberg_url( '/build-module/block-library/query/view.min.js' ); } wp_register_script_module( diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 39b8591c86600..fb09cdd36406e 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -82,7 +82,7 @@ function render_block_core_search( $attributes ) { if ( $is_expandable_searchfield ) { $suffix = wp_scripts_get_suffix(); if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build/interactivity/search.min.js' ); + $module_url = gutenberg_url( '/build-module/block-library/search/view.min.js' ); } wp_register_script_module( diff --git a/packages/interactivity-router/package.json b/packages/interactivity-router/package.json index 53b68068c528a..db85c0d8bdba3 100644 --- a/packages/interactivity-router/package.json +++ b/packages/interactivity-router/package.json @@ -26,6 +26,7 @@ "module": "build-module/index.js", "react-native": "src/index", "types": "build-types", + "wpScriptModuleExports": "./build-module/index.js", "dependencies": { "@wordpress/interactivity": "file:../interactivity" }, diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index 3e29d9aabeeab..6be9f3c4a0d7d 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -26,6 +26,10 @@ "module": "build-module/index.js", "react-native": "src/index", "types": "build-types", + "wpScriptModuleExports": { + ".": "./build-module/index.js", + "./debug": "./build-module/debug.js" + }, "dependencies": { "@preact/signals": "^1.2.2", "preact": "^10.19.3" diff --git a/tools/webpack/interactivity.js b/tools/webpack/interactivity.js deleted file mode 100644 index 7f5c7f64a09d7..0000000000000 --- a/tools/webpack/interactivity.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * External dependencies - */ -const { join } = require( 'path' ); - -/** - * WordPress dependencies - */ -const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); - -/** - * Internal dependencies - */ -const { baseConfig, plugins } = require( './shared' ); - -module.exports = { - ...baseConfig, - name: 'interactivity', - entry: { - index: './packages/interactivity', - debug: './packages/interactivity/src/debug', - router: './packages/interactivity-router', - navigation: './packages/block-library/src/navigation/view.js', - query: './packages/block-library/src/query/view.js', - image: './packages/block-library/src/image/view.js', - file: './packages/block-library/src/file/view.js', - search: './packages/block-library/src/search/view.js', - }, - experiments: { - outputModule: true, - }, - output: { - devtoolNamespace: 'wp', - filename: './build/interactivity/[name].min.js', - library: { - type: 'module', - }, - path: join( __dirname, '..', '..' ), - environment: { module: true }, - module: true, - chunkFormat: 'module', - }, - resolve: { - extensions: [ '.js', '.ts', '.tsx' ], - }, - module: { - rules: [ - { - test: /\.(j|t)sx?$/, - exclude: /node_modules/, - use: [ - { - loader: require.resolve( 'babel-loader' ), - options: { - cacheDirectory: - process.env.BABEL_CACHE_DIRECTORY || true, - babelrc: false, - configFile: false, - presets: [ - '@babel/preset-typescript', - '@babel/preset-react', - ], - }, - }, - ], - }, - ], - }, - plugins: [ ...plugins, new DependencyExtractionWebpackPlugin() ], - watchOptions: { - ignored: [ '**/node_modules' ], - aggregateTimeout: 500, - }, -}; diff --git a/tools/webpack/script-modules.js b/tools/webpack/script-modules.js new file mode 100644 index 0000000000000..57652e0be28e2 --- /dev/null +++ b/tools/webpack/script-modules.js @@ -0,0 +1,133 @@ +/** + * External dependencies + */ +const { join } = require( 'path' ); + +/** + * WordPress dependencies + */ +const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); + +/** + * Internal dependencies + */ +const { baseConfig, plugins } = require( './shared' ); + +const WORDPRESS_NAMESPACE = '@wordpress/'; +const { createRequire } = require( 'node:module' ); + +const rootURL = new URL( '..', `file://${ __dirname }` ); +const fromRootRequire = createRequire( rootURL ); + +/** @type {Iterable<[string, string]>} */ +const iterableDeps = Object.entries( + fromRootRequire( './package.json' ).dependencies +); + +/** @type {Map} */ +const gutenbergScriptModules = new Map(); +for ( const [ packageName, versionSpecifier ] of iterableDeps ) { + if ( + ! packageName.startsWith( WORDPRESS_NAMESPACE ) || + ! versionSpecifier.startsWith( 'file:' ) || + packageName.startsWith( WORDPRESS_NAMESPACE + 'react-native' ) + ) { + continue; + } + + const packageRequire = createRequire( + // Remove the leading "file:" specifier to build a package URL. + new URL( `${ versionSpecifier.substring( 5 ) }/`, rootURL ) + ); + + const depPackageJson = packageRequire( './package.json' ); + if ( ! Object.hasOwn( depPackageJson, 'wpScriptModuleExports' ) ) { + continue; + } + + const moduleName = packageName.substring( WORDPRESS_NAMESPACE.length ); + let { wpScriptModuleExports } = depPackageJson; + + // Special handling for { "wpScriptModuleExports": "./build-module/index.js" }. + if ( typeof wpScriptModuleExports === 'string' ) { + wpScriptModuleExports = { '.': wpScriptModuleExports }; + } + + if ( Object.getPrototypeOf( wpScriptModuleExports ) !== Object.prototype ) { + throw new Error( 'wpScriptModuleExports must be an object' ); + } + + for ( const [ exportName, exportPath ] of Object.entries( + wpScriptModuleExports + ) ) { + if ( typeof exportPath !== 'string' ) { + throw new Error( 'wpScriptModuleExports paths must be strings' ); + } + + if ( ! exportPath.startsWith( './' ) ) { + throw new Error( + 'wpScriptModuleExports paths must start with "./"' + ); + } + + const name = + exportName === '.' ? 'index' : exportName.replace( /^\.\/?/, '' ); + + gutenbergScriptModules.set( + `${ moduleName }/${ name }`, + packageRequire.resolve( exportPath ) + ); + } +} + +module.exports = { + ...baseConfig, + name: 'script-modules', + entry: Object.fromEntries( gutenbergScriptModules.entries() ), + experiments: { + outputModule: true, + }, + output: { + devtoolNamespace: 'wp', + filename: './build-module/[name].min.js', + library: { + type: 'module', + }, + path: join( __dirname, '..', '..' ), + environment: { module: true }, + module: true, + chunkFormat: 'module', + asyncChunks: false, + }, + resolve: { + extensions: [ '.js', '.ts', '.tsx' ], + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve( 'babel-loader' ), + options: { + cacheDirectory: + process.env.BABEL_CACHE_DIRECTORY || true, + babelrc: false, + configFile: false, + presets: [ + '@babel/preset-typescript', + '@babel/preset-react', + ], + }, + }, + ], + }, + ], + }, + plugins: [ ...plugins, new DependencyExtractionWebpackPlugin() ], + watchOptions: { + ignored: [ '**/node_modules' ], + aggregateTimeout: 500, + }, +}; diff --git a/webpack.config.js b/webpack.config.js index 45b22cc5354dc..51889b06d1eb4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,13 +3,13 @@ */ const blocksConfig = require( './tools/webpack/blocks' ); const developmentConfigs = require( './tools/webpack/development' ); -const interactivity = require( './tools/webpack/interactivity' ); +const scriptModules = require( './tools/webpack/script-modules' ); const packagesConfig = require( './tools/webpack/packages' ); const vendorsConfig = require( './tools/webpack/vendors' ); module.exports = [ ...blocksConfig, - interactivity, + scriptModules, packagesConfig, ...developmentConfigs, ...vendorsConfig,