Skip to content

Premium Analytics: route packages, widgets and routes through the externals module - #50964

Merged
louwie17 merged 4 commits into
trunkfrom
fix/premium-analytics-ui-externals
Aug 4, 2026
Merged

Premium Analytics: route packages, widgets and routes through the externals module#50964
louwie17 merged 4 commits into
trunkfrom
fix/premium-analytics-ui-externals

Conversation

@louwie17

@louwie17 louwie17 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #

Proposed changes

Follow-up to #50932 (WOOA7S-1836), which added @jetpack-premium-analytics/externals and migrated packages/widgets-toolkit. Everything else still imported those libraries directly, so they were still compiled in more than once. This finishes the migration and locks it in with lint.

  • Route packages/ui, packages/fields, packages/icons, and all of widgets/ and routes/ through the passthrough module — 69 files in total.
  • Add the missing re-exports: Fieldset, Input, the Field namespace, and the Option / ViewBaseProps / DataViewRenderFieldProps DataViews types.
  • @wordpress/ui's Field namespace collides with DataViews' Field type under one barrel, so it is re-exported as FormField. DataViews' Field keeps its name — it is what consumers already import from here.
  • Move @automattic/ui into externals too. DateRangeCalendar is its only consumer in the package, but it reaches react-day-picker behind it — ~55 KB of vendor code that was re-emitted on every edit to packages/ui.
  • date-fns deliberately stays out. ~30 files import it directly across data, datetime, routing, widgets/ and routes/, and it is tree-shaken per function, so routing it through here would mean a barrel that grows every time any consumer needs one more function — exactly the churn this module exists to avoid.

The ESLint guard

Now covers packages/**, widgets/** and routes/**, with packages/externals excluded (it is the passthrough). Stylesheet imports stay allowed everywhere — plain CSS carries none of the bundling cost.

Under widgets/ and routes/ the rule splits in two: @automattic/charts must still come from @jetpack-premium-analytics/widgets-toolkit, which themes the chart components and takes charts from externals itself; the design-system libraries come straight from externals. Both patterns live in one ESLint block on purpose — a second files entry naming no-restricted-imports again would replace the rule for those paths rather than add to it, silently dropping the charts guard. The charts pattern also moves from group to regex so it picks up the same .css exemption as the rest.

Build output

artifact trunk this PR delta
modules/ui/index.min.js 766,662 173,201 −77.4%
modules/ui/index.min.js.map 3,749,692 561,370 −85.0%
modules/fields/index.min.js 243,511 11,450 −95.3%
modules/fields/index.min.js.map 1,202,474 96,740 −92.0%
widget render bundles (150 files) 958,410 604,295 −36.9%
route bundles (12 files) 3,867,104 3,736,108 −3.4%
modules/externals/index.min.js 1,074,665 1,159,187 +7.9%

Whole build: 22,044 KB → 17,060 KB.

Attributing minified bytes through the source map, ui goes from 18% to 82% first-party. fields was only 3% first-party — almost the entire module was @wordpress/ui and its transitive deps.

Routes move least because webpack was already tree-shaking them down to the few components each one used — routes/dashboard only pulled in Stack, so it is flat. The small route bundles still drop sharply (syncing −82.8%, connect −49.9%), and every route and widget asset now lists @jetpack-premium-analytics/externals under module_dependencies, so none of them can re-inline the libraries going forward. That structural guarantee is the point more than the byte count.

One expected side effect: fields drops out of i18n-manifest.json. It has no translatable strings of its own, so every string it carried came from the inlined @wordpress/ui copy, which now lives once in externals.

Rebased on trunk b2d013e031; the table above is measured against it. Trunk work that landed while this was open needed the same migration to satisfy the widened rule: the Comments-widget split (#50929), the new /reports/locations route, and the preset-measuring probe added by #50906.

Test mocks

Three suites needed updating — both cases are the barrel deepening the import graph:

  • routes/reports/{tags,comments}/config/fields.test.tsx and report-csv-exports.test.tsx mocked @wordpress/ui, which the components no longer import, leaving the mocks inert. Retargeted to the externals barrel behind a lazy Proxy so the rest of it still resolves.
  • routes/{video,post}-detail/stage.test.tsx mocked @wordpress/data with only useSelect. Reaching externals pulls @wordpress/components@wordpress/rich-text into the graph, whose store calls combineReducers at import time, so those suites failed to load. Same lazy Proxy fallthrough.

Related product discussion/links

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

No.

Testing instructions

This is a build-output change with no intended user-visible effect, so the goal is to confirm nothing regressed.

Automated

  • cd projects/packages/premium-analytics
  • pnpm run typecheck, pnpm exec eslint . --ext .ts,.tsx,.mjs, pnpm run test — all clean (212 suites / 1495 tests).
  • pnpm run build-production, then check that build/modules/{ui,fields}/index.min.asset.php and every build/{widgets,routes}/**/*.min.asset.php list @jetpack-premium-analytics/externals under module_dependencies — that is the proof the libraries are externalised rather than inlined.

Guard

Drop a throwaway file into packages/ui/src/, widgets/<any>/ and routes/<any>/ importing @wordpress/ui, @automattic/ui, @automattic/charts, a charts subpath, an import type from @wordpress/dataviews, and a .css import. Everything but the .css import must error; under widgets//routes/ the charts ones must point at widgets-toolkit while the rest point at externals. The same file under packages/externals/src/ must produce no errors.

Dashboard

Load the dashboard (?page=jetpack-premium-analytics) and confirm it renders as before: tabs, the date preset row, the "Compare to" dropdown, charts, widgets, and the report pages under /reports/*. Open the Custom date range and confirm the calendar still renders — DateRangeCalendar now crosses a module boundary, so it is the highest-risk item here.

Verified locally: all six script modules load, zero console/page errors, and every symbol crossing the new module boundary resolves from the built externals module (including DateRangeCalendar as a real function, plus FormField.Root, Tabs.Panel and Fieldset.Legend).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TsoMLFnEC9XsUJxNiP3VZ5

@louwie17 louwie17 self-assigned this Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 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 fix/premium-analytics-ui-externals branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/premium-analytics-ui-externals
bin/jetpack-downloader test jetpack-mu-wpcom-plugin fix/premium-analytics-ui-externals

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

@github-actions

github-actions Bot commented Jul 31, 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!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jul 31, 2026
@jp-launch-control

jp-launch-control Bot commented Jul 31, 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

@louwie17
louwie17 marked this pull request as ready for review July 31, 2026 15:02
@louwie17
louwie17 requested review from a team as code owners July 31, 2026 15:02
@louwie17 louwie17 added [Status] Needs Review This PR is ready for review. and removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Status] In Progress labels Jul 31, 2026
@louwie17 louwie17 changed the title Premium Analytics: route ui, fields and icons through the externals module Premium Analytics: route packages, widgets and routes through the externals module Jul 31, 2026
chihsuan
chihsuan previously approved these changes Aug 3, 2026

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this! @louwie17

Left a minor comment for SectionTabPanel docblock but tested well locally and looks good.

* and shared packages can each bundle their own copy of `@wordpress/ui`, and
* Base UI's tabs context does not cross bundle copies. Importing the panel and
* root through the same package specifier guarantees both share one instance.
* and widgets still import `@wordpress/ui` directly, so they can each bundle

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we flip this one around?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, thanks good catch!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — that wording described a state partway through the branch. It was accurate when packages/ui was migrated, but the follow-up commit moved routes/ and widgets/ onto the passthrough too, which made "still import @wordpress/ui directly" false.

Flipped it to state the invariant instead: panel and root have to come from the same Tabs instance, and importing from externals is what makes that instance shared.

Also rebased on trunk — #50929 split the Comments widget while this was open, so most-commented-posts/most-commented-authors and the new /reports/locations route needed the same migration to satisfy the widened lint rule.

@louwie17
louwie17 force-pushed the fix/premium-analytics-ui-externals branch from 3cdfe8d to fcc9600 Compare August 3, 2026 08:16
louwie17 and others added 4 commits August 4, 2026 12:39
…odule

Follow-up to #50932, which added `@jetpack-premium-analytics/externals` and
migrated `widgets-toolkit`. Everything else still imported the same libraries
directly, so they were compiled in more than once.

Route `packages/ui`, `packages/fields` and `packages/icons` through the
passthrough, adding the missing re-exports (`Fieldset`, `Input`, the `Field`
namespace as `FormField`, and the `Option` / `ViewBaseProps` /
`DataViewRenderFieldProps` DataViews types).

`@wordpress/ui`'s `Field` namespace collides with DataViews' `Field` type under
one barrel, so it is re-exported as `FormField`; DataViews' `Field` is the name
consumers already import from here.

Also move `@automattic/ui` into externals. `DateRangeCalendar` is its only
consumer in the package, but it reaches `react-day-picker` behind it — ~55 KB of
vendor code re-emitted on every edit to `packages/ui`. `date-fns` deliberately
stays out: ~30 files import it directly across data, datetime, routing, widgets
and routes, and it is tree-shaken per function, so a barrel would grow whenever
any consumer needed one more function.

Production output:

  modules/ui/index.min.js          759,657 B ->   165,951 B   (-78.2%)
  modules/ui/index.min.js.map    3,703,019 B ->   511,315 B   (-86.2%)
  modules/fields/index.min.js      243,651 B ->    11,588 B   (-95.2%)
  modules/fields/index.min.js.map 1,203,243 B ->    97,539 B  (-91.9%)
  modules/externals/index.min.js 1,073,311 B -> 1,157,834 B   (+7.9%)

Attributing minified bytes through the source map, `ui` goes from 18% to 82%
first-party; `fields` was only 3% first-party and is now almost entirely its own
code. Widget, route and other module bundles are byte-identical. The whole build
drops 21,932 KB -> 17,400 KB, because `ui` and `fields` were duplicating vendor
code that externals already carried.

Widen the ESLint guard from `packages/widgets-toolkit/**` to all of
`packages/**`, excluding `packages/externals` (the passthrough itself), and add
`@automattic/ui` to the restricted pattern. Verified the rule errors on value,
subpath and `import type` forms while leaving `.css` imports alone.

`fields` drops out of the i18n manifest: it has no translatable strings of its
own, so every string it carried came from the inlined `@wordpress/ui` copy,
which now lives once in externals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsoMLFnEC9XsUJxNiP3VZ5
Completes the migration: the 47 remaining files under `widgets/` and `routes/`
that imported `@wordpress/ui` or `@wordpress/dataviews` directly now go through
`@jetpack-premium-analytics/externals`, and the ESLint guard covers those trees.

Under `widgets/` and `routes/` the rule splits in two: `@automattic/charts` must
still come from `@jetpack-premium-analytics/widgets-toolkit` (which themes the
chart components and takes charts from externals itself), while the design-system
libraries come straight from externals. Both patterns live in one ESLint block —
a second `files` entry naming `no-restricted-imports` again would replace the
rule for those paths rather than add to it, silently dropping the charts guard.

The charts pattern also moves from `group` to `regex` so it picks up the same
`.css` exemption as the rest.

Build output:

  widget render bundles  957,006 B -> 598,691 B  (-37.4%, 148 files)
  route bundles        3,852,094 B -> 3,721,496 B  (-3.4%, 12 files)
  whole build             21,932 KB -> 16,928 KB

Routes move less because webpack was already tree-shaking them down to the few
components each one used — `routes/dashboard` only pulled in `Stack`. The small
bundles still drop sharply (`syncing` -82.8%, `connect` -49.9%) and every route
and widget asset now lists `@jetpack-premium-analytics/externals` under
`module_dependencies`, so none of them can re-inline the libraries.

Three test mocks needed updating, both cases the barrel deepening the import
graph:

- `routes/reports/{tags,comments}/config/fields.test.tsx` and
  `report-csv-exports.test.tsx` mocked `@wordpress/ui`, which the components no
  longer import. Retargeted to the externals barrel behind a lazy `Proxy` so the
  rest of it still resolves.
- `routes/{video,post}-detail/stage.test.tsx` mocked `@wordpress/data` with only
  `useSelect`. Reaching externals pulls `@wordpress/components` ->
  `@wordpress/rich-text` into the graph, whose store calls `combineReducers` at
  import time, so the suites failed to load. Same lazy `Proxy` fallthrough.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsoMLFnEC9XsUJxNiP3VZ5
…dule

wp-build externalises a `@wordpress/*` import only when the library's own
package.json declares `wpScript` or `wpScriptModuleExports`. `@wordpress/ui` and
`@wordpress/dataviews` declare neither, which is the only reason they are routed
through this module — unlike `@wordpress/components` and friends, a direct import
of them is not a free externalised reference.

Record that, plus the check to run: if either ever ships one of those fields,
they should be dropped from here and imported directly again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsoMLFnEC9XsUJxNiP3VZ5
The wording described the state partway through this branch: it said routes and
widgets still import `@wordpress/ui` directly, which stopped being true once
they were migrated to the externals passthrough. State the invariant instead —
panel and root must come from the same `Tabs` instance, and importing from
externals is what makes that instance shared.

Spotted in review by @chihsuan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsoMLFnEC9XsUJxNiP3VZ5
@louwie17
louwie17 force-pushed the fix/premium-analytics-ui-externals branch from fcc9600 to 650ffc3 Compare August 4, 2026 10:46
@louwie17
louwie17 merged commit 798cca8 into trunk Aug 4, 2026
82 checks passed
@louwie17
louwie17 deleted the fix/premium-analytics-ui-externals branch August 4, 2026 14:46
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label Aug 4, 2026
dognose24 added a commit that referenced this pull request Aug 4, 2026
…ment panel

Conflicts were the #50964 externals refactor against the video-detail
stage/summary-card imports (resolved to the externals convention) and the
date-filters panel, taken wholly from trunk — this branch's net change to
packages/ui is zero since 'Drop the header date-filter half', and trunk's
WOOA7S-1817 content-measurement rework is what that deferral was waiting for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chihsuan added a commit that referenced this pull request Aug 5, 2026
A direct `@wordpress/ui` import re-bundles the library into this module;
the no-restricted-imports rule added in #50964 rejects it.
chihsuan added a commit that referenced this pull request Aug 5, 2026
* feat: mark the Stats header with the Jetpack logo and unify breadcrumbs

The dashboard's root breadcrumb said "Analytics" while every report page's
said "Stats", so the product read as two names depending on the page. Both
now come from a shared StatsBreadcrumbs, which supplies the root crumb and
links it back to the dashboard, and a StatsPageIcon fills the page header's
visual slot with the Jetpack mark.

Refs WOOA7S-1784

* fix: keep the Stats title aligned with the header's Jetpack mark

wp-admin's `dd, li { margin-bottom: 6px }` reaches the breadcrumb list items,
so the trail measures 6px taller than its text. The header row centres that
taller box, leaving the title 3px above the mark beside it. Reset the item
margin from the trail's own wrapper.

Refs WOOA7S-1784

* docs: simplify Premium Analytics code comments

* fix: name the Premium Analytics menu Stats

* docs: update report page header example

* fix: keep Stats breadcrumbs linked until titles resolve

* fix: keep the sidebar menu label as Stats v2

The beta runs both dashboards side by side, so the sidebar keeps the
v2 label to distinguish it from Jetpack Stats. Only the page header and
breadcrumbs read "Stats".

* fix: import Icon from the externals module

A direct `@wordpress/ui` import re-bundles the library into this module;
the no-restricted-imports rule added in #50964 rejects it.
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