Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .agents/rules/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ directly from a widget: that inlines the entire charting stack (charts, visx, re
into the widget's render bundle. If a chart component isn't exposed yet, re-export it from
the toolkit's "Charts passthrough" section.

Design-system components (`@wordpress/ui`, `@wordpress/dataviews`, `@automattic/ui`) come
from `@jetpack-premium-analytics/externals`, the passthrough script module, for the same
reason. ESLint enforces both rules; see `packages/externals/README.md`.

The render component is bound by `WidgetRenderProps<Item>` from
`@wordpress/widget-primitives`: it receives only `{ attributes, setAttributes }`.
`attributes` may arrive empty — default it (`= {}`).
Expand Down
10 changes: 6 additions & 4 deletions projects/packages/premium-analytics/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,12 @@ See Automattic/jetpack#50266 for the PR that established this contract.
- Don't edit dashboard React in Calypso — it lives here now.
- Internal package names use `@jetpack-premium-analytics/*` aliases throughout the package —
never `@automattic/jetpack-premium-analytics-*`.
- Never import `@automattic/charts`, `@wordpress/ui`, or `@wordpress/dataviews` directly from a
package that compiles to a script module — go through `@jetpack-premium-analytics/externals`.
A direct import compiles the whole library into that module again; see
`packages/externals/README.md`.
- Never import `@automattic/ui`, `@wordpress/ui`, or `@wordpress/dataviews` directly from
anything under `packages/`, `widgets/`, or `routes/` — go through
`@jetpack-premium-analytics/externals`. A direct import compiles the whole library into that
bundle again; ESLint enforces this. `@automattic/charts` follows the same rule under
`packages/`, but under `widgets/` and `routes/` it must come from
`@jetpack-premium-analytics/widgets-toolkit` instead. See `packages/externals/README.md`.
- All source code comments must be in English.

## Widgets
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Route the ui, fields, and icons packages through the externals script module so shared third-party libraries are no longer duplicated across their bundles.
61 changes: 43 additions & 18 deletions projects/packages/premium-analytics/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,24 @@ export default defineConfig(
},
},
{
// The toolkit is a shared script module, so a direct import of one of
// these libraries compiles it *into* the toolkit instead of resolving
// to the externals module — which is what made an unrelated one-line
// change rewrite megabytes of build output (WOOA7S-1836).
// Each package under `packages/` that declares `wpScriptModuleExports`
// is a shared script module, so a direct import of one of these
// libraries compiles it *into* that module instead of resolving to the
// externals module — which is what made an unrelated one-line change
// rewrite megabytes of build output (WOOA7S-1836).
//
// Scoped to the toolkit because that is the only module migrated so
// far; `packages/ui`, `widgets/`, and `routes/` still import these
// directly and are the follow-ups tracked in the PR description.
// `packages/externals` is deliberately not covered: it *is* the
// Covers all of `packages/**`, including the packages that are bundled
// from source rather than compiled to their own module: they end up
// inside whichever module imports them, so a direct import there costs
// the same. `widgets/` and `routes/` are covered by the block below,
// which adds the charts-via-toolkit rule on top.
//
// `packages/externals` is deliberately excluded: it *is* the
// passthrough, so it has to import them directly.
files: [
'packages/widgets-toolkit/**',
'projects/packages/premium-analytics/packages/widgets-toolkit/**',
files: [ 'packages/**', 'projects/packages/premium-analytics/packages/**' ],
ignores: [
'packages/externals/**',
'projects/packages/premium-analytics/packages/externals/**',
],
rules: {
'no-restricted-imports': [
Expand All @@ -149,7 +154,8 @@ export default defineConfig(
// stylesheet side-effect imports: those are plain
// CSS, not the library, so they carry none of the
// bundling cost.
regex: '^(@automattic/charts|@wordpress/ui|@wordpress/dataviews)(/(?!.*\\.css$).*)?$',
regex:
'^(@automattic/charts|@automattic/ui|@wordpress/ui|@wordpress/dataviews)(/(?!.*\\.css$).*)?$',
message:
'Import these from @jetpack-premium-analytics/externals instead: it compiles them once into a shared script module rather than into every module that imports them. Missing an export? Add it to packages/externals/src/index.ts.',
},
Expand All @@ -159,21 +165,40 @@ export default defineConfig(
},
},
{
// Widgets must reach chart components through the widgets-toolkit
// shared script module: a direct `@automattic/charts` import gets
// silently inlined into that widget's render bundle, dragging the
// whole charting stack (charts, visx, react-spring) along with it.
files: [ 'widgets/**', 'projects/packages/premium-analytics/widgets/**' ],
// Widget render bundles and route bundles are lazy-loaded on their own,
// so a direct import of one of these libraries is inlined into that
// bundle rather than resolved from a shared script module.
//
// Charts are called out separately: they must come through
// widgets-toolkit, which themes them and takes charts from externals
// itself, so a toolkit passthrough costs nothing. Everything else goes
// straight to externals.
//
// Both rules live in one block on purpose — a second `files` entry
// naming `no-restricted-imports` again would replace this rule for
// these paths rather than add to it.
files: [
'widgets/**',
'routes/**',
'projects/packages/premium-analytics/widgets/**',
'projects/packages/premium-analytics/routes/**',
],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [ '@automattic/charts', '@automattic/charts/*' ],
// Same `.css` exemption as the packages rule above.
regex: '^@automattic/charts(/(?!.*\\.css$).*)?$',
message:
'Import chart components from @jetpack-premium-analytics/widgets-toolkit instead: it is a shared script module, so charts is bundled once for the whole dashboard. Missing a component? Re-export it from the toolkit "Charts passthrough" section.',
},
{
regex: '^(@automattic/ui|@wordpress/ui|@wordpress/dataviews)(/(?!.*\\.css$).*)?$',
message:
'Import these from @jetpack-premium-analytics/externals instead: it compiles them once into a shared script module rather than into every bundle that imports them. Missing an export? Add it to packages/externals/src/index.ts.',
},
],
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `@jetpack-premium-analytics/externals`

A passthrough script module for the heavy third-party libraries the dashboard shares:
`@automattic/charts`, `@wordpress/ui`, and `@wordpress/dataviews`.
`@automattic/charts`, `@automattic/ui`, `@wordpress/ui`, and `@wordpress/dataviews`.

## Why this exists

Expand All @@ -26,13 +26,62 @@ change when the libraries themselves are upgraded. Feature work no longer rewrit
invalidates the artifact for everyone, which is exactly what this package exists to avoid.
- **Import from here, never from the library directly.** `import { LineChart } from
'@automattic/charts'` inside another module bundles charts into that module again and silently
undoes the split. ESLint enforces this for `packages/widgets-toolkit` (`no-restricted-imports`
in `eslint.config.mjs`); stylesheet imports are exempt, since plain CSS carries none of the
bundling cost. The rule is scoped to the toolkit because that is the only module migrated so
far — widen it as `packages/ui`, `widgets/`, and `routes/` follow.
undoes the split. ESLint enforces this across `packages/**`, `widgets/**` and `routes/**`
(`no-restricted-imports` in `eslint.config.mjs`), with `packages/externals` itself excluded — it
*is* the passthrough, so it has to import the libraries directly. Stylesheet imports are exempt
everywhere, since plain CSS carries none of the bundling cost.

Under `widgets/` and `routes/` the rule splits in two: `@automattic/charts` must come from
`@jetpack-premium-analytics/widgets-toolkit`, which themes the chart components and takes charts
from here itself, so a toolkit passthrough costs nothing; everything else comes straight from
here. 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.
- **Adding an export is cheap; adding a library is not.** A new library only belongs here if more
than one module needs it, or if it is large enough that re-emitting it per module hurts.
`@automattic/ui` qualifies on the second count alone: `DateRangeCalendar` is its only consumer
in the package, but it reaches `react-day-picker` behind it, so leaving it in `packages/ui`
re-emitted ~55 KB of vendor code on every edit to that module.

`date-fns` deliberately stays out. It is imported directly by ~30 files across `data`,
`datetime`, `routing`, `widgets/`, and `routes/`, and it is tree-shaken per function — routing it
through here would mean a barrel that grows every time any consumer needs one more function,
which is exactly the churn this module exists to avoid.

## What wp-build externalises on its own

wp-build externalises a `@wordpress/*` import only when that library's own `package.json` declares
one of two fields (see `isScriptModuleImport` in `@wordpress/build/lib/wordpress-externals-plugin.mjs`):

- `wpScript` — externalised as a classic `wp-<name>` script handle, listed under `dependencies` in
the generated `*.asset.php`.
- `wpScriptModuleExports` — externalised as a script module, listed under `module_dependencies`.

`@wordpress/components`, `@wordpress/data`, `@wordpress/element`, `@wordpress/i18n`,
`@wordpress/theme` and `@wordpress/compose` all declare `wpScript`, so importing them costs
nothing and they must **not** be routed through here. The same goes for `react` and `react-dom`.

`@wordpress/ui` and `@wordpress/dataviews` declare **neither**, so esbuild compiles them into every
bundle that imports them. That is the only reason they are in this module: a direct import of
`@wordpress/ui` is not the free externalised reference that a direct import of
`@wordpress/components` is.

### Revisit this when upstream changes

**If `@wordpress/ui` or `@wordpress/dataviews` ever ship `wpScript` or `wpScriptModuleExports`,
drop them from here and import them directly again.** At that point wp-build externalises them
natively, this passthrough stops earning its keep for those two, and the extra hop is pure
indirection. Check with:

```sh
# Run from projects/packages/premium-analytics.
for p in @wordpress/ui @wordpress/dataviews; do
node -p "const j=require('$p/package.json');
'$p: ' + ((j.wpScript || j.wpScriptModuleExports) ? 'externalised by wp-build — remove from externals' : 'still compiled in — keep here')"
done
```

Today both report *still compiled in* (`@wordpress/ui` declares `wpScript: false`,
`@wordpress/dataviews` declares nothing).

Libraries WordPress already registers as script modules or globals (`@wordpress/components`,
`@wordpress/data`, `@wordpress/i18n`, `react`, …) are externalised by wp-build on their own and
must **not** be routed through here.
`@automattic/charts` and `@automattic/ui` are third-party to WordPress and will never gain those
fields, so they stay here regardless.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
],
"dependencies": {
"@automattic/charts": "workspace:*",
"@automattic/ui": "1.0.2",
"@wordpress/dataviews": "17.1.0",
"@wordpress/ui": "0.17.0",
"react": "18.3.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* External dependencies
*/
import '@automattic/charts/style.css';
import '@automattic/ui/style.css';

/**
* Charts
Expand Down Expand Up @@ -43,13 +44,31 @@ export {

export { LineShape, RectShape } from '@automattic/charts/visx/legend';

/**
* Calendar
*
* `DateRangeCalendar` is the package's only `@automattic/ui` consumer, but it
* reaches `react-day-picker` and `date-fns` behind it — ~55 KB of minified
* vendor code that would otherwise be re-emitted on every edit to the module
* that imports it.
*/
export { DateRangeCalendar } from '@automattic/ui';

/**
* WordPress design system
*
* `Field` is exported as `FormField`: `@wordpress/ui`'s form-field namespace and
* `@wordpress/dataviews`' `Field` type collide under one barrel, and DataViews'
* `Field` is the name consumers already import from here. The alias is still a
* plain re-export — it renames, it does not wrap.
*/
export {
Button,
EmptyState,
Field as FormField,
Fieldset,
Icon,
Input,
Link,
SelectControl,
Stack,
Expand All @@ -66,7 +85,10 @@ export {
filterSortAndPaginate,
type Action,
type DataFormControlProps,
type DataViewRenderFieldProps,
type Field,
type Option,
type SupportedLayouts,
type View,
type ViewBaseProps,
} from '@wordpress/dataviews';
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
"dependencies": {
"@jetpack-premium-analytics/data": "workspace:*",
"@jetpack-premium-analytics/datetime": "workspace:*",
"@jetpack-premium-analytics/externals": "workspace:*",
"@jetpack-premium-analytics/routing": "workspace:*",
"@jetpack-premium-analytics/ui": "workspace:*",
"@wordpress/components": "37.0.0",
"@wordpress/dataviews": "17.1.0",
"@wordpress/element": "8.3.0",
"@wordpress/icons": "^15.0.0",
"@wordpress/private-apis": "1.51.0",
"@wordpress/ui": "0.17.0",
"date-fns": "4.1.0",
"react": "18.3.1"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/**
* WordPress dependencies
*/
import { Button, Fieldset, Icon, Stack } from '@jetpack-premium-analytics/externals';
import { CheckboxControl, privateApis, Spinner } from '@wordpress/components';
import { useCallback } from '@wordpress/element';
import { chevronDown } from '@wordpress/icons';
import { Button, Fieldset, Icon, Stack } from '@wordpress/ui';
/**
* Internal dependencies
*/
import useElements from '../helpers/use-elements';
import { unlock } from '../lock/unlock';
import styles from './array-checkbox-field.module.css';
import type { DataFormControlProps } from '@wordpress/dataviews';
import type { DataFormControlProps } from '@jetpack-premium-analytics/externals';

const { Menu } = unlock( privateApis );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Internal dependencies
*/
import { fromSelectValue, toSelectItems } from '../select-field';
import type { Option } from '@wordpress/dataviews';
import type { Option } from '@jetpack-premium-analytics/externals';

const NUMERIC_ELEMENTS: Option[] = [
{ value: 10, label: 'Ten' },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* WordPress dependencies
*/
import { SelectControl } from '@jetpack-premium-analytics/externals';
import { Spinner } from '@wordpress/components';
import { useCallback, useMemo } from '@wordpress/element';
import { SelectControl } from '@wordpress/ui';
/**
* Internal dependencies
*/
import useElements from '../helpers/use-elements';
import type { DataFormControlProps, Option } from '@wordpress/dataviews';
import type { DataFormControlProps, Option } from '@jetpack-premium-analytics/externals';

export function toSelectItems( elements: Option[] ) {
return elements.map( element => ( {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { useEffect, useMemo, useState } from '@wordpress/element';
import type { Option } from '@wordpress/dataviews';
import type { Option } from '@jetpack-premium-analytics/externals';

type UseElementsParams = {
elements?: Option[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {
isPrimaryPreset,
type DateRange,
} from '@jetpack-premium-analytics/datetime';
import { Stack } from '@jetpack-premium-analytics/externals';
import { deriveComparisonRange, encodeDateToSearchParam } from '@jetpack-premium-analytics/routing';
import { DateFiltersPanel } from '@jetpack-premium-analytics/ui';
import { Stack } from '@wordpress/ui';
import { endOfDay } from 'date-fns';
import { useCallback, useMemo, useState } from 'react';
import { getStoreInfo } from '../helpers/store-info';
import type { DataFormControlProps } from '@wordpress/dataviews';
import type { DataFormControlProps } from '@jetpack-premium-analytics/externals';

/*
* Copied from widgets-toolkit `fields/date-report-params-field` so widget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@wordpress/primitives": "4.51.0"
},
"devDependencies": {
"@storybook/react": "10.4.6",
"@wordpress/ui": "0.17.0"
"@jetpack-premium-analytics/externals": "workspace:*",
"@storybook/react": "10.4.6"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon, Stack } from '@wordpress/ui';
import { Icon, Stack } from '@jetpack-premium-analytics/externals';
import * as icons from '../index';
import type { Meta, StoryObj } from '@storybook/react';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
"*.scss"
],
"dependencies": {
"@automattic/ui": "1.0.2",
"@jetpack-premium-analytics/datetime": "workspace:*",
"@jetpack-premium-analytics/externals": "workspace:*",
"@jetpack-premium-analytics/formatters": "workspace:*",
"@wordpress/components": "37.0.0",
"@wordpress/compose": "8.4.0",
"@wordpress/dataviews": "17.1.0",
"@wordpress/i18n": "^6.9.0",
"@wordpress/icons": "^15.0.0",
"@wordpress/ui": "0.17.0",
"clsx": "2.1.1",
"react": "18.3.1"
},
Expand Down
Loading
Loading