Skip to content

Commit

Permalink
WP Scripts: Build block.json viewModule (#57461)
Browse files Browse the repository at this point in the history
Add handling of viewModule field in block.json when using wp-scripts build or start. As the Modules API matures, our tooling should support it.

We add an option to clearly mark this an an experimental feature: `wp-scripts build --experimental-modules` or `wp-scripts start --experimental-modules`.

To support modules with webpack, it was necessary to run a multi-compilation. An array of webpack configurations is used instead of a single webpack configuration. This is because module compilation is an option at the compilation level and cannot be set for specific entrypoints.

If the `--experimental-modules` option is found, we use an environment variable to change the webpack.config export to return an array `[ scriptWebpackConfig, moduleWebpackConfig ]`. Without the experimental option, the `webpack.config` export is the same. Consumers should be able to continue extending this config without noticing any differences.

Part of #57492.
  • Loading branch information
sirreal authored Jan 9, 2024
1 parent e0eb749 commit 240f303
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 181 deletions.
4 changes: 4 additions & 0 deletions packages/scripts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features

- Add experimental support for `viewModule` field in block.json for `build` and `start` scripts ([#57461](https://github.com/WordPress/gutenberg/pull/57461)).

### Breaking Changes

- Drop support for Node.js versions < 18.
Expand Down
14 changes: 12 additions & 2 deletions packages/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ This script automatically use the optimized config but sometimes you may want to
- `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`.
- `--output-path` – Allows customization of the output directory. Default is `build`.

Experimental support for the block.json `viewModule` field is available via the
`--experimental-modules` option. With this option enabled, script and module fields will all be
compiled. The `viewModule` field is analogous to the `viewScript` field, but will compile a module
and should be registered in WordPress using the Modules API.

#### Advanced information

This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section.
Expand Down Expand Up @@ -391,6 +396,11 @@ This script automatically use the optimized config but sometimes you may want to
- `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`.
- `--output-path` – Allows customization of the output directory. Default is `build`.

Experimental support for the block.json `viewModule` field is available via the
`--experimental-modules` option. With this option enabled, script and module fields will all be
compiled. The `viewModule` field is analogous to the `viewScript` field, but will compile a module
and should be registered in WordPress using the Modules API.

#### Advanced information

This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section.
Expand Down Expand Up @@ -723,8 +733,8 @@ module.exports = {

If you follow this approach, please, be aware that:

- You should keep using the `wp-scripts` commands (`start` and `build`). Do not use `webpack` directly.
- Future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading.
- You should keep using the `wp-scripts` commands (`start` and `build`). Do not use `webpack` directly.
- Future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading.

## Contributing to this package

Expand Down
196 changes: 131 additions & 65 deletions packages/scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' );
const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' );
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
const { DefinePlugin } = require( 'webpack' );
const webpack = require( 'webpack' );
const browserslist = require( 'browserslist' );
const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' );
const { basename, dirname, resolve } = require( 'path' );
Expand All @@ -30,6 +30,9 @@ const {
getWordPressSrcDirectory,
getWebpackEntryPoints,
getRenderPropPaths,
getAsBooleanFromENV,
getBlockJsonModuleFields,
getBlockJsonScriptFields,
} = require( '../utils' );

const isProduction = process.env.NODE_ENV === 'production';
Expand All @@ -39,6 +42,9 @@ if ( ! browserslist.findConfig( '.' ) ) {
target += ':' + fromConfigRoot( '.browserslistrc' );
}
const hasReactFastRefresh = hasArgInCLI( '--hot' ) && ! isProduction;
const hasExperimentalModulesFlag = getAsBooleanFromENV(
'WP_EXPERIMENTAL_MODULES'
);

/**
* The plugin recomputes the render paths once on each compilation. It is necessary to avoid repeating processing
Expand Down Expand Up @@ -110,10 +116,10 @@ const cssLoaders = [
},
];

const config = {
/** @type {webpack.Configuration} */
const baseConfig = {
mode,
target,
entry: getWebpackEntryPoints,
output: {
filename: '[name].js',
path: resolve( process.cwd(), 'build' ),
Expand Down Expand Up @@ -165,7 +171,7 @@ const config = {
module: {
rules: [
{
test: /\.(j|t)sx?$/,
test: /\.m?(j|t)sx?$/,
exclude: /node_modules/,
use: [
{
Expand Down Expand Up @@ -245,21 +251,72 @@ const config = {
},
],
},
stats: {
children: false,
},
};

// WP_DEVTOOL global variable controls how source maps are generated.
// See: https://webpack.js.org/configuration/devtool/#devtool.
if ( process.env.WP_DEVTOOL ) {
baseConfig.devtool = process.env.WP_DEVTOOL;
}

if ( ! isProduction ) {
// Set default sourcemap mode if it wasn't set by WP_DEVTOOL.
baseConfig.devtool = baseConfig.devtool || 'source-map';
}

// Add source-map-loader if devtool is set, whether in dev mode or not.
if ( baseConfig.devtool ) {
baseConfig.module.rules.unshift( {
test: /\.(j|t)sx?$/,
exclude: [ /node_modules/ ],
use: require.resolve( 'source-map-loader' ),
enforce: 'pre',
} );
}

/** @type {webpack.Configuration} */
const scriptConfig = {
...baseConfig,

entry: getWebpackEntryPoints( 'script' ),

devServer: isProduction
? undefined
: {
devMiddleware: {
writeToDisk: true,
},
allowedHosts: 'auto',
host: 'localhost',
port: 8887,
proxy: {
'/build': {
pathRewrite: {
'^/build': '',
},
},
},
},

plugins: [
new DefinePlugin( {
new webpack.DefinePlugin( {
// Inject the `SCRIPT_DEBUG` global, used for development features flagging.
SCRIPT_DEBUG: ! isProduction,
} ),
// During rebuilds, all webpack assets that are not used anymore will be
// removed automatically. There is an exception added in watch mode for
// fonts and images. It is a known limitations:
// https://github.com/johnagan/clean-webpack-plugin/issues/159
new CleanWebpackPlugin( {
cleanAfterEveryBuildPatterns: [ '!fonts/**', '!images/**' ],
// Prevent it from deleting webpack assets during builds that have
// multiple configurations returned in the webpack config.
cleanStaleWebpackAssets: false,
} ),

// If we run a modules build, the 2 compilations can "clean" each other's output
// Prevent the cleaning from happening
! hasExperimentalModulesFlag &&
new CleanWebpackPlugin( {
cleanAfterEveryBuildPatterns: [ '!fonts/**', '!images/**' ],
// Prevent it from deleting webpack assets during builds that have
// multiple configurations returned in the webpack config.
cleanStaleWebpackAssets: false,
} ),

new RenderPathsPlugin(),
new CopyWebpackPlugin( {
patterns: [
Expand All @@ -269,27 +326,33 @@ const config = {
noErrorOnMissing: true,
transform( content, absoluteFrom ) {
const convertExtension = ( path ) => {
return path.replace( /\.(j|t)sx?$/, '.js' );
return path.replace( /\.m?(j|t)sx?$/, '.js' );
};

if ( basename( absoluteFrom ) === 'block.json' ) {
const blockJson = JSON.parse( content.toString() );
[ 'viewScript', 'script', 'editorScript' ].forEach(
( key ) => {
if ( Array.isArray( blockJson[ key ] ) ) {
blockJson[ key ] =
blockJson[ key ].map(
convertExtension
);
} else if (
typeof blockJson[ key ] === 'string'
) {
blockJson[ key ] = convertExtension(
blockJson[ key ]
);

[
getBlockJsonScriptFields( blockJson ),
getBlockJsonModuleFields( blockJson ),
].forEach( ( fields ) => {
if ( fields ) {
for ( const [
key,
value,
] of Object.entries( fields ) ) {
if ( Array.isArray( value ) ) {
blockJson[ key ] =
value.map( convertExtension );
} else if (
typeof value === 'string'
) {
blockJson[ key ] =
convertExtension( value );
}
}
}
);
} );

return JSON.stringify( blockJson, null, 2 );
}
Expand Down Expand Up @@ -317,52 +380,55 @@ const config = {
process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(),
// MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript.
new MiniCSSExtractPlugin( { filename: '[name].css' } ),
// React Fast Refresh.
hasReactFastRefresh && new ReactRefreshWebpackPlugin(),
// WP_NO_EXTERNALS global variable controls whether scripts' assets get
// generated, and the default externals set.
! process.env.WP_NO_EXTERNALS &&
new DependencyExtractionWebpackPlugin(),
].filter( Boolean ),
stats: {
children: false,
},
};

// WP_DEVTOOL global variable controls how source maps are generated.
// See: https://webpack.js.org/configuration/devtool/#devtool.
if ( process.env.WP_DEVTOOL ) {
config.devtool = process.env.WP_DEVTOOL;
}
if ( hasExperimentalModulesFlag ) {
/** @type {webpack.Configuration} */
const moduleConfig = {
...baseConfig,

if ( ! isProduction ) {
// Set default sourcemap mode if it wasn't set by WP_DEVTOOL.
config.devtool = config.devtool || 'source-map';
config.devServer = {
devMiddleware: {
writeToDisk: true,
entry: getWebpackEntryPoints( 'module' ),

experiments: {
...baseConfig.experiments,
outputModule: true,
},
allowedHosts: 'auto',
host: 'localhost',
port: 8887,
proxy: {
'/build': {
pathRewrite: {
'^/build': '',
},

output: {
...baseConfig.output,
module: true,
chunkFormat: 'module',
library: {
...baseConfig.output.library,
type: 'module',
},
},

plugins: [
new webpack.DefinePlugin( {
// Inject the `SCRIPT_DEBUG` global, used for development features flagging.
SCRIPT_DEBUG: ! isProduction,
} ),
// The WP_BUNDLE_ANALYZER global variable enables a utility that represents
// bundle content as a convenient interactive zoomable treemap.
process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(),
// MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript.
new MiniCSSExtractPlugin( { filename: '[name].css' } ),
// React Fast Refresh.
hasReactFastRefresh && new ReactRefreshWebpackPlugin(),
// WP_NO_EXTERNALS global variable controls whether scripts' assets get
// generated, and the default externals set.
! process.env.WP_NO_EXTERNALS &&
new DependencyExtractionWebpackPlugin(),
].filter( Boolean ),
};
}

// Add source-map-loader if devtool is set, whether in dev mode or not.
if ( config.devtool ) {
config.module.rules.unshift( {
test: /\.(j|t)sx?$/,
exclude: [ /node_modules/ ],
use: require.resolve( 'source-map-loader' ),
enforce: 'pre',
} );
module.exports = [ scriptConfig, moduleConfig ];
} else {
module.exports = scriptConfig;
}

module.exports = config;
4 changes: 4 additions & 0 deletions packages/scripts/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const EXIT_ERROR_CODE = 1;

process.env.NODE_ENV = process.env.NODE_ENV || 'production';

if ( hasArgInCLI( '--experimental-modules' ) ) {
process.env.WP_EXPERIMENTAL_MODULES = true;
}

if ( hasArgInCLI( '--webpack-no-externals' ) ) {
process.env.WP_NO_EXTERNALS = true;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/scripts/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const { sync: resolveBin } = require( 'resolve-bin' );
const { getArgFromCLI, getWebpackArgs, hasArgInCLI } = require( '../utils' );
const EXIT_ERROR_CODE = 1;

if ( hasArgInCLI( '--experimental-modules' ) ) {
process.env.WP_EXPERIMENTAL_MODULES = true;
}

if ( hasArgInCLI( '--webpack-no-externals' ) ) {
process.env.WP_NO_EXTERNALS = true;
}
Expand Down
41 changes: 41 additions & 0 deletions packages/scripts/utils/block-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const moduleFields = new Set( [ 'viewModule' ] );
const scriptFields = new Set( [ 'viewScript', 'script', 'editorScript' ] );

/**
* @param {Object} blockJson
* @return {null|Record<string, unknown>} Fields
*/
function getBlockJsonModuleFields( blockJson ) {
let result = null;
for ( const field of moduleFields ) {
if ( Object.hasOwn( blockJson, field ) ) {
if ( ! result ) {
result = {};
}
result[ field ] = blockJson[ field ];
}
}
return result;
}

/**
* @param {Object} blockJson
* @return {null|Record<string, unknown>} Fields
*/
function getBlockJsonScriptFields( blockJson ) {
let result = null;
for ( const field of scriptFields ) {
if ( Object.hasOwn( blockJson, field ) ) {
if ( ! result ) {
result = {};
}
result[ field ] = blockJson[ field ];
}
}
return result;
}

module.exports = {
getBlockJsonModuleFields,
getBlockJsonScriptFields,
};
Loading

0 comments on commit 240f303

Please sign in to comment.