Skip to content

Webpack: opt-in filesystem cache + stop wiping .cache/ on clean - #49174

Merged
enejb merged 6 commits into
trunkfrom
add/webpack-filesystem-cache
Jun 15, 2026
Merged

enejb merged 6 commits into
trunkfrom
add/webpack-filesystem-cache

Conversation

@enejb

@enejb enejb commented May 26, 2026 •

Copy link
Copy Markdown
Member

Fixes # N/A

Proposed changes

Follow-up to #49173. Opt-in webpack filesystem cache + stops several clean scripts from wiping .cache/.

The fast-build PR can detect "this project's JS sources changed since last build" via per-project SHA stamps, but the actual rebuild for those projects still pays full webpack cold-start cost (~60–90s on plugins/jetpack's three concurrent bundles). Webpack's filesystem cache cuts incremental rebuilds to ~5–10s.

This PR ships the foundation plus the highest-impact adoption:

  • @automattic/jetpack-webpack-config now exports a cache( configFile ) helper. Consumers opt in by spreading cache: jetpackWebpackConfig.cache( __filename ) into their config. It returns { type: 'filesystem', cacheDirectory: path.resolve( process.cwd(), '.cache/webpack', <configFile basename> ), store: 'pack', buildDependencies: { config: [ configFile ] } }:
    • Passing the config's own path means the cache invalidates automatically when that config changes (buildDependencies.config), and is namespaced per config file so a project's multiple — possibly concurrently built — configs don't share, and clobber, a single pack.
    • name is left unset: webpack's default already namespaces by mode, keeping production and development caches separate.
    • Returns undefined when process.env.CI is set, since CI doesn't preserve the cache between builds.
    • README has a new section documenting it.
  • plugins/jetpack opts in across all three of its webpack configs (tools/webpack.config.js, webpack.config.extensions.js, webpack.config.widget-visibility.js) by passing cache: jetpackWebpackConfig.cache( __filename ). After the first cold build, subsequent rebuilds are cache-hit and dramatically faster.
  • clean script audit: eight packages (blaze, classic-theme-helper, forms, masterbar, paypal-payments, podcast, search, yoast-promo) had clean scripts of the form rm -rf <build-output>/ .cache/. The .cache/ wipe pre-dated this PR; per git archaeology it traces back to Search: Add essential scaffolding for package #21814 with no stated rationale (that PR also deleted vendor/node_modules, so it reads as an overzealous cleanup that was then copy-pasted around). Removed it from all eight so persistent build caches survive clean. Build-output dirs (dist/, build/) are still wiped as before.

Note this does change the babel cache's lifetime in those eight packages: .cache/babel/ was previously deleted on every clean, and now persists. Per @anomiex's experiment on this PR, dev and production builds of the eight packages from this branch produced output identical to trunk, so persisting the cache appears safe.

Other webpack-config consumers can opt in incrementally by passing cache: jetpackWebpackConfig.cache( __filename ) to their config — no required follow-up.

Why opt-in rather than always-on

If we forced cache: ... from inside webpack-config, every consumer would inherit it whether or not they're ready. Opt-in keeps each project's webpack config explicit about what it's doing, lets risky bundles (e.g. anything generating dynamic entry points) decide for themselves, and gives reviewers a single grep target.

Other information

  • Generate changelog entries for this PR (using AI).

Related product discussion/links

Follows up on #49173 (the jetpack fast-build CLI command).

Does this pull request change what data or activity we track or use?

No.

Testing instructions

Verify the cache export

node -e "const { cache } = require('./projects/js-packages/webpack-config/src/webpack'); console.log( cache('/some/project/tools/webpack.config.js') );"

Expected: an object with type: 'filesystem', store: 'pack', a cacheDirectory ending in .cache/webpack/webpack.config (namespaced by the config file's basename), and buildDependencies.config containing the path you passed. Run it again with CI=true set and it should print undefined instead.

Verify the cache works end-to-end on plugins/jetpack

  1. cd projects/plugins/jetpack && rm -rf .cache/webpack — start from a clean cache.
  2. Time the first build: time pnpm run build-widget-visibility (smallest bundle; or build-development for all). Note duration (cold).
  3. Without changing anything, time it again. Should be substantially faster — webpack reports [cached] … modules (locally: ~800ms → ~95ms for widget-visibility).
  4. ls .cache/webpack/ shows a populated, per-config-namespaced cache, e.g. .cache/webpack/webpack.config.widget-visibility/default-development/.

Verify clean no longer wipes .cache/

For any of the 8 packages (e.g. projects/packages/forms):

  1. Create a probe: mkdir -p .cache/babel && touch .cache/babel/probe.
  2. pnpm run clean, then check .cache/babel/probe still exists. The build-output dirs (dist/, build/) should be gone.

Verify no regression elsewhere

  • node --check projects/plugins/jetpack/tools/webpack.config.js — no errors (also for .extensions.js and .widget-visibility.js).
  • A full jetpack build plugins/jetpack should still produce correct output. The cache only affects build time, not output.

@enejb
enejb requested a review from a team as a code owner May 26, 2026 20:44
@enejb enejb added the [Status] Needs Review This PR is ready for review. label May 26, 2026
@enejb enejb self-assigned this May 26, 2026
@github-actions

github-actions Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • 🔴 Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: July 7, 2026
    • Code freeze: July 6, 2026

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@github-actions

github-actions Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the add/webpack-filesystem-cache branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/webpack-filesystem-cache
bin/jetpack-downloader test jetpack-mu-wpcom-plugin add/webpack-filesystem-cache

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@jp-launch-control

jp-launch-control Bot commented May 26, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

This PR did not change code coverage!

That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷

Full summary · PHP report · JS report

@anomiex anomiex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like it may be worth pursuing for its own sake, not only as a follow-up to #49173.

But before changing this, it may be worth looking into why we're wiping the .cache dirs in the first place.

The babel cache at .cache/babel/ is unaffected.

It's completely affected, in that it's no longer being deleted on every build.

Unfortunately git archaeology doesn't turn up anything obvious. The first place adding it seems to be #21814, which has no explanation for why it's doing it; the original version of the PR also deleted vendor and node_modules too, so it may have just been an overzealous cleanup. Most of the other additions I see seem like they may well just be copy-pasting from that.

Lacking any obvious reason for the deletion, I decided to try a simple experiment: I did a build of the eight packages from trunk, and then a dev build of the eight packages with this PR, and then a production build of the eight packages from this PR. The two production builds were the same, so 🤷 it's probably not that, at least.

@@ -96,6 +96,24 @@ const watchOptions = {
ignored: [ '**/node_modules', '**/dist', '**/vendor' ],
};

// Filesystem cache rooted at the consumer's project directory. Opt-in: consumers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Everything at the top level is opt-in. Usage instructions are in the readme. There's no point to having this gigantic comment here too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed — trimmed to a two-line note; usage lives only in the readme now.

Comment on lines +104 to +109
// To invalidate the cache after upgrading webpack-config or changing your own
// webpack config, delete `.cache/webpack/` in the project. (We deliberately don't
// list config files in `buildDependencies.config` from here — the consumer's config
// path isn't known to this module. Consumers can extend this object with their own
// `buildDependencies.config: [ require.resolve('./webpack.config.js') ]` if they
// want config-change invalidation.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems rather unfortunate, and is probably part of the reason we delete the .cache dir in the first place.

You might do better by exporting a function rather than an object, and requiring the caller to pass in __filename / import.meta.filename so it can do things more correctly by default.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. cache is now cache( configFile ); callers pass __filename, and it sets buildDependencies: { config: [ configFile ] } so the cache invalidates when the config changes.

const cache = {
type: 'filesystem',
cacheDirectory: path.resolve( process.cwd(), '.cache/webpack' ),
name: `jetpack-${ mode }`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems the default for name already includes the mode. We might do better leaving this unset.

https://webpack.js.org/configuration/cache/#cachename

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Dropped the explicit name (the hardcoded jetpack- was also wrong for a package shared beyond Jetpack).

One caveat surfaced while doing it: the three plugins/jetpack configs run as separate, concurrent webpack --config processes (see the watch / concurrently script) and none sets a top-level config name. With name unset they would all default to the same name and share — and clobber — a single pack under .cache/webpack. Since the helper now receives the config path, I namespace cacheDirectory per config file (.cache/webpack/<config basename>/) to keep them isolated. Verified a widget-visibility build lands in .cache/webpack/webpack.config.widget-visibility/default-development/, which also confirms the default name still namespaces by mode.

Comment thread projects/js-packages/webpack-config/src/webpack.js
Significance: patch
Type: changed

Build: stop wiping `.cache/` on `pnpm clean` so persistent build caches (babel, webpack) survive cleans.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

End users of the package won't care about this. No point in putting it in the changelog.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Converted these to a Comment: field instead of a user-facing entry, so the change is still recorded (and the changelog-presence check still passes) without adding a line to the published changelog. Applied to all eight packages.

Significance: patch
Type: changed

Build: stop wiping `.cache/` on `pnpm clean` so persistent build caches (babel, webpack) survive cleans.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same.

Significance: patch
Type: changed

Build: stop wiping `.cache/` on `pnpm clean` so persistent build caches (babel, webpack) survive cleans.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same.

Significance: patch
Type: changed

Build: stop wiping `.cache/` on `pnpm clean` so persistent build caches (babel, webpack) survive cleans.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same.

Significance: patch
Type: changed

Build: stop wiping `.cache/` on `pnpm clean` so persistent build caches (babel, webpack) survive cleans.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same.

Significance: patch
Type: changed

Build: enable webpack's filesystem cache for the main, extensions, and widget-visibility bundles. Reduces local incremental rebuild time substantially after the first build; no effect on production output.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

End users of the plugin really won't care about this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same treatment — converted to a Comment: entry (and switched Type to other, since the plugin changelog vocabulary does not include changed).

@enejb

enejb commented May 30, 2026

Copy link
Copy Markdown
Member Author

Pushed a round addressing the review:

  • cache is now cache( configFile ) — caller passes __filename; sets buildDependencies.config for config-change invalidation, namespaces cacheDirectory per config file (avoids the concurrent plugins/jetpack configs sharing one pack), leaves name unset, and returns undefined under CI.
  • Giant code comment removed; readme updated for the function form.
  • The eight package changelogs and the plugin changelog are now Comment:-only (no user-facing lines).

On the babel cache: you are right, my wording was backwards — it is very much affected, in that .cache/babel/ now survives clean rather than being deleted each time. Corrected the PR description accordingly, and folded in your #21814 archaeology and the dev-vs-prod identical-output check there.

@github-actions github-actions Bot added [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. and removed [Status] Needs Review This PR is ready for review. labels May 30, 2026

@anomiex anomiex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking good! I left some suggestions on the documentation.

The eight package changelogs and the plugin changelog are now Comment:-only (no user-facing lines).

It seems instead they got deleted entirely?

Comment thread projects/js-packages/webpack-config/src/webpack.js Outdated
Comment thread projects/js-packages/webpack-config/src/webpack.js
Comment thread projects/js-packages/webpack-config/README.md Outdated
Comment thread projects/js-packages/webpack-config/README.md Outdated
@enejb
enejb force-pushed the add/webpack-filesystem-cache branch from b7d2da6 to f0cc884 Compare June 3, 2026 21:25
@anomiex

anomiex commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@enejb
enejb force-pushed the add/webpack-filesystem-cache branch from 974e96e to bcc085c Compare June 8, 2026 22:34

@anomiex anomiex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One suggestion inline. Other than that, looks reasonable, let's try it!

Comment thread projects/js-packages/webpack-config/changelog/add-filesystem-cache-export Outdated
enejb and others added 6 commits June 15, 2026 15:58
The jetpack plugin changelogger only allows major/enhancement/compat/bugfix/other; "changed" is not valid. Use "other" for this non-user-facing build change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fig, skip in CI; drop user-facing changelog noise

(cherry picked from commit 927946047b67476d6dc65a354d3c348968096171)
Co-authored-by: Brad Jorsch <anomiex@users.noreply.github.com>
(cherry picked from commit b7d2da683c4b024aa4d2b5011f2c3e2a90fca6de)
…ache-export

Co-authored-by: Brad Jorsch <anomiex@users.noreply.github.com>
@enejb
enejb force-pushed the add/webpack-filesystem-cache branch from cd57ff4 to df9101e Compare June 15, 2026 23:16
@enejb
enejb merged commit 0bd9426 into trunk Jun 15, 2026
96 checks passed
@enejb
enejb deleted the add/webpack-filesystem-cache branch June 15, 2026 23:49
@github-actions github-actions Bot removed the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jun 15, 2026
@github-actions github-actions Bot added this to the jetpack/16.0 milestone Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants