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

Dependency Extraction Webpack Plugin impaired DevX #35630

Open
tomalec opened this issue Oct 14, 2021 · 6 comments
Open

Dependency Extraction Webpack Plugin impaired DevX #35630

tomalec opened this issue Oct 14, 2021 · 6 comments

Comments

@tomalec
Copy link
Contributor

@tomalec tomalec commented Oct 14, 2021

Forgive me for the generic title, but I'd like to tackle many interconnected issues related to Dependency Extraction Webpack Plugin (DEWP) and its DevX, as I believe we need a holistic view to solve those.

TL;DR
Plugin developers have no control or even introspection over the extracted dependencies. Not only the versions but also the packages being extracted. Which heavily decreases the quality of our products and the maintenance cost.

I'm trying to gather here the problems and ideas. So, we could refer to this issue from GH issues and PRs with potential solutions (like #35106 (comment)), without losing the overview of the problem.

Context

I'm a WooCommerce(WC) plugin developer, so the perspective below could be WC-oriented. However, I believe the problems stated here are not unique to WooCommerce or my plugin and apply to any other WordPress plugin. WooCommerce adds just another layer of dependencies.

Dependency Extraction Webpack Plugin (DEWP)

AFAIK, the main goal and reason why we use DEWP are to avoid delivering duplicated JS packages from plugins to a single WordPress (WP) site. It reduces the network and CPU cost for users and saves us from a number of problems related to the packages which have a side effect, which may collide when imported/used twice.

I see dependency extraction as a nice way to address that, also I believe that customers' costs and experience should be the top priority when looking for a solution.

But the way our tool works introduces a number of struggles for developers. IMHO, the impact is severve. I noticed a number of people working on different plugins and products discussing their struggles with DEWP. To somewhat evaluate the impact in the plugin I work for, I marked all issues that are affected by this problem (However, it doesn't include everyday frustration)

Where we are at?

Let’s draw a picture of what dependencies and problems we face while developing a plugin.

Naturally, we have (let’s call them “main”) dependencies: WordPress and WooCommerce. We have a strict requirement – driven by the actual merchant/customer expectation – to support at least a few versions behind, not only the latest, as updating the stack is a cost and burden for our merchants. However, given the finite resources we have, we’d rather balance it not to support too many combinations with legacy code. We have a defined process to track updates of those, and a tool (wc-compat-checker) to support the PHP side of it. But it's still manual labor, and we do not have much tooling to support the person doing a compatibility check on the JS side.

We have “granular” dependencies – individual npm packages stated in package.json. There are also composer dependencies, but I’m not sure if they are also that problematic. I guess, there is no customer-driven requirement for those. I doubt we have customer requests like “Hey, I’d like AutomateWoo to work with my @woocommerce/number@1.9.0-based store”. However, we do have some constraint that comes from using many dependencies – they need to be cross-compatible with each other and with the WordPress & WooCommerce being used.

Then we have dev dependencies, for which the merchant should not care about at all. Unfortunately, they are also tied to WP/WC versions being in use.

Problems

The above creates three fundamental problems:

  1. If every plugin would bundle all their dependencies, there would be a lot of duplicates, pushing the unnecessarily big cost of the transfer, parsing, and execution to the merchant, and their customers.
  2. The number of dependencies multiplied by the range of WP & WC versions supported is a maintenance cost for developers to track, update, resolve compatibility conflicts.
  3. Sometimes, the versions being used, or supported are not easy to find and match. Like @woocommerce/components version being used for respective WC version.
    • What's even worse it sometimes happens to be an unreleased node from the git tree, not even something published to npm. So, there is even no version to look for. See the woocommerce/woocommerce-admin#7628.

To solve/mitigate the first problem, there are @wordpress/ and @woocommerce/dependency-extraction-webpack-plugin. However, the way it works makes the whole development quite indeterministic: which packages are extracted, which main or granular dependency is actually used in runtime? It’s being extracted and used blindly, regardless of versions, without even reporting what was extracted, and what is assured to have a specific version. That created a bunch of other problems:

  1. As a plugin developer, I can’t assert/specify the version of my granular dependency, like @woocommerce/components. Unless, I carefully, manually, and deeply curated and extracted all the granular dependency trees across all (minor and patch) versions of main dependencies. Then all I get is still a range of versions.
    This is very time-consuming labor, that needs to be done very often. When adding a dependency, when updating anything, when debugging an issue, when checking compatibility. Currently, there is no tool to support that or even a specific list of resources to track.
  2. As a plugin developer, I don't even know what dependencies are extracted, as the list is maintained in the package repo, and not reported while bundling.
    That may result in unexpected behavior and error. Checking that again requires even more manual effort. As it requires digging into the source code of DEWP at a given version and manually comparing packages list.
  3. As a merchant, support engineer, or dev investigating the issue, I cannot easily, precisely tell what versions of packages are being used for the given setup/instance.
    That makes reproducing the reported problems harder and more time-consuming, eventually affecting customer experience.
  4. Running automated unit tests could give a false impression, that the speced behavior is covered, even though it may not be true, as the unit tests run against granular dependency versions specified in the local package.json while the one run by the customer is totally different.
  5. In our CI we would have to run our unit and E2E tests for all the combinations. That would cost us CI time and again is hard to specify, and pretty volatile over time. Moreover, we cannot even state which exact combination is run, to be able to assert at least one.

In a Slack discussion, @nerrad suggested implementing the L-2 policy, and supporting only two versions behind, then supporting the lowest versions available. This naturally limits the ranges and number of combinations but does not tackle the problems themselves. Plus, may fail if any main dependency decreases the version of its dependency, or introduce backward-incompatible change.

Cost of status quo

In my opinion, the above impair not only DevX but also innovation and the quality of our products.

Innovation

  • The delay of many versions between releasing the features and being able to use them slows down the adoption.
  • The dependant project cannot early-adopt the new features, and cannot upstream local experiments instantly to the used components

Quality

  • If the dependants cannot switch to the latest version, they do not use it => they do not test it on time. Two versions later the feedback on some fundamental issues may be too late.
  • the bugs become stale and becomes the “features” we need to support for backward compatibility
  • If the dependency version becomes too hard to manage, and a dev becomes too frustrated to work it around, they may include it in the local package. Forcing the customer to pay for the DevX problem we created ourselves.
  • If the dependant project cannot use the latest, fixed version, they need to incorporate workarounds, which may have side effects, own bugs, require maintenance, which reduces the resources for other areas of the project.
  • … workarounds usually take space and CPU time which again is pushing the cost on the customer.

Ways to go

I think when looking for a solution for the problems stated above, we could take a few (non excluding) strategies:

  1. Replace DEWP with some other solution
  2. Change the way DEWP works
  3. Improve developer experience and quality assurance with the DEWP as it is, with tooling around and minor tweaks.

Personally, I'd start from the latter, as it's the cheapest to start with (in time, effort, and chaos it'd generate).

//cc all folks who I noticed discussing related ideas: @ecgan, @roo2, @scinos, @jsnajdr, @fullofcaffeine, @noahtallen, @sirreal, @gziolo, @mcsf, @nerrad


Solutions

I don’t have any precise well-established solution for the above. I’d rather start a discussion about those. But here are few ideas I managed to gather or come to my mind. I put them in separate comments, for easier reference.

@tomalec
Copy link
Contributor Author

@tomalec tomalec commented Oct 14, 2021

L-2 policy

L-2/L-n policy means that the plugin should support only the latest n versions behind the current one.
In regards to WordPress & WooCommerce for sure would help to limit the number of combinations we test and check. I'd add one additional rule to that, to be able not to limit the innovation, use the root fixes, instead of working them around over and over again. I’d say we should make it so: “We support version of main dependency no older than n, but occasionally we may still use the newer versions of granular dependencies”

It's to be implemented on the plugin level only.
It helps with Problems 2-6 but puts pressure on merchants to frequently update, so we need to balance the n. Plus, it does not solve the problem at all, just limits its range.

I think it's a nice policy to start with, but we should not stop here, as we still face all the DevX frustration even with n=2.

@tomalec
Copy link
Contributor Author

@tomalec tomalec commented Oct 14, 2021

Build/automate WP, WC, plugin version maps

I think that all the data is there (somewhere) we just need a nice and automated way to gather it all in a single place. As mentioned in the comment by @jsnajdr

We could be maintaining machine-readable information about which WordPress version ships which versions of the Gutenberg plugin and the NPM libraries

I’d only add to make it machine and human-readable, and for WooCommerce as well.

Conceptually it’s gathering all package.jsons for respective WP, WC, DEWP, and plugin versions and putting them in a nice table. I’d also add there the WP versions supported by a specific WC version.

Thanks to that a developer, support engineer, merchant, further tools could inspect what versions are used where and decide upon that.
It will help in

  • debugging issues, to know in which version of a package should we look for a bug.
  • support engineers communicating with customers, as one could easily check that a given plugin uses a granular dependency that's far behind the WP they use, which may be a reason for problems
  • developing a plugin, as a developer could check:
    • which packages were actually extracted, where they should put additional caution
    • what granular dependency versions range is supported in the range of WP & WC they support, so know which features could be used, and which should be worked around
    • versions in the upcoming WP/WC release to be able to anticipate it and fix compatibility problems before they arrive to support engineers
  • testing a plugin, as we could set up automated tests for the combinations that would be actually in use
  • implementing further solutions for the dependency problems, as the build- and run-time code could take and process that information, to serve the code to be imported/executed.
  • gathering the data about the scale of the version problems, as we will finally see if/how far the ranges do not match. We could even track/report that data on production builds.

It solves 3, 5, 6; helps with 2, 4.
Requires action/changes in WordPress, WooCommerce, or DEWP repos.

I think it's something we should start with, as it would give us an insight and data to reason about the problem.

@tomalec
Copy link
Contributor Author

@tomalec tomalec commented Oct 14, 2021

Make Dependency Extraction Plugin handle dependency versions

I believe we have all the data in place to solve the problem, assuming we already have a way to get granular dependencies’ versions for a given WordPress version. The local package.json gives the versions for dependencies in us. WP version (range) is also given in the repo.

I’m not very experienced with Webpack. But to me, the feature that DEWP brings is similar to what native Import maps do: “Check what dependencies are already available in WP/WC and map those imports to external modules” (instead of adding and looking them up in the local bundle).

What’s cool about native import maps (besides the fact, it’s native, already available for free in Chromium, and does not require us to complicate our tools and stack) is that they seem to solve the problem of multiple versions problem – with scopes. So if someday we’ll switch to native ESM, we could use a single map for all plugins, without a need for each plugin adding DEWP to their tool stack.

I’ll use import maps syntax as I believe it’s clear and declarative enough to express the behavior we’d like to have:

Consider the given WooCommerce version uses

"@woocommerce/components": "5.1.2",
"@woocommerce/currency": "3.1.0",
"@woocommerce/number": "1.2.3",

Then the import map for a plugin that would like to use shared dependencies would look like

"imports": {
    "@woocommerce/components": "/wp-content/…/woocommerce-admin/…/components.js",
    "@woocommerce/currency": "/wp-content/…/woocommerce-admin/…/currency.js",
    "@woocommerce/number": "/wp-content/…/woocommerce-admin/…/number.js",
},

That’s what we have today. And AFAIK, that’s where DEWP functionality ends in terms of versions. But hopefully, we can add a bit of checking logic there.

Overlap

If our plugin uses

"@woocommerce/components": "^5.1.1",
"@woocommerce/number": ">=1.1.1 <1.2.0",
"fast-json-patch": "^3.0.0",

Then all the shared dependencies are there, so the bundle will include only fast-json-patch from gla/node_modules/, and the @woo… dependencies will be mapped as above.

Newer version

If our plugin uses

"@woocommerce/components": "^5.1.1",
"@woocommerce/number": "^2.0.0",
"fast-json-patch": "^3.0.0",

@woo…/components version matches, so will be removed from the bundle. But the …/number does not, so will be added to the bundle together with fast-json-patch, and the import map will be modified to look like this:

"imports": {
    "@woocommerce/components": "/wp-content/…/woocommerce-admin/…/components.js",
    "@woocommerce/currency": "/wp-content/…/woocommerce-admin/…/currency.js",
    "@woocommerce/number": "/wp-content/…/woocommerce-admin/…/number.js",
},
"scopes": {
    "/wp-content/plugins/gla/": {
        "@woocommerce/number": "/wp-content/…/gla/…/number.js",
    },
},

(So the imports originating from …/gla/ in GLA bundle will point to GLA-specific version)

Native import maps are resolved on the run-time, so they could use the WP/WC dependency maps from the currently running setup.
With Webpack, we need to do it on the build-time, so we would have to consider the ranges of granular dependency versions from the range of main dependencies. But the logic stays the same.

It solves 1, 2, 4, 5, 7, 8.
Requires action/changes in WordPress and DEWP repos.

@tomalec
Copy link
Contributor Author

@tomalec tomalec commented Oct 14, 2021

Add runtime import map shim/resolver

This one could be the trickiest to implement, but if a given WordPress version knows its own granular dependency versions and receive the plugin's map, then theoretically it knows whether the versions match. If so it would do what it does today, and return the import from the WP bundle. If not could point back to the plugins bundle. So the "extended" bundle would be requested only if the currently running WP environment does not have a matching dependency.

However, I don't know enough on how WP handles all the scripts, to propose something more precise, or judge how doable it is.

It may solve all the problems, but would require a lot of changes across the stack.

@scinos
Copy link
Contributor

@scinos scinos commented Oct 14, 2021

Just spitballing some ideas:


Sounds like Webpack Module Federation could help here. The official documentation says

Many applications share a common components library which could be built as a container with each component exposed. Each application consumes components from the components library container. Changes to the components library can be separately deployed without the need to re-deploy all applications. The application automatically uses the up-to-date version of the components library.

If we replace application -> plugin and components -> main dependencies, it really sound similar to the problem exposed above.


Maybe we can replace DEWP with Webpack DLLs. Maybe we can publish NPM packages with the DLLs for specific WP versions (eg: @wordpress/wp-dll). This package will contain a pre-built DLL that plugin authors must include in their Webpack compilation. It will take care of extract the main dependencies.

This package could also provide a set of peerDependencies with the exact version of the expected libraries (i.e like your "Conceptually it’s gathering all package.jsons for respective WP, WC, DEWP, and plugin versions and putting them in a nice table.", but in JSON format).

So MyPlugin depends on @wordpress/wp-dll@5.8.1. Via peerDependencies, that forces my plugin to also depend on specific versions of other dependencies (eg: @woocommerce/components@5.0.0). This allows me to run tests with the exact versions that will be used in prod. It also includes a DLL config that I need to plug into my Webpack config, so it knows which packages to extract.

Although that would mean I need to maintain several "lines" of MyPlugin: one compatible with @woocommerce/components@5.0.0, another one compatible with @woocommerce/components@4.0.0... So maybe this is not as good as it sounded in my head when I started this comment :)

@fabiankaegy
Copy link
Member

@fabiankaegy fabiankaegy commented Oct 14, 2021

I also have one thing that I think causes a lot of pain points when it comes to working with the DEWP. And that is the inability to tree shake. If you use a singular function from lodash for example it adds the dependency lodash and therefore the entire library gets loaded. Which is a lot of overhead for a small simple function. And since the request is an external running something like the WebpackBundleAnalyzer won't know about it and therefore it can be hard to spot the actual cost of importing a package.

Regarding your comments, I actually like the idea of a runtime import map shim/resolver quite a lot. Core currently doesn't ship with multiple versions of the packages and I'm not sure whether it would be wise to do so. Of course, you could always bundle all your dependencies with your plugin but as you mentioned that leads to the same code being imported by multiple plugins and therefore a lot of overhead. So the idea of only loading a bundle if the version isn't a match is very intriguing. I also have no clue however how feasible it would actually be to implement something like it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants