Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WP Scripts: Build block.json viewModule #57461

Merged
merged 21 commits into from
Jan 9, 2024
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
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' ),
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be refactored back to a callback function. There are two reasons:

  • compatibility with the existing projects that override entry points
  • more importantly, entry points are recomputed in the watch mode anymore

Copy link
Member Author

Choose a reason for hiding this comment

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

It is a callback, the signature changed so this now returns the callback:

/**
* Detects the list of entry points to use with webpack. There are three ways to do this:
* 1. Use the legacy webpack 4 format passed as CLI arguments.
* 2. Scan `block.json` files for scripts.
* 3. Fallback to `src/index.*` file.
*
* @see https://webpack.js.org/concepts/entry-points/
*
* @param {'script' | 'module'} buildType
*/
function getWebpackEntryPoints( buildType ) {
/**
* @return {Object<string,string>} The list of entry points.
*/
return () => {

I'm investigating the problem now.

Copy link
Member Author

Choose a reason for hiding this comment

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

more importantly, entry points are recomputed in the watch mode anymore

Do you mean if I change block.json (to add another file) when wp-scripts start --experimental-modules is running, the build doesn't pick it up?

I don't think this has changed at all for script builds or for the script compilation of module builds. I believe the copy webpack plugin is adding the block.json files to the build which makes webpack observe those changes.

Because the modules compilation doesn't include the copy plugins for block.json, when block.json changes the module compilation won't pick it up. I'll look into how to make it observe those changes.

Copy link
Member

Choose a reason for hiding this comment

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

It looks like it should work correctly for scripts when the is no experimental flag provided 👍

Copy link
Member Author

@sirreal sirreal Jan 17, 2024

Choose a reason for hiding this comment

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

Here's a PR to add block.json files as webpack file dependencies so they're watched from the module build: #57927


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.
Copy link
Member

Choose a reason for hiding this comment

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

It looks like React Fast Refresh was misplaced. It should remain here for the scripts configuration as it works withe the existing Babel config and the dev server.

It won’t work with the modules config as it’s missing proper Babel handling, the dev server integration. In addition, I’m not sure whether it’s even possible to make it work with Preact.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're absolutely correct, thanks: #57777

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(),
Copy link
Member

Choose a reason for hiding this comment

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

I don’t think React Fast Refresh will work out of the box for viewModule.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it may work, but I'm not really sure how to test. Interactivity uses preact, which I don't think is compatible with the react refresh work. I saw this issue about refresh support for preact preactjs/preact#2184

We probably just remove this for now for modules.

Copy link
Member

Choose a reason for hiding this comment

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

// 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
Loading