Premium Analytics: route packages, widgets and routes through the externals module - #50964
Conversation
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
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:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
Code Coverage SummaryThis 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. 🤷 |
| * 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 |
There was a problem hiding this comment.
Should we flip this one around?
There was a problem hiding this comment.
Yeah, thanks good catch!
There was a problem hiding this comment.
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.
3cdfe8d to
fcc9600
Compare
…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
fcc9600 to
650ffc3
Compare
…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>
A direct `@wordpress/ui` import re-bundles the library into this module; the no-restricted-imports rule added in #50964 rejects it.
* 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.
Fixes #
Proposed changes
Follow-up to #50932 (WOOA7S-1836), which added
@jetpack-premium-analytics/externalsand migratedpackages/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.packages/ui,packages/fields,packages/icons, and all ofwidgets/androutes/through the passthrough module — 69 files in total.Fieldset,Input, theFieldnamespace, and theOption/ViewBaseProps/DataViewRenderFieldPropsDataViews types.@wordpress/ui'sFieldnamespace collides with DataViews'Fieldtype under one barrel, so it is re-exported asFormField. DataViews'Fieldkeeps its name — it is what consumers already import from here.@automattic/uiinto externals too.DateRangeCalendaris its only consumer in the package, but it reachesreact-day-pickerbehind it — ~55 KB of vendor code that was re-emitted on every edit topackages/ui.date-fnsdeliberately stays out. ~30 files import it directly acrossdata,datetime,routing,widgets/androutes/, 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/**androutes/**, withpackages/externalsexcluded (it is the passthrough). Stylesheet imports stay allowed everywhere — plain CSS carries none of the bundling cost.Under
widgets/androutes/the rule splits in two:@automattic/chartsmust 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 secondfilesentry namingno-restricted-importsagain would replace the rule for those paths rather than add to it, silently dropping the charts guard. The charts pattern also moves fromgrouptoregexso it picks up the same.cssexemption as the rest.Build output
modules/ui/index.min.jsmodules/ui/index.min.js.mapmodules/fields/index.min.jsmodules/fields/index.min.js.mapmodules/externals/index.min.jsWhole build: 22,044 KB → 17,060 KB.
Attributing minified bytes through the source map,
uigoes from 18% to 82% first-party.fieldswas only 3% first-party — almost the entire module was@wordpress/uiand its transitive deps.Routes move least because webpack was already tree-shaking them down to the few components each one used —
routes/dashboardonly pulled inStack, 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/externalsundermodule_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:
fieldsdrops out ofi18n-manifest.json. It has no translatable strings of its own, so every string it carried came from the inlined@wordpress/uicopy, which now lives once inexternals.Test mocks
Three suites needed updating — both cases are the barrel deepening the import graph:
routes/reports/{tags,comments}/config/fields.test.tsxandreport-csv-exports.test.tsxmocked@wordpress/ui, which the components no longer import, leaving the mocks inert. Retargeted to the externals barrel behind a lazyProxyso the rest of it still resolves.routes/{video,post}-detail/stage.test.tsxmocked@wordpress/datawith onlyuseSelect. Reaching externals pulls@wordpress/components→@wordpress/rich-textinto the graph, whose store callscombineReducersat import time, so those suites failed to load. Same lazyProxyfallthrough.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-analyticspnpm run typecheck,pnpm exec eslint . --ext .ts,.tsx,.mjs,pnpm run test— all clean (212 suites / 1495 tests).pnpm run build-production, then check thatbuild/modules/{ui,fields}/index.min.asset.phpand everybuild/{widgets,routes}/**/*.min.asset.phplist@jetpack-premium-analytics/externalsundermodule_dependencies— that is the proof the libraries are externalised rather than inlined.Guard
Drop a throwaway file into
packages/ui/src/,widgets/<any>/androutes/<any>/importing@wordpress/ui,@automattic/ui,@automattic/charts, a charts subpath, animport typefrom@wordpress/dataviews, and a.cssimport. Everything but the.cssimport must error; underwidgets//routes/the charts ones must point at widgets-toolkit while the rest point at externals. The same file underpackages/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 —DateRangeCalendarnow 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
externalsmodule (includingDateRangeCalendaras a real function, plusFormField.Root,Tabs.PanelandFieldset.Legend).🤖 Generated with Claude Code
https://claude.ai/code/session_01TsoMLFnEC9XsUJxNiP3VZ5