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

Rewrite sideEffects flags to use only positive patterns #26452

Merged
merged 2 commits into from
Oct 27, 2020

Conversation

jsnajdr
Copy link
Member

@jsnajdr jsnajdr commented Oct 26, 2020

Since v5, webpack doesn't support extended globs in the sideEffects flag, which also include the ! operator to negate glob conditions. We therefore need to rewrite our patterns to be positive 🙂

There are several categories of packages and side effects in the Gutenberg repo, and the set of used combinations is rather small:

  • some packages are not libraries that would have many exports to use by consumers, but are apps or plugins that have one entrypoint function (usually called like initialize()). They don't need to declare any side effects, as it's expected to bundle and use the entire package. Examples are edit-post, edit-site, edit-navigation, edit-widgets. In the Gutenberg plugin, they are used by PHP pages that load the script and then call, e.g., window.wp.editWidgets.initialize(). Then there's the block-directory plugin that is initialized simply by loading the script. From these packages, I removed the existing sideEffects declarations from package.json. The flag defaults to sideEffects: true, as desired.

  • some packages register a data store. Then the files that have side effects are store/index.js (contains the registerStore call) and index.js (contains the import './store' call, which is itself a side effect that needs to be declared)

  • some packages install hooks (actions or filters). That usualy adds hooks/** as side-effectful, and also the root index.js that does import './hooks'

  • some packages have *.scss files in their sources and produce a build-style directory. This directory needs to be declared as side-effectful.

I'm not sure whether we need to declare src/**/*.scss, too. These files are every directly imported only by the React Native code. I don't know how that is bundled.

Todo:

  • update the documentation. So far, I only removed the obsolete parts and didn't replace them with anything new.
  • check that we are declaring styles correctly. I'm unsure about what's needed here. Who is importing the files from build-style and do they need them declared as side-effectful?

This PR is a spinoff from the big webpack 5 upgrade branch in #26382.

@gziolo
Copy link
Member

gziolo commented Oct 26, 2020

This PR is something that @sgomes is the most suited person to review :)

@gziolo gziolo added the npm Packages Related to npm packages label Oct 26, 2020
Copy link
Contributor

@sgomes sgomes left a comment

Choose a reason for hiding this comment

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

Thank you, @jsnajdr! These changes look good to me.

I added some comments around problematic or confusing usage of side effects, not so much as something to be fixed in this PR (that would be way out of scope), but rather as a list of things that made this PR harder. Hopefully they can be fixed over time.

Before merging this we'd ideally do some testing with Gutenboarding or another consumer that doesn't make full use of everything, to see if the resulting bundle sizes don't change too much. In practice, that's a bit hard to do (at least with Gutenboarding), so we'll probably just have to push these changes live and see how they work. They seem safe in that they shouldn't break anything, but they may result in larger bundles for consumers.

@@ -23,10 +23,6 @@
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"sideEffects": [
Copy link
Contributor

Choose a reason for hiding this comment

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

This module seems to export an initialize method, so I'm wondering why the data setup isn't done through that, rather than as a side effect? Even if everything is indeed needed, it would be cleaner and easier to grok.

@@ -21,10 +21,6 @@
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"sideEffects": [
Copy link
Contributor

Choose a reason for hiding this comment

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

This package confuses me. How is it supposed to be used?

Is it meant to be a single side effect, by doing just import '@wordpress/block-directory';? I'm seeing a bunch of components in there that don't seem to be loaded directly, but I guess they're all loaded transitively?

Copy link
Member Author

Choose a reason for hiding this comment

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

block-directory is a JS plugin. You just load the script and it hooks into the wp.data, wp.hooks and wp.plugins APIs, adding a new feature to the editor.

If you were building your own app with the NPM packages, the package would be activated by a simple side-effectful import, as you wrote. The APIs it hooks into are (ideally) peer dependencies of the block-editor package.

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'm seeing a bunch of components in there that don't seem to be loaded directly, but I guess they're all loaded transitively?

These are probably all used to implement the plugin UI, rather than being exported for 3rd party consumers. Are there any specific components that look suspicious?

Copy link
Contributor

Choose a reason for hiding this comment

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

None in particular. I expect them all to be used to implement the plugin UI, yes, unless one of them got dropped over time.

Copy link
Member

Choose a reason for hiding this comment

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

We could make it side effects free. I didn't consider it when refactoring to behave like a plugin. Edit family of packages uses initialize methods and we could offer something similar. Definitely a follow up task if you feel like it's worth the hassle.

Would it be useful if more functionality would be initialized this way?

Copy link
Contributor

@sgomes sgomes Oct 27, 2020

Choose a reason for hiding this comment

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

Great question, @gziolo!

In general, I feel that any package that could be classified as a library/plugin/utility (that is, something that doesn't invert control) should avoid side effects as much as possible.

Even setting aside the issues with bundlers for a second, initialisation functions generally provide more flexibility, because the consumer has more control over when they're invoked. Consumers can e.g. import something statically and call initialize() only when the editor is being loaded; without an initialisation method, they'd have to instead use a dynamic import, which by its async nature could be hard to fit into an existing codebase.

In terms of difficulty, it's generally a trivial amount of extra work for the naive use-case anyway. It's really not too bad to go from:

import 'side-effectful-module';

to

import { initialize } from 'side-effect-free-module';
initialize();

This way, users who don't care still get their initialisation at the top level of their module, while users who do care can move it elsewhere.

And on the package side, it reduces guesswork and maintenance around the sideEffects property in package.json, as this PR sadly shows 😕

So while I don't think it's anything urgent that needs immediate attention, I'd definitely recommend switching to initialisation methods whenever the next code revamp opportunity comes along 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

Would it be useful if more functionality would be initialized this way?

What @sgomes said 🙂 I'll only add that the inflexible initialization was sometimes causing problems in the first Gutenlypso integration 2 years ago. The problematic code usually looks like this:

import '@wordpress/rigid';
import flexible from '@wordpress/flexible';

getInitData().then( initData => {
  flexible.init( initData );
  // I'd love to do the following, but can't
  // rigid.init();
} );

The rigid package needs to be initialized only after flexible (because, for example, it does select( 'core/flexible' ) during initialization), but I can't do that. import statements must be at the top level of the module, not inside a callback.

Solutions are either using require( '@wordpress/rigid' ), or creating an extra async-loaded module that is loaded just-in-time.

On the other hand, I think that declaring sideEffects on absolutely everything is not needed. sideEffects: false is useful for libraries where you can use just a little part of the library and want to avoid bundling the rest.

For plugins like block-directory or apps like edit-post we can keep the default where everything is assumed to have side effects, and I don't think it causes much deoptimization if any.

Copy link
Member

Choose a reason for hiding this comment

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

In WordPress core we would have to call wp.blockLibrary.initialize() but it wouldn't be that much work. It's definitely something to keep in mind when working on new packages.

@@ -22,10 +22,6 @@
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"sideEffects": [
Copy link
Contributor

Choose a reason for hiding this comment

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

This package has a whole bunch of components, and this way none of them are modularised 😞 I think that it loads all of them anyway, so it should be fine, but it's hard to tell. And since there's an initialize method, I believe this could be done without side effects here too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean components like PluginBlockSettingsMenuItem that are reexported by edit-post/src/index.js? These are not really components. It's a plugin API that exposes some areas in UI where you can insert your own UI. More precisely, it's a Fill component that inserts something into a sibling Slot, where the pair is created by a createSlotFill call.

If you don't use these pseudo-components, it doesn't mean they can be removed from the bundle. The corresponding Slots are still there.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, you're right, I missed the re-exports at the end of the file, my bad 👍

@@ -22,10 +22,6 @@
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"sideEffects": [
Copy link
Contributor

Choose a reason for hiding this comment

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

See above.

@@ -22,10 +22,6 @@
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"sideEffects": [
Copy link
Contributor

Choose a reason for hiding this comment

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

See above.

@github-actions
Copy link

Size Change: +66 B (0%)

Total Size: 1.2 MB

Filename Size Change
build/annotations/index.js 3.54 kB +2 B (0%)
build/blob/index.js 665 B +1 B
build/block-directory/index.js 8.57 kB -3 B (0%)
build/block-editor/index.js 130 kB -1 B
build/block-library/index.js 146 kB -4 B (0%)
build/block-serialization-default-parser/index.js 1.78 kB +1 B
build/components/index.js 172 kB +6 B (0%)
build/compose/index.js 9.62 kB +1 B
build/data/index.js 8.61 kB -1 B
build/date/index.js 31.9 kB +1 B
build/edit-navigation/index.js 10.7 kB +3 B (0%)
build/edit-post/index.js 306 kB +26 B (0%)
build/edit-site/index.js 22.2 kB +1 B
build/edit-widgets/index.js 26.6 kB +36 B (0%)
build/editor/index.js 42.6 kB -3 B (0%)
build/format-library/index.js 7.47 kB +2 B (0%)
build/hooks/index.js 1.75 kB +1 B
build/is-shallow-equal/index.js 713 B +1 B
build/keycodes/index.js 1.84 kB +1 B
build/list-reusable-blocks/index.js 3.01 kB +1 B
build/nux/index.js 3.26 kB +1 B
build/plugins/index.js 2.44 kB -2 B (0%)
build/redux-routine/index.js 2.85 kB -2 B (0%)
build/reusable-blocks/index.js 3.05 kB -1 B
build/rich-text/index.js 13 kB -2 B (0%)
build/server-side-render/index.js 2.6 kB -1 B
build/shortcode/index.js 1.69 kB -1 B
build/url/index.js 4.05 kB +1 B
build/viewport/index.js 1.74 kB -1 B
build/warning/index.js 1.14 kB +1 B
build/wordcount/index.js 1.22 kB +1 B
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/api-fetch/index.js 3.34 kB 0 B
build/autop/index.js 2.73 kB 0 B
build/block-directory/style-rtl.css 943 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-editor/style-rtl.css 11 kB 0 B
build/block-editor/style.css 11 kB 0 B
build/block-library/editor-rtl.css 8.93 kB 0 B
build/block-library/editor.css 8.93 kB 0 B
build/block-library/style-rtl.css 7.75 kB 0 B
build/block-library/style.css 7.75 kB 0 B
build/block-library/theme-rtl.css 741 B 0 B
build/block-library/theme.css 741 B 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 47.6 kB 0 B
build/components/style-rtl.css 15.3 kB 0 B
build/components/style.css 15.3 kB 0 B
build/core-data/index.js 12.1 kB 0 B
build/data-controls/index.js 679 B 0 B
build/deprecated/index.js 769 B 0 B
build/dom-ready/index.js 571 B 0 B
build/dom/index.js 4.42 kB 0 B
build/edit-navigation/style-rtl.css 881 B 0 B
build/edit-navigation/style.css 885 B 0 B
build/edit-post/style-rtl.css 6.37 kB 0 B
build/edit-post/style.css 6.35 kB 0 B
build/edit-site/style-rtl.css 3.79 kB 0 B
build/edit-site/style.css 3.79 kB 0 B
build/edit-widgets/style-rtl.css 3.09 kB 0 B
build/edit-widgets/style.css 3.09 kB 0 B
build/editor/editor-styles-rtl.css 480 B 0 B
build/editor/editor-styles.css 482 B 0 B
build/editor/style-rtl.css 3.85 kB 0 B
build/editor/style.css 3.84 kB 0 B
build/element/index.js 4.44 kB 0 B
build/escape-html/index.js 735 B 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/html-entities/index.js 623 B 0 B
build/i18n/index.js 3.55 kB 0 B
build/keyboard-shortcuts/index.js 2.38 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/media-utils/index.js 5.11 kB 0 B
build/notices/index.js 1.69 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/primitives/index.js 1.35 kB 0 B
build/priority-queue/index.js 790 B 0 B
build/token-list/index.js 1.24 kB 0 B

compressed-size-action

@jsnajdr
Copy link
Member Author

jsnajdr commented Oct 27, 2020

@sgomes I added some missing declarations for src/**/*.scss files in b80f0c3

The rules for styles are:

  • if the src folder contains any *.scss files, they should be declared. The src folder is shipped as part of the published NPM package, and *.scss imports from there would be optimized out otherwise
  • if there are any *.scss files directly in src/ (not in a subfolder), these are compiled into a build-style directory. That means that not every package that has *.scss files in src/ also has build-style and needs to declare it.

@gziolo I have a local Node script that goes through the package/* directories and their package.json files and verifies that the sideEffect rules for style files are followed. What's the best way to make that a part of the linting or pre-commit check process?

@sgomes
Copy link
Contributor

sgomes commented Oct 27, 2020

@sgomes I added some missing declarations for src/**/*.scss files in b80f0c3

The rules for styles are:

  • if the src folder contains any *.scss files, they should be declared. The src folder is shipped as part of the published NPM package, and *.scss imports from there would be optimized out otherwise
  • if there are any *.scss files directly in src/ (not in a subfolder), these are compiled into a build-style directory. That means that not every package that has *.scss files in src/ also has build-style and needs to declare it.

Thanks, @jsnajdr! I've taken a look, and everything looks correct to me, according to the rules you've described 👍

We could probably go as far as proactively adding the style declarations to every package, regardless of whether they have any styles, but you never know if that might cause a particular bundler or bundler version to break. Your linter approach is definitely safer 👍

@jsnajdr jsnajdr merged commit 325498e into master Oct 27, 2020
@jsnajdr jsnajdr deleted the update/positive-side-effect-flags branch October 27, 2020 12:20
@github-actions github-actions bot added this to the Gutenberg 9.3 milestone Oct 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
npm Packages Related to npm packages [Type] Build Tooling Issues or PRs related to build tooling
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants