From 00fed2dbff5130fba9736d0ce073c40eb57b0e0d Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 27 May 2026 15:23:32 +0800 Subject: [PATCH 01/10] feat(premium-analytics): add tsconfig paths and typecheck for internal packages --- pnpm-lock.yaml | 3 ++ projects/packages/premium-analytics/README.md | 29 +++++++++++++++---- .../changelog/add-internal-package-resolution | 4 +++ .../packages/premium-analytics/package.json | 2 ++ .../packages/premium-analytics/tsconfig.json | 9 ++++++ 5 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 projects/packages/premium-analytics/changelog/add-internal-package-resolution diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 526dabd96ca4..85e3d1751a8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3849,6 +3849,9 @@ importers: '@babel/core': specifier: 7.29.0 version: 7.29.0 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260225.1 + version: 7.0.0-dev.20260225.1 '@wordpress/build': specifier: 0.13.0 version: 0.13.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) diff --git a/projects/packages/premium-analytics/README.md b/projects/packages/premium-analytics/README.md index e3f361a44074..5b26b1fbcaa5 100644 --- a/projects/packages/premium-analytics/README.md +++ b/projects/packages/premium-analytics/README.md @@ -42,17 +42,19 @@ jetpack build packages/premium-analytics # via Jetpack CLI ### Adding a route 1. Create `routes//package.json`: + ```json { - "name": "-route", - "route": { - "path": "/", - "page": "jetpack-premium-analytics" - } + "name": "-route", + "route": { + "path": "/", + "page": "jetpack-premium-analytics" + } } ``` 2. Create `routes//stage.tsx` exporting `stage()`: + ```tsx export const stage = () =>
My new page
; ``` @@ -74,11 +76,28 @@ fixes the template or the minimum WordPress version is 7.0+. ### Init module (`packages/init/`) Serves two purposes: + 1. Sets the dashboard menu icon via `@wordpress/boot` store 2. Forces `@wordpress/build` to track `@wordpress/boot` as a module dependency — without an init module that imports boot, the build skips it +## Internal packages (`packages/*`) + +App-internal modules discovered by `@wordpress/build`. Types/IDE resolve +`@jetpack-premium-analytics/` imports via the `tsconfig.json` `paths` alias +(`pnpm typecheck`). + +To import one from a route/another package, the build also needs it symlinked in +`node_modules` under that specifier. Name the package +`@automattic/jetpack-premium-analytics-` (a bare `@jetpack-premium-analytics/*` +name fails the repo name lint and `_@…` is invalid to pnpm), then add a `link:` +dep on the top-level `package.json` (routes aren't workspace members): + +```jsonc +"dependencies": { "@jetpack-premium-analytics/": "link:packages/" } +``` + ## File structure ``` diff --git a/projects/packages/premium-analytics/changelog/add-internal-package-resolution b/projects/packages/premium-analytics/changelog/add-internal-package-resolution new file mode 100644 index 000000000000..d35865145ec1 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/add-internal-package-resolution @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add a tsconfig paths alias and typecheck script so internal packages/* resolve for types/IDE, and document how to wire cross-package imports for the build. diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index ada5924f05de..e30e5e144b49 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", "build-production": "NODE_ENV=production wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", + "typecheck": "tsgo --noEmit", "watch": "wp-build --watch" }, "wpPlugin": { @@ -38,6 +39,7 @@ }, "devDependencies": { "@babel/core": "7.29.0", + "@typescript/native-preview": "7.0.0-dev.20260225.1", "@wordpress/build": "0.13.0", "browserslist": "4.28.2" } diff --git a/projects/packages/premium-analytics/tsconfig.json b/projects/packages/premium-analytics/tsconfig.json index 1e36ba0293d3..f018f27578e3 100644 --- a/projects/packages/premium-analytics/tsconfig.json +++ b/projects/packages/premium-analytics/tsconfig.json @@ -1,4 +1,13 @@ { "extends": "jetpack-js-tools/tsconfig.base.json", + "compilerOptions": { + // Resolve cross-package imports between internal `packages/*` modules + // (`@jetpack-premium-analytics/`) to their TypeScript source for + // type-checking + IDE. The build resolves the same specifier separately (see + // README → "Internal packages"); this keeps tsc/esbuild and `tsgo` in sync. + "paths": { + "@jetpack-premium-analytics/*": [ "./packages/*/src" ] + } + }, "include": [ "routes/**/*", "packages/**/*" ] } From 20e62414b3a35c1ef8cfb65f271e5bebf95d8134 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 27 May 2026 16:16:30 +0800 Subject: [PATCH 02/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- projects/packages/premium-analytics/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/projects/packages/premium-analytics/README.md b/projects/packages/premium-analytics/README.md index 5b26b1fbcaa5..5abfb8f0e512 100644 --- a/projects/packages/premium-analytics/README.md +++ b/projects/packages/premium-analytics/README.md @@ -45,11 +45,11 @@ jetpack build packages/premium-analytics # via Jetpack CLI ```json { - "name": "-route", - "route": { - "path": "/", - "page": "jetpack-premium-analytics" - } + "name": "-route", + "route": { + "path": "/", + "page": "jetpack-premium-analytics" + } } ``` From 662c887ff7538d3ea7aa57a8026f301a11d2ae0e Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 27 May 2026 16:16:40 +0800 Subject: [PATCH 03/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- projects/packages/premium-analytics/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/README.md b/projects/packages/premium-analytics/README.md index 5abfb8f0e512..2785620dabee 100644 --- a/projects/packages/premium-analytics/README.md +++ b/projects/packages/premium-analytics/README.md @@ -92,7 +92,8 @@ To import one from a route/another package, the build also needs it symlinked in `node_modules` under that specifier. Name the package `@automattic/jetpack-premium-analytics-` (a bare `@jetpack-premium-analytics/*` name fails the repo name lint and `_@…` is invalid to pnpm), then add a `link:` -dep on the top-level `package.json` (routes aren't workspace members): +dep in this package's `projects/packages/premium-analytics/package.json` +(not the repo root `package.json`; routes aren't workspace members): ```jsonc "dependencies": { "@jetpack-premium-analytics/": "link:packages/" } From d4da9cad4304fe48243f8c8250e76a72f483e7de Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 28 May 2026 14:07:06 +0800 Subject: [PATCH 04/10] docs(premium-analytics): clarify internal-package naming and rename init Address PR review feedback on the "Internal packages" section: - Lead with scope intent: internal-only, never published, in-tree symlink-only resolution (answers the npm-squatting concern) - Explicitly explain the structural dual naming between the package name field and the wp-build-derived import specifier - Rename packages/init from `_@jetpack-premium-analytics/init` to `@automattic/jetpack-premium-analytics-init` so the codebase matches the documented pattern (the old placeholder is invalid to pnpm) --- projects/packages/premium-analytics/README.md | 40 ++++++++++++------- .../packages/init/package.json | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/projects/packages/premium-analytics/README.md b/projects/packages/premium-analytics/README.md index 2785620dabee..bf7ad81a5191 100644 --- a/projects/packages/premium-analytics/README.md +++ b/projects/packages/premium-analytics/README.md @@ -45,11 +45,11 @@ jetpack build packages/premium-analytics # via Jetpack CLI ```json { - "name": "-route", - "route": { - "path": "/", - "page": "jetpack-premium-analytics" - } + "name": "-route", + "route": { + "path": "/", + "page": "jetpack-premium-analytics" + } } ``` @@ -84,16 +84,26 @@ Serves two purposes: ## Internal packages (`packages/*`) -App-internal modules discovered by `@wordpress/build`. Types/IDE resolve -`@jetpack-premium-analytics/` imports via the `tsconfig.json` `paths` alias -(`pnpm typecheck`). - -To import one from a route/another package, the build also needs it symlinked in -`node_modules` under that specifier. Name the package -`@automattic/jetpack-premium-analytics-` (a bare `@jetpack-premium-analytics/*` -name fails the repo name lint and `_@…` is invalid to pnpm), then add a `link:` -dep in this package's `projects/packages/premium-analytics/package.json` -(not the repo root `package.json`; routes aren't workspace members): +App-internal modules used only by this package — never published to npm, never +shared across the monorepo. Resolution is entirely in-tree (the local symlink); +the `@jetpack-premium-analytics/*` scope is never looked up against any registry. + +**The dual naming is structural.** `@wordpress/build` derives the import +specifier as `@/`, so the specifier here is +always `@jetpack-premium-analytics/`. The package's own `name` field has +to be different (`@automattic/jetpack-premium-analytics-`) because pnpm +rejects the `_@…` escape and the repo name lint (`lint-project-structure.sh`) +rejects the bare `@jetpack-premium-analytics/*` scope. They don't need to +match: pnpm symlinks under the **dep key**, so the import resolves regardless +of the linked package's `name`. + +Types/IDE: the `tsconfig.json` `paths` alias maps the specifier to +`./packages//src` (covered by `pnpm typecheck`). + +Build: to import one from a route or another package, add a `link:` dep on +**this package's `package.json`** (`projects/packages/premium-analytics/package.json` — +routes aren't workspace members, so the dep belongs here, not in the route's +`package.json`): ```jsonc "dependencies": { "@jetpack-premium-analytics/": "link:packages/" } diff --git a/projects/packages/premium-analytics/packages/init/package.json b/projects/packages/premium-analytics/packages/init/package.json index 6ee2ce1e4470..70de0aea9d5b 100644 --- a/projects/packages/premium-analytics/packages/init/package.json +++ b/projects/packages/premium-analytics/packages/init/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "_@jetpack-premium-analytics/init", + "name": "@automattic/jetpack-premium-analytics-init", "version": "0.1.0", "type": "module", "wpScript": true, From b5893ea53326fcc3b6dfa2b5cf5d269afa39817c Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 13:55:42 +0800 Subject: [PATCH 05/10] test(premium-analytics): add jest infrastructure for internal packages --- pnpm-lock.yaml | 12 ++++++++++++ projects/packages/premium-analytics/jest.config.cjs | 10 ++++++++++ projects/packages/premium-analytics/package.json | 7 ++++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 projects/packages/premium-analytics/jest.config.cjs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85e3d1751a8d..4337c249f39c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3849,6 +3849,15 @@ importers: '@babel/core': specifier: 7.29.0 version: 7.29.0 + '@babel/preset-react': + specifier: 7.28.5 + version: 7.28.5(@babel/core@7.29.0) + '@testing-library/dom': + specifier: 10.4.1 + version: 10.4.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@typescript/native-preview': specifier: 7.0.0-dev.20260225.1 version: 7.0.0-dev.20260225.1 @@ -3858,6 +3867,9 @@ importers: browserslist: specifier: 4.28.2 version: 4.28.2 + jest: + specifier: 30.4.2 + version: 30.4.2 projects/packages/protect-models: {} diff --git a/projects/packages/premium-analytics/jest.config.cjs b/projects/packages/premium-analytics/jest.config.cjs new file mode 100644 index 000000000000..1d9ddc72ec86 --- /dev/null +++ b/projects/packages/premium-analytics/jest.config.cjs @@ -0,0 +1,10 @@ +const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); + +module.exports = { + ...baseConfig, + rootDir: '.', + collectCoverageFrom: [ + '/packages/**/src/**/*.{js,jsx,ts,tsx}', + ...baseConfig.collectCoverageFrom, + ], +}; diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index e30e5e144b49..d3a9b50a0dc7 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", "build-production": "NODE_ENV=production wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", "typecheck": "tsgo --noEmit", "watch": "wp-build --watch" }, @@ -39,8 +40,12 @@ }, "devDependencies": { "@babel/core": "7.29.0", + "@babel/preset-react": "7.28.5", + "@testing-library/dom": "10.4.1", + "@testing-library/react": "16.3.2", "@typescript/native-preview": "7.0.0-dev.20260225.1", "@wordpress/build": "0.13.0", - "browserslist": "4.28.2" + "browserslist": "4.28.2", + "jest": "30.4.2" } } From 7aa601a9d6ae41459feebd475d8f25a31014ab8d Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:41:00 +0800 Subject: [PATCH 06/10] feat(premium-analytics): add site-sync package --- pnpm-lock.yaml | 21 +++ .../premium-analytics/jest.config.cjs | 11 ++ .../packages/premium-analytics/package.json | 9 +- .../packages/site-sync/package.json | 18 ++ .../site-sync/src/api/fetch-sync-status.ts | 18 ++ .../site-sync/src/api/trigger-full-sync.ts | 17 ++ .../packages/site-sync/src/constants.ts | 20 +++ .../hooks/__tests__/use-sync-status.test.ts | 120 +++++++++++++ .../site-sync/src/hooks/use-sync-status.ts | 127 ++++++++++++++ .../packages/site-sync/src/index.ts | 2 + .../site-sync/src/jetpack-script-data.d.ts | 14 ++ .../packages/site-sync/src/status.test.ts | 160 ++++++++++++++++++ .../packages/site-sync/src/status.ts | 56 ++++++ .../packages/site-sync/src/types.ts | 39 +++++ 14 files changed, 631 insertions(+), 1 deletion(-) create mode 100644 projects/packages/premium-analytics/packages/site-sync/package.json create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/api/fetch-sync-status.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/api/trigger-full-sync.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/constants.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/hooks/__tests__/use-sync-status.test.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/hooks/use-sync-status.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/jetpack-script-data.d.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/status.test.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/status.ts create mode 100644 projects/packages/premium-analytics/packages/site-sync/src/types.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4337c249f39c..6dd7fdbf1814 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3824,6 +3824,12 @@ importers: projects/packages/premium-analytics: dependencies: + '@automattic/jetpack-script-data': + specifier: workspace:* + version: link:../../js-packages/script-data + '@wordpress/api-fetch': + specifier: ^7.22.0 + version: 7.46.0 '@wordpress/boot': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3846,24 +3852,39 @@ importers: specifier: 18.3.1 version: 18.3.1(react@18.3.1) devDependencies: + '@automattic/jetpack-webpack-config': + specifier: workspace:* + version: link:../../js-packages/webpack-config '@babel/core': specifier: 7.29.0 version: 7.29.0 '@babel/preset-react': specifier: 7.28.5 version: 7.28.5(@babel/core@7.29.0) + '@babel/preset-typescript': + specifier: 7.28.5 + version: 7.28.5(@babel/core@7.29.0) + '@babel/runtime': + specifier: 7.29.2 + version: 7.29.2 '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 '@testing-library/react': specifier: 16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/jest': + specifier: 30.0.0 + version: 30.0.0 '@typescript/native-preview': specifier: 7.0.0-dev.20260225.1 version: 7.0.0-dev.20260225.1 '@wordpress/build': specifier: 0.13.0 version: 0.13.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + babel-jest: + specifier: 30.4.1 + version: 30.4.1(@babel/core@7.29.0) browserslist: specifier: 4.28.2 version: 4.28.2 diff --git a/projects/packages/premium-analytics/jest.config.cjs b/projects/packages/premium-analytics/jest.config.cjs index 1d9ddc72ec86..3769cabf2379 100644 --- a/projects/packages/premium-analytics/jest.config.cjs +++ b/projects/packages/premium-analytics/jest.config.cjs @@ -7,4 +7,15 @@ module.exports = { '/packages/**/src/**/*.{js,jsx,ts,tsx}', ...baseConfig.collectCoverageFrom, ], + // Compile TS/JSX to CommonJS (rather than leaving native ESM). `jest.mock` + // hoisting relies on `require`, which is undefined when `.ts` runs as ESM + // under `--experimental-vm-modules`; emitting CJS keeps mocks working without + // that flag. Mirrors the shared transform used by js-packages/charts. + transform: { + ...baseConfig.transform, + '\\.[jt]sx?$': require( 'jetpack-js-tools/jest/babel-jest-config-factory.js' )( + require.resolve + ), + }, + extensionsToTreatAsEsm: [], }; diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index d3a9b50a0dc7..3d6b6b663651 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", "build-production": "NODE_ENV=production wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", - "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "jest", "typecheck": "tsgo --noEmit", "watch": "wp-build --watch" }, @@ -30,6 +30,8 @@ } }, "dependencies": { + "@automattic/jetpack-script-data": "workspace:*", + "@wordpress/api-fetch": "^7.22.0", "@wordpress/boot": "0.13.0", "@wordpress/data": "10.46.0", "@wordpress/i18n": "^6.9.0", @@ -39,12 +41,17 @@ "react-dom": "18.3.1" }, "devDependencies": { + "@automattic/jetpack-webpack-config": "workspace:*", "@babel/core": "7.29.0", "@babel/preset-react": "7.28.5", + "@babel/preset-typescript": "7.28.5", + "@babel/runtime": "7.29.2", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", + "@types/jest": "30.0.0", "@typescript/native-preview": "7.0.0-dev.20260225.1", "@wordpress/build": "0.13.0", + "babel-jest": "30.4.1", "browserslist": "4.28.2", "jest": "30.4.2" } diff --git a/projects/packages/premium-analytics/packages/site-sync/package.json b/projects/packages/premium-analytics/packages/site-sync/package.json new file mode 100644 index 000000000000..bbf6d4b8f2a4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/package.json @@ -0,0 +1,18 @@ +{ + "private": true, + "name": "@automattic/jetpack-premium-analytics-site-sync", + "version": "0.1.0", + "type": "module", + "wpScript": true, + "module": "build-module/index.mjs", + "wpScriptModuleExports": "./build-module/index.mjs", + "dependencies": { + "@automattic/jetpack-script-data": "workspace:*", + "@wordpress/api-fetch": "^7.22.0", + "@wordpress/i18n": "^6.9.0", + "react": "18.3.1" + }, + "devDependencies": { + "@testing-library/react": "16.3.2" + } +} diff --git a/projects/packages/premium-analytics/packages/site-sync/src/api/fetch-sync-status.ts b/projects/packages/premium-analytics/packages/site-sync/src/api/fetch-sync-status.ts new file mode 100644 index 000000000000..017c6e5c9a7f --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/api/fetch-sync-status.ts @@ -0,0 +1,18 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +/** + * Internal dependencies + */ +import { SYNC_STATUS_PATH } from '../constants'; +import type { SyncStatusApiResponse } from '../types'; + +/** + * Fetch the current sync status from Jetpack core. + * + * @return The current sync status. + */ +export function fetchSyncStatus(): Promise< SyncStatusApiResponse > { + return apiFetch( { path: SYNC_STATUS_PATH } ); +} diff --git a/projects/packages/premium-analytics/packages/site-sync/src/api/trigger-full-sync.ts b/projects/packages/premium-analytics/packages/site-sync/src/api/trigger-full-sync.ts new file mode 100644 index 000000000000..ee16471a2d5b --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/api/trigger-full-sync.ts @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +/** + * Internal dependencies + */ +import { FULL_SYNC_PATH } from '../constants'; + +/** + * Trigger a Jetpack full sync. + * + * @return The full-sync trigger response. + */ +export function triggerFullSync(): Promise< unknown > { + return apiFetch( { path: FULL_SYNC_PATH, method: 'POST' } ); +} diff --git a/projects/packages/premium-analytics/packages/site-sync/src/constants.ts b/projects/packages/premium-analytics/packages/site-sync/src/constants.ts new file mode 100644 index 000000000000..a76673f5072c --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/constants.ts @@ -0,0 +1,20 @@ +/** + * Polling interval in milliseconds. + */ +export const POLL_INTERVAL = 3_000; + +/** + * Jetpack core sync status endpoint (queue + full-sync state). + */ +export const SYNC_STATUS_PATH = '/jetpack/v4/sync/status'; + +/** + * Jetpack core full-sync trigger endpoint. + */ +export const FULL_SYNC_PATH = '/jetpack/v4/sync/full-sync'; + +/** + * Sync-module key whose progress gates the analytics dashboard. Mirrors the + * backend default (`Sync_Status_Tracker::ANALYTICS_SYNC_MODULE`). + */ +export const ANALYTICS_SYNC_MODULE = 'woocommerce_analytics'; diff --git a/projects/packages/premium-analytics/packages/site-sync/src/hooks/__tests__/use-sync-status.test.ts b/projects/packages/premium-analytics/packages/site-sync/src/hooks/__tests__/use-sync-status.test.ts new file mode 100644 index 000000000000..5aabc33db11a --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/hooks/__tests__/use-sync-status.test.ts @@ -0,0 +1,120 @@ +/** + * External dependencies + */ +import { getScriptData } from '@automattic/jetpack-script-data'; +import { renderHook, act, waitFor } from '@testing-library/react'; +/** + * Internal dependencies + */ +import { fetchSyncStatus } from '../../api/fetch-sync-status'; +import { triggerFullSync } from '../../api/trigger-full-sync'; +import { useSyncStatus } from '../use-sync-status'; +import type { SyncStatusApiResponse } from '../../types'; + +jest.mock( '../../api/fetch-sync-status' ); +jest.mock( '../../api/trigger-full-sync' ); +jest.mock( '@automattic/jetpack-script-data' ); + +const mockFetch = fetchSyncStatus as jest.MockedFunction< typeof fetchSyncStatus >; +const mockTrigger = triggerFullSync as jest.MockedFunction< typeof triggerFullSync >; +const mockScriptData = getScriptData as jest.MockedFunction< typeof getScriptData >; + +/** + * Build a raw sync-status API response for tests. + * + * @param overrides - Fields to override on the default running-analytics response. + * @return A raw sync-status API response. + */ +function rawStatus( overrides: Partial< SyncStatusApiResponse > = {} ): SyncStatusApiResponse { + return { + started: true, + finished: false, + progress: { woocommerce_analytics: { sent: 1, total: 2 } }, + ...overrides, + }; +} + +beforeEach( () => { + jest.useFakeTimers(); + // Default: milestone not set. + mockScriptData.mockReturnValue( { + premium_analytics: { initial_full_sync_finished: 0 }, + } as ReturnType< typeof getScriptData > ); + mockFetch.mockResolvedValue( rawStatus() ); + mockTrigger.mockResolvedValue( undefined ); +} ); + +afterEach( () => { + jest.clearAllTimers(); + jest.useRealTimers(); + jest.clearAllMocks(); +} ); + +describe( 'useSyncStatus', () => { + it( 'exposes normalized progress after the first poll', async () => { + const { result } = renderHook( () => useSyncStatus() ); + + await waitFor( () => expect( result.current.isLoading ).toBe( false ) ); + expect( result.current.data?.percentage ).toBe( 50 ); + expect( result.current.data?.isRunning ).toBe( true ); + expect( result.current.error ).toBeNull(); + } ); + + it( 'reports complete and stops polling when analytics reaches 100', async () => { + mockFetch.mockResolvedValue( + rawStatus( { + finished: true, + progress: { woocommerce_analytics: { sent: 2, total: 2 } }, + } ) + ); + const { result } = renderHook( () => useSyncStatus() ); + + await waitFor( () => expect( result.current.isComplete ).toBe( true ) ); + const callsAfterComplete = mockFetch.mock.calls.length; + + await act( async () => { + jest.advanceTimersByTime( 10_000 ); + } ); + expect( mockFetch.mock.calls ).toHaveLength( callsAfterComplete ); + } ); + + it( 'flags a stalled sync with an error', async () => { + mockFetch.mockResolvedValue( + rawStatus( { + started: true, + finished: true, + progress: { woocommerce_analytics: { sent: 1, total: 2 } }, + } ) + ); + const { result } = renderHook( () => useSyncStatus() ); + + await waitFor( () => expect( result.current.isStalled ).toBe( true ) ); + expect( result.current.error ).toBeInstanceOf( Error ); + } ); + + it( 'surfaces fetch errors and never rejects triggerSync', async () => { + mockFetch.mockRejectedValueOnce( new Error( 'boom' ) ); + const { result } = renderHook( () => useSyncStatus() ); + + await waitFor( () => expect( result.current.error ).toBeInstanceOf( Error ) ); + expect( result.current.error?.message ).toBe( 'boom' ); + + // triggerSync resolves even if the trigger call fails. + mockTrigger.mockRejectedValueOnce( new Error( 'nope' ) ); + await act( async () => { + await result.current.triggerSync(); + } ); + expect( result.current.error?.message ).toBe( 'nope' ); + } ); + + it( 'starts complete and skips polling when the milestone is set', async () => { + mockScriptData.mockReturnValue( { + premium_analytics: { initial_full_sync_finished: 1_700_000_000 }, + } as ReturnType< typeof getScriptData > ); + + const { result } = renderHook( () => useSyncStatus() ); + + await waitFor( () => expect( result.current.isComplete ).toBe( true ) ); + expect( mockFetch ).not.toHaveBeenCalled(); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/site-sync/src/hooks/use-sync-status.ts b/projects/packages/premium-analytics/packages/site-sync/src/hooks/use-sync-status.ts new file mode 100644 index 000000000000..8c79e2248a7d --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/hooks/use-sync-status.ts @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import { getScriptData } from '@automattic/jetpack-script-data'; +import { __ } from '@wordpress/i18n'; +import { useState, useEffect, useRef, useCallback } from 'react'; +/** + * Internal dependencies + */ +import { fetchSyncStatus } from '../api/fetch-sync-status'; +import { triggerFullSync } from '../api/trigger-full-sync'; +import { POLL_INTERVAL } from '../constants'; +import { toSyncStatus, isSyncComplete, isSyncStalled } from '../status'; +import type { SyncStatus, UseSyncStatusReturn } from '../types'; + +/** + * Read the page-load milestone injected by the backend Sync_Status_Tracker. + * Static for the lifetime of the page — never re-read while polling. + * + * @return The initial full-sync milestone (unix ts), or 0 if never finished. + */ +function readMilestone(): number { + return getScriptData()?.premium_analytics?.initial_full_sync_finished ?? 0; +} + +/** + * Polls Jetpack's sync status and returns analytics-scoped progress. + * + * Polling auto-stops when the sync is complete, stalled, or errors. If the + * page-load milestone is already set, the dashboard is gated open immediately + * and no polling occurs. `triggerSync` POSTs the full-sync trigger and resumes + * polling; it never rejects (failures surface via `error`). + * + * @return The current sync state plus a `triggerSync` action. + */ +export function useSyncStatus(): UseSyncStatusReturn { + const milestoneRef = useRef< number >( readMilestone() ); + const [ data, setData ] = useState< SyncStatus >(); + const [ error, setError ] = useState< Error | null >( null ); + const [ isStalled, setIsStalled ] = useState( false ); + + const intervalRef = useRef< ReturnType< typeof setInterval > | null >( null ); + // Hold the latest `poll` in a ref so the interval always calls the current + // closure. Preserves the original package's pollRef pattern and keeps the + // interval stable if `poll`'s identity ever changes. + const pollRef = useRef< () => void >(); + + const clearPolling = useCallback( () => { + if ( intervalRef.current ) { + clearInterval( intervalRef.current ); + intervalRef.current = null; + } + }, [] ); + + const poll = useCallback( () => { + fetchSyncStatus() + .then( raw => { + const status = toSyncStatus( raw, milestoneRef.current ); + setData( status ); + setError( null ); + setIsStalled( false ); + + if ( isSyncComplete( status ) ) { + clearPolling(); + return; + } + + if ( isSyncStalled( status ) ) { + clearPolling(); + setIsStalled( true ); + setError( + new Error( __( 'Sync has stalled. Please try again.', 'jetpack-premium-analytics' ) ) + ); + } + } ) + .catch( ( e: unknown ) => { + clearPolling(); + const message = + e instanceof Error + ? e.message + : __( 'Unable to get sync status.', 'jetpack-premium-analytics' ); + setError( new Error( message ) ); + } ); + }, [ clearPolling ] ); + + pollRef.current = poll; + + const startPolling = useCallback( () => { + clearPolling(); + intervalRef.current = setInterval( () => { + pollRef.current?.(); + }, POLL_INTERVAL ); + }, [ clearPolling ] ); + + const triggerSync = useCallback( async () => { + clearPolling(); + setError( null ); + setIsStalled( false ); + + try { + await triggerFullSync(); + poll(); + startPolling(); + } catch ( e: unknown ) { + const message = + e instanceof Error ? e.message : __( 'Unable to start sync.', 'jetpack-premium-analytics' ); + setError( new Error( message ) ); + } + }, [ clearPolling, poll, startPolling ] ); + + useEffect( () => { + // Already finished before this page load — gate open, no polling needed. + if ( milestoneRef.current > 0 ) { + setData( toSyncStatus( {}, milestoneRef.current ) ); + return; + } + + poll(); + startPolling(); + return clearPolling; + }, [ poll, startPolling, clearPolling ] ); + + const isComplete = data ? isSyncComplete( data ) : false; + const isLoading = ! data && ! error; + + return { data, error, isLoading, isComplete, isStalled, triggerSync }; +} diff --git a/projects/packages/premium-analytics/packages/site-sync/src/index.ts b/projects/packages/premium-analytics/packages/site-sync/src/index.ts new file mode 100644 index 000000000000..db2c2897a38a --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/index.ts @@ -0,0 +1,2 @@ +export { useSyncStatus } from './hooks/use-sync-status'; +export type { SyncStatus, SyncStatusApiResponse, UseSyncStatusReturn } from './types'; diff --git a/projects/packages/premium-analytics/packages/site-sync/src/jetpack-script-data.d.ts b/projects/packages/premium-analytics/packages/site-sync/src/jetpack-script-data.d.ts new file mode 100644 index 000000000000..f366df3d7891 --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/jetpack-script-data.d.ts @@ -0,0 +1,14 @@ +/** + * The backend `Sync_Status_Tracker` (jetpack PR #49211) injects this block into + * `window.JetpackScriptData` via the `jetpack_admin_js_script_data` filter. The + * base `@automattic/jetpack-script-data` types don't know about it, so augment. + */ +import '@automattic/jetpack-script-data'; + +declare module '@automattic/jetpack-script-data' { + interface JetpackScriptData { + premium_analytics?: { + initial_full_sync_finished: number; + }; + } +} diff --git a/projects/packages/premium-analytics/packages/site-sync/src/status.test.ts b/projects/packages/premium-analytics/packages/site-sync/src/status.test.ts new file mode 100644 index 000000000000..b5da2b27e181 --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/status.test.ts @@ -0,0 +1,160 @@ +import { toSyncStatus, isSyncComplete, isSyncStalled } from './status'; + +describe( 'toSyncStatus', () => { + it( 'reports not-started when the sync has never run', () => { + const status = toSyncStatus( { started: false }, 0 ); + expect( status ).toEqual( { + isStarted: false, + isRunning: false, + percentage: 0, + initialFullSyncFinished: 0, + } ); + } ); + + it( 'computes analytics-scoped percentage from the module bucket', () => { + const status = toSyncStatus( + { + started: true, + finished: false, + progress: { woocommerce_analytics: { sent: 1, total: 4 } }, + }, + 0 + ); + expect( status.isRunning ).toBe( true ); + expect( status.percentage ).toBe( 25 ); + } ); + + it( 'ignores non-analytics modules when computing percentage', () => { + const status = toSyncStatus( + { + started: true, + finished: false, + progress: { + posts: { sent: 100, total: 100 }, + woocommerce_analytics: { sent: 1, total: 2 }, + }, + }, + 0 + ); + expect( status.percentage ).toBe( 50 ); + } ); + + it( 'is 100% when the page-load milestone is set', () => { + const status = toSyncStatus( { started: false }, 1_700_000_000 ); + expect( status.percentage ).toBe( 100 ); + expect( status.initialFullSyncFinished ).toBe( 1_700_000_000 ); + } ); + + it( 'caps percentage at 100', () => { + const status = toSyncStatus( + { + started: true, + finished: false, + progress: { woocommerce_analytics: { sent: 9, total: 4 } }, + }, + 0 + ); + expect( status.percentage ).toBe( 100 ); + } ); + + it( 'is complete (and gates open) when analytics hits 100% even if finished is still false', () => { + const status = toSyncStatus( + { + started: true, + finished: false, + progress: { woocommerce_analytics: { sent: 2, total: 2 } }, + }, + 0 + ); + expect( status.percentage ).toBe( 100 ); + expect( status.isRunning ).toBe( true ); + expect( isSyncComplete( status ) ).toBe( true ); + expect( isSyncStalled( status ) ).toBe( false ); + } ); + + it( 'treats a numeric finished timestamp as finished', () => { + const status = toSyncStatus( + { + started: true, + finished: 1_700_000_000, + progress: { woocommerce_analytics: { sent: 1, total: 2 } }, + }, + 0 + ); + // total > 0 branch: floor(1/2 * 100) = 50; numeric finished coerces to true + // so isRunning = started && !finished = false. + expect( status.percentage ).toBe( 50 ); + expect( status.isRunning ).toBe( false ); + expect( isSyncStalled( status ) ).toBe( true ); + } ); +} ); + +describe( 'isSyncComplete', () => { + it( 'is complete when the milestone is set', () => { + expect( + isSyncComplete( { + isStarted: false, + isRunning: false, + percentage: 100, + initialFullSyncFinished: 1_700_000_000, + } ) + ).toBe( true ); + } ); + + it( 'is complete when analytics progress reaches 100 this session', () => { + expect( + isSyncComplete( { + isStarted: true, + isRunning: false, + percentage: 100, + initialFullSyncFinished: 0, + } ) + ).toBe( true ); + } ); + + it( 'is not complete mid-progress', () => { + expect( + isSyncComplete( { + isStarted: true, + isRunning: true, + percentage: 50, + initialFullSyncFinished: 0, + } ) + ).toBe( false ); + } ); +} ); + +describe( 'isSyncStalled', () => { + it( 'is stalled when started, no longer running, and not complete', () => { + expect( + isSyncStalled( { + isStarted: true, + isRunning: false, + percentage: 50, + initialFullSyncFinished: 0, + } ) + ).toBe( true ); + } ); + + it( 'is not stalled when it never started', () => { + expect( + isSyncStalled( { + isStarted: false, + isRunning: false, + percentage: 0, + initialFullSyncFinished: 0, + } ) + ).toBe( false ); + } ); + + it( 'is not stalled when complete', () => { + expect( + isSyncStalled( { + isStarted: true, + isRunning: false, + percentage: 100, + initialFullSyncFinished: 1_700_000_000, + } ) + ).toBe( false ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/site-sync/src/status.ts b/projects/packages/premium-analytics/packages/site-sync/src/status.ts new file mode 100644 index 000000000000..c88b416a4d55 --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/status.ts @@ -0,0 +1,56 @@ +/** + * Internal dependencies + */ +import { ANALYTICS_SYNC_MODULE } from './constants'; +import type { SyncStatus, SyncStatusApiResponse } from './types'; + +/** + * Normalize Jetpack's raw sync status into the analytics-scoped shape. + * + * @param raw - Raw GET /jetpack/v4/sync/status response. + * @param milestone - Page-load milestone (unix ts, or 0 if never finished). + * @return Analytics-scoped sync status. + */ +export function toSyncStatus( raw: SyncStatusApiResponse, milestone: number ): SyncStatus { + const started = Boolean( raw.started ); + const finished = Boolean( raw.finished ); + const bucket = raw.progress?.[ ANALYTICS_SYNC_MODULE ]; + const total = bucket?.total ?? 0; + const sent = bucket?.sent ?? 0; + + let percentage = 0; + if ( total > 0 ) { + percentage = Math.min( 100, Math.floor( ( sent / total ) * 100 ) ); + } else if ( milestone > 0 || finished ) { + // No analytics bucket in this batch, but the sync has finished (now or + // before) — treat analytics as fully synced. + percentage = 100; + } + + return { + isStarted: started, + isRunning: started && ! finished, + percentage, + initialFullSyncFinished: milestone, + }; +} + +/** + * The analytics initial sync has finished — either before this page load + * (milestone) or analytics progress reached 100 during this session. + * @param status - Normalized sync status. + * @return Whether the analytics initial sync has finished. + */ +export function isSyncComplete( status: SyncStatus ): boolean { + return status.initialFullSyncFinished > 0 || status.percentage >= 100; +} + +/** + * Stalled = the sync started but is no longer running and hasn't completed. A + * sync that never started is NOT stalled — it just needs to be triggered. + * @param status - Normalized sync status. + * @return Whether the sync has stalled. + */ +export function isSyncStalled( status: SyncStatus ): boolean { + return status.isStarted && ! status.isRunning && ! isSyncComplete( status ); +} diff --git a/projects/packages/premium-analytics/packages/site-sync/src/types.ts b/projects/packages/premium-analytics/packages/site-sync/src/types.ts new file mode 100644 index 000000000000..07262f6da459 --- /dev/null +++ b/projects/packages/premium-analytics/packages/site-sync/src/types.ts @@ -0,0 +1,39 @@ +/** + * Subset of Jetpack core's GET /jetpack/v4/sync/status response that this + * package consumes. `progress` is keyed by sync-module name; each module + * reports items `sent` of `total`. + */ +export type SyncStatusApiResponse = { + started?: boolean; + finished?: boolean | number; + progress?: Record< string, { sent?: number; total?: number } >; +}; + +/** + * Normalized, analytics-scoped sync status. + */ +export type SyncStatus = { + isStarted: boolean; + isRunning: boolean; + /** Analytics-module progress, 0–100, computed client-side. */ + percentage: number; + /** Page-load milestone: unix ts when the initial analytics sync first finished, else 0. */ + initialFullSyncFinished: number; +}; + +/** + * Return type for the useSyncStatus hook. + */ +export type UseSyncStatusReturn = { + data: SyncStatus | undefined; + error: Error | null; + isLoading: boolean; + isComplete: boolean; + isStalled: boolean; + /** + * POST the full-sync trigger and resume polling. The returned promise always + * resolves; failures surface via `error` so callers can `void triggerSync()` + * from event handlers without an unhandled rejection. + */ + triggerSync: () => Promise< void >; +}; From 84266593f9a9d3b5d86015f996fe5df308827bf6 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:41:03 +0800 Subject: [PATCH 07/10] feat(premium-analytics): configure apiFetch auth in init module --- .../premium-analytics/packages/init/package.json | 2 ++ .../premium-analytics/packages/init/src/index.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/projects/packages/premium-analytics/packages/init/package.json b/projects/packages/premium-analytics/packages/init/package.json index 70de0aea9d5b..a9c75f14d5e4 100644 --- a/projects/packages/premium-analytics/packages/init/package.json +++ b/projects/packages/premium-analytics/packages/init/package.json @@ -7,6 +7,8 @@ "module": "build-module/index.mjs", "wpScriptModuleExports": "./build-module/index.mjs", "dependencies": { + "@automattic/jetpack-script-data": "workspace:*", + "@wordpress/api-fetch": "^7.22.0", "@wordpress/boot": "0.13.0", "@wordpress/data": "10.46.0", "@wordpress/icons": "^13.0.0" diff --git a/projects/packages/premium-analytics/packages/init/src/index.ts b/projects/packages/premium-analytics/packages/init/src/index.ts index f4a5da3c37c8..3955304e7c49 100644 --- a/projects/packages/premium-analytics/packages/init/src/index.ts +++ b/projects/packages/premium-analytics/packages/init/src/index.ts @@ -1,6 +1,8 @@ /** * External dependencies */ +import { getScriptData } from '@automattic/jetpack-script-data'; +import apiFetch from '@wordpress/api-fetch'; import { store as bootStore } from '@wordpress/boot'; import { dispatch } from '@wordpress/data'; import { chartBar } from '@wordpress/icons'; @@ -10,6 +12,16 @@ import { chartBar } from '@wordpress/icons'; * Runs before routes render. */ export async function init(): Promise< void > { + // Point apiFetch at this site's REST API and authenticate requests. Required + // before any package (e.g. site-sync) calls apiFetch against /jetpack/v4/*. + const site = getScriptData()?.site; + if ( site?.rest_root ) { + apiFetch.use( apiFetch.createRootURLMiddleware( site.rest_root ) ); + } + if ( site?.rest_nonce ) { + apiFetch.use( apiFetch.createNonceMiddleware( site.rest_nonce ) ); + } + dispatch( bootStore ).updateMenuItem( 'dashboard', { icon: chartBar, } ); From adbed20fad8f4e55c15810b100d2e53fd4bde0f2 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:41:05 +0800 Subject: [PATCH 08/10] changelog: add premium-analytics site-sync entry --- projects/packages/premium-analytics/changelog/add-site-sync | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/add-site-sync diff --git a/projects/packages/premium-analytics/changelog/add-site-sync b/projects/packages/premium-analytics/changelog/add-site-sync new file mode 100644 index 000000000000..78c519e2c855 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/add-site-sync @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add site-sync package (useSyncStatus hook) and configure apiFetch auth in init. From feeb212fd34ef003d5e873ad87c6b33ec013f733 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 10:15:02 +0800 Subject: [PATCH 09/10] docs(premium-analytics): revert internal-packages README section Defer the internal-packages naming docs until the upstream wp-build identity change (gutenberg#78822 / #48089) lands; restore README to trunk. --- projects/packages/premium-analytics/README.md | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/projects/packages/premium-analytics/README.md b/projects/packages/premium-analytics/README.md index bf7ad81a5191..e3f361a44074 100644 --- a/projects/packages/premium-analytics/README.md +++ b/projects/packages/premium-analytics/README.md @@ -42,19 +42,17 @@ jetpack build packages/premium-analytics # via Jetpack CLI ### Adding a route 1. Create `routes//package.json`: - ```json { - "name": "-route", - "route": { - "path": "/", - "page": "jetpack-premium-analytics" - } + "name": "-route", + "route": { + "path": "/", + "page": "jetpack-premium-analytics" + } } ``` 2. Create `routes//stage.tsx` exporting `stage()`: - ```tsx export const stage = () =>
My new page
; ``` @@ -76,39 +74,11 @@ fixes the template or the minimum WordPress version is 7.0+. ### Init module (`packages/init/`) Serves two purposes: - 1. Sets the dashboard menu icon via `@wordpress/boot` store 2. Forces `@wordpress/build` to track `@wordpress/boot` as a module dependency — without an init module that imports boot, the build skips it -## Internal packages (`packages/*`) - -App-internal modules used only by this package — never published to npm, never -shared across the monorepo. Resolution is entirely in-tree (the local symlink); -the `@jetpack-premium-analytics/*` scope is never looked up against any registry. - -**The dual naming is structural.** `@wordpress/build` derives the import -specifier as `@/`, so the specifier here is -always `@jetpack-premium-analytics/`. The package's own `name` field has -to be different (`@automattic/jetpack-premium-analytics-`) because pnpm -rejects the `_@…` escape and the repo name lint (`lint-project-structure.sh`) -rejects the bare `@jetpack-premium-analytics/*` scope. They don't need to -match: pnpm symlinks under the **dep key**, so the import resolves regardless -of the linked package's `name`. - -Types/IDE: the `tsconfig.json` `paths` alias maps the specifier to -`./packages//src` (covered by `pnpm typecheck`). - -Build: to import one from a route or another package, add a `link:` dep on -**this package's `package.json`** (`projects/packages/premium-analytics/package.json` — -routes aren't workspace members, so the dep belongs here, not in the route's -`package.json`): - -```jsonc -"dependencies": { "@jetpack-premium-analytics/": "link:packages/" } -``` - ## File structure ``` From 2beeaa9a2c6ac17b2d050597d09229f1254bd49e Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 10:35:23 +0800 Subject: [PATCH 10/10] fix(premium-analytics): align route name with internal-package convention Rename routes/dashboard to @automattic/jetpack-premium-analytics-dashboard-route to match the packages/* naming (per review), and drop the stale changelog line about README build docs that were reverted. Build output is unchanged (routes key off the directory name). --- .../premium-analytics/changelog/add-internal-package-resolution | 2 +- .../packages/premium-analytics/routes/dashboard/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/premium-analytics/changelog/add-internal-package-resolution b/projects/packages/premium-analytics/changelog/add-internal-package-resolution index d35865145ec1..f4343aa091ea 100644 --- a/projects/packages/premium-analytics/changelog/add-internal-package-resolution +++ b/projects/packages/premium-analytics/changelog/add-internal-package-resolution @@ -1,4 +1,4 @@ Significance: patch Type: added -Add a tsconfig paths alias and typecheck script so internal packages/* resolve for types/IDE, and document how to wire cross-package imports for the build. +Add a tsconfig paths alias and typecheck script so internal packages/* resolve for types/IDE. diff --git a/projects/packages/premium-analytics/routes/dashboard/package.json b/projects/packages/premium-analytics/routes/dashboard/package.json index e71390452278..b0139ff6987b 100644 --- a/projects/packages/premium-analytics/routes/dashboard/package.json +++ b/projects/packages/premium-analytics/routes/dashboard/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "_@jetpack-premium-analytics/dashboard-route", + "name": "@automattic/jetpack-premium-analytics-dashboard-route", "route": { "path": "/", "page": "jetpack-premium-analytics"