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/15] 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/15] 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/15] 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/15] 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 ca2ffad63589eeb82c65e5d5afcdad2cc1b34d40 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 28 May 2026 14:55:00 +0800 Subject: [PATCH 05/15] feat(premium-analytics): port datetime package from next-woocommerce-analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Near-verbatim copy of next-woocommerce-analytics/packages/datetime/. Provides timezone-aware date helpers (date-fns + @date-fns/tz) and date-range presets used by analytics widgets. Monorepo adaptations: - Rename to @automattic/jetpack-premium-analytics-datetime per the internal-packages convention (parent README, "Internal packages"). - Pin date-fns 4.1.0 and @date-fns/tz 1.4.1 instead of upstream "*"; add @wordpress/i18n ^6.9.0 to match parent. - Drop the leaf tsconfig.json — parent already includes packages/**/* and packages/init has no leaf tsconfig either. - Replace `any[]` with `unknown[]` in GrowTuple constraint (tz.ts). - Add date-fns + @date-fns/tz to parent package.json so imports resolve from premium-analytics/node_modules (packages/datetime is not a pnpm workspace member, only the parent is). - ESLint text-domain autofix swept `woocommerce-analytics` → `jetpack-premium-analytics` in preset labels. - New eslint.config.mjs softens JSDoc rules for packages/datetime/** to keep upstream re-syncs mechanical (mirrors activity-log's DateRangePicker precedent). Refs WOOA7S-1312 --- pnpm-lock.yaml | 6 + .../premium-analytics/eslint.config.mjs | 18 ++ .../packages/premium-analytics/package.json | 2 + .../packages/datetime/README.md | 133 +++++++++++ .../packages/datetime/package.json | 14 ++ .../datetime/src/get-comparison-range.ts | 105 +++++++++ .../packages/datetime/src/index.ts | 49 ++++ .../datetime/src/presets/comparison.ts | 65 ++++++ .../packages/datetime/src/presets/index.ts | 27 +++ .../packages/datetime/src/presets/primary.ts | 220 ++++++++++++++++++ .../packages/datetime/src/presets/types.ts | 63 +++++ .../packages/datetime/src/tz.ts | 129 ++++++++++ 12 files changed, 831 insertions(+) create mode 100644 projects/packages/premium-analytics/eslint.config.mjs create mode 100644 projects/packages/premium-analytics/packages/datetime/README.md create mode 100644 projects/packages/premium-analytics/packages/datetime/package.json create mode 100644 projects/packages/premium-analytics/packages/datetime/src/get-comparison-range.ts create mode 100644 projects/packages/premium-analytics/packages/datetime/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/datetime/src/presets/comparison.ts create mode 100644 projects/packages/premium-analytics/packages/datetime/src/presets/index.ts create mode 100644 projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts create mode 100644 projects/packages/premium-analytics/packages/datetime/src/presets/types.ts create mode 100644 projects/packages/premium-analytics/packages/datetime/src/tz.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85e3d1751a8d..3c99cde016d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3824,6 +3824,9 @@ importers: projects/packages/premium-analytics: dependencies: + '@date-fns/tz': + specifier: 1.4.1 + version: 1.4.1 '@wordpress/boot': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3839,6 +3842,9 @@ importers: '@wordpress/route': specifier: 0.12.0 version: 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + date-fns: + specifier: 4.1.0 + version: 4.1.0 react: specifier: 18.3.1 version: 18.3.1 diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs new file mode 100644 index 000000000000..f6c971e27ddb --- /dev/null +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -0,0 +1,18 @@ +import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs'; + +/** + * `packages/datetime/` is a near-verbatim port of + * `next-woocommerce-analytics/packages/datetime/`. Forcing full JSDoc + * on the port would add churn on each upstream re-sync without adding + * clarity (param names match signatures). Soften the Jetpack profile + * for the ported sources only. + */ +export default defineConfig( makeBaseConfig( import.meta.url ), { + files: [ 'packages/datetime/**' ], + rules: { + 'jsdoc/require-description': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/check-indentation': 'off', + }, +} ); diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index e30e5e144b49..742153f28d0a 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -29,11 +29,13 @@ } }, "dependencies": { + "@date-fns/tz": "1.4.1", "@wordpress/boot": "0.13.0", "@wordpress/data": "10.46.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^13.0.0", "@wordpress/route": "0.12.0", + "date-fns": "4.1.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/projects/packages/premium-analytics/packages/datetime/README.md b/projects/packages/premium-analytics/packages/datetime/README.md new file mode 100644 index 000000000000..1570fe08a4de --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/README.md @@ -0,0 +1,133 @@ +# @next-woo-analytics/datetime + +Date and timezone utilities for WooCommerce Analytics. + +## Overview + +This package provides timezone-aware date handling and comparison range +calculations for the WooCommerce Analytics dashboard. + +## Functions + +### Timezone Utilities + +#### `createTZDateFromParts( dateParts: number[], timezone? )` + +Creates a timezone-aware date in the specified timezone using the provided date parts. +**Important:** Months are zero-based (0 = January, 11 = December). + +```ts +// October 09, 2025 00:00 AM in America/New_York time +const date = toLocalTZ( [ 2025, 9, 9 ], 'America/New_York' ); +``` + +**Parameters:** + +- `dateParts` : `number[]` - Date value to convert +- `timezone` (optional): `string` - Target timezone, default is GMT + +**Returns:** `TZDate` - Timezone-aware date object + +#### `toLocalTZ( value?, timezone? )` + +Creates a timezone-aware date in the specified timezone. + +```typescript +const date = toLocalTZ( '2024-01-15', 'America/New_York' ); +const now = toLocalTZ( undefined, '+05:30' ); // Current time in +05:30 +``` + +**Parameters:** + +- `value` (optional): `number | string | Date` - Date value to convert +- `timezone` (optional): `string` - Target timezone + +**Returns:** `TZDate` - Timezone-aware date object + +#### `formatToTimezoneNaiveString( date, timezone )` + +Formats a date to an ISO string without timezone offset. + +```typescript +const naive = formatToTimezoneNaiveString( new Date(), 'Europe/London' ); +// Returns: "2024-01-15T14:30:00.000" +``` + +**Parameters:** + +- `date`: `Date` - Date to format +- `timezone`: `string` - Timezone for interpretation + +**Returns:** `string` - ISO string without timezone offset + +#### `dateToISOStringWithTZ( date, timezone )` + +Converts a date to ISO string with timezone offset applied. + +```typescript +const withTZ = dateToISOStringWithTZ( new Date(), 'America/New_York' ); +// Returns: "2024-01-15T14:30:00.000-05:00" +``` + +**Parameters:** + +- `date`: `Date` - Date to convert +- `timezone`: `string` - Target timezone + +**Returns:** `string` - ISO string with timezone offset + +### Comparison Range Calculations + +#### `getComparisonRangeFromPreset( reference, presetId )` + +Calculates comparison date ranges based on predefined presets. + +```typescript +const reference = { + from: new Date( '2024-01-15' ), + to: new Date( '2024-01-21' ), +}; +const comparison = getComparisonRangeFromPreset( reference, 'previous-week' ); +// Returns dates for Jan 8-14, 2024 +``` + +**Parameters:** + +- `reference`: `DateRange` - Reference date range with `from` and `to` +- `presetId`: `ComparisonPresetId` - One of the supported preset identifiers + +**Returns:** `DateRange | undefined` - Comparison date range or undefined +if inputs are invalid + +**Supported presets:** + +- `previous-period` - Same duration, immediately before reference +- `previous-week` - One week before reference dates +- `previous-month` - One month before reference dates +- `previous-year` - One year before reference dates + +## Types + +### `DateRange` + +```typescript +type DateRange = { + from?: Date; + to?: Date; +}; +``` + +### `ComparisonPresetId` + +```typescript +type ComparisonPresetId = + | 'previous-period' + | 'previous-week' + | 'previous-month' + | 'previous-year'; +``` + +## Dependencies + +- `date-fns` - Date manipulation functions +- `@date-fns/tz` - Timezone support for date-fns diff --git a/projects/packages/premium-analytics/packages/datetime/package.json b/projects/packages/premium-analytics/packages/datetime/package.json new file mode 100644 index 000000000000..b4a9dfcea148 --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/package.json @@ -0,0 +1,14 @@ +{ + "name": "@automattic/jetpack-premium-analytics-datetime", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "sideEffects": false, + "dependencies": { + "@date-fns/tz": "1.4.1", + "@wordpress/i18n": "^6.9.0", + "date-fns": "4.1.0" + } +} diff --git a/projects/packages/premium-analytics/packages/datetime/src/get-comparison-range.ts b/projects/packages/premium-analytics/packages/datetime/src/get-comparison-range.ts new file mode 100644 index 000000000000..5c04d56f6323 --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/get-comparison-range.ts @@ -0,0 +1,105 @@ +/** + * External dependencies + */ +import { + differenceInDays, + subDays, + subWeeks, + subMonths, + subYears, + startOfDay, + endOfDay, +} from 'date-fns'; + +/** + * Supported comparison preset identifiers. + */ +export type DateRange = { from?: Date; to?: Date }; + +/** + * Named constants for comparison preset identifiers. + */ +export const COMPARISON_PREVIOUS_PERIOD = 'previous-period' as const; +export const COMPARISON_PREVIOUS_WEEK = 'previous-week' as const; +export const COMPARISON_PREVIOUS_MONTH = 'previous-month' as const; +export const COMPARISON_PREVIOUS_YEAR = 'previous-year' as const; + +/** + * All comparison preset identifiers, in display order. + */ +export const COMPARISON_PRESETS = [ + COMPARISON_PREVIOUS_PERIOD, + COMPARISON_PREVIOUS_WEEK, + COMPARISON_PREVIOUS_MONTH, + COMPARISON_PREVIOUS_YEAR, +] as const; + +export type ComparisonPresetId = ( typeof COMPARISON_PRESETS )[ number ]; + +/** + * Type guard to check if a string is a valid ComparisonPresetId. + * + * @param value - The value to check. + * @return True if the value is a valid ComparisonPresetId, false otherwise. + */ +export function isComparisonPresetId( value: unknown ): value is ComparisonPresetId { + return typeof value === 'string' && ( COMPARISON_PRESETS as readonly string[] ).includes( value ); +} + +/** + * Returns a comparison DateRange (as Date objects) derived from a reference range + * and a given preset. + * + * - This function is pure and has no side effects. + * - It does not apply any timezone adjustments. The caller is responsible for + * normalizing dates to the desired local day boundaries before passing them in. + * + * @param reference - The reference range to compare against (must include both `from` and `to`). + * @param presetId - One of the supported preset identifiers. + * @return A new DateRange for the comparison period, or `undefined` if inputs are invalid. + */ +export function getComparisonRangeFromPreset( + reference: DateRange, + presetId: ComparisonPresetId +): DateRange | undefined { + if ( ! reference?.from || ! reference?.to ) { + return undefined; + } + + const refFrom = reference.from; + const refTo = reference.to; + + const clampDayBound = ( date: Date, bound: 0 | 1 ) => + bound === 1 ? endOfDay( startOfDay( date ) ) : startOfDay( date ); + + if ( presetId === COMPARISON_PREVIOUS_PERIOD ) { + const daysInclusive = differenceInDays( refTo, refFrom ) + 1; + return { + from: clampDayBound( subDays( refFrom, daysInclusive ), 0 ), + to: clampDayBound( subDays( refTo, daysInclusive ), 1 ), + }; + } + + if ( presetId === COMPARISON_PREVIOUS_WEEK ) { + return { + from: clampDayBound( subWeeks( refFrom, 1 ), 0 ), + to: clampDayBound( subWeeks( refTo, 1 ), 1 ), + }; + } + + if ( presetId === COMPARISON_PREVIOUS_MONTH ) { + return { + from: clampDayBound( subMonths( refFrom, 1 ), 0 ), + to: clampDayBound( subMonths( refTo, 1 ), 1 ), + }; + } + + if ( presetId === COMPARISON_PREVIOUS_YEAR ) { + return { + from: clampDayBound( subYears( refFrom, 1 ), 0 ), + to: clampDayBound( subYears( refTo, 1 ), 1 ), + }; + } + + return undefined; +} diff --git a/projects/packages/premium-analytics/packages/datetime/src/index.ts b/projects/packages/premium-analytics/packages/datetime/src/index.ts new file mode 100644 index 000000000000..fd8f8ac3e8ff --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/index.ts @@ -0,0 +1,49 @@ +export { + getComparisonRangeFromPreset, + isComparisonPresetId, + type DateRange, + type ComparisonPresetId, +} from './get-comparison-range'; + +export { + createTZDateFromParts, + toLocalTZ, + formatToTimezoneNaiveString, + dateToISOStringWithTZ, + startOfDayTZ, + endOfDayTZ, +} from './tz'; + +export { + // Constants + SELECTABLE_PRESETS, + PRESET_TODAY, + PRESET_YESTERDAY, + PRESET_LAST_7_DAYS, + PRESET_LAST_30_DAYS, + PRESET_LAST_90_DAYS, + PRESET_LAST_365_DAYS, + PRESET_LAST_MONTH, + PRESET_LAST_12_MONTHS, + PRESET_LAST_YEAR, + PRESET_CUSTOM, + + // Guards + isSelectablePreset, + isPrimaryPreset, + + // Types + type SelectablePresetId, + type PrimaryPresetId, + + // Primary presets + PRESET_DEFINITIONS, + getPresetLabel, + getDefaultDateRangePresets, + computePrimaryRange, + type DateRangePreset, + + // Comparison presets + getComparisonPresetLabel, + getComparisonPresetConfigs, +} from './presets'; diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/comparison.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/comparison.ts new file mode 100644 index 000000000000..b993189947fc --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/comparison.ts @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { + COMPARISON_PREVIOUS_PERIOD, + COMPARISON_PREVIOUS_WEEK, + COMPARISON_PREVIOUS_MONTH, + COMPARISON_PREVIOUS_YEAR, + type ComparisonPresetId, +} from '../get-comparison-range'; + +/** + * Comparison preset label configuration. + */ +const COMPARISON_PRESET_LABELS: { + id: ComparisonPresetId; + getLabel: () => string; +}[] = [ + { + id: COMPARISON_PREVIOUS_PERIOD, + getLabel: () => __( 'Previous period', 'jetpack-premium-analytics' ), + }, + { + id: COMPARISON_PREVIOUS_WEEK, + getLabel: () => __( 'Previous week', 'jetpack-premium-analytics' ), + }, + { + id: COMPARISON_PREVIOUS_MONTH, + getLabel: () => __( 'Previous month', 'jetpack-premium-analytics' ), + }, + { + id: COMPARISON_PREVIOUS_YEAR, + getLabel: () => __( 'Previous year', 'jetpack-premium-analytics' ), + }, +]; + +/** + * Get the label for a comparison preset. + * + * @param id - The comparison preset identifier. + * @return The label string, or null if not found. + */ +export function getComparisonPresetLabel( id: ComparisonPresetId ): string | null { + const config = COMPARISON_PRESET_LABELS.find( item => item.id === id ); + return config?.getLabel() ?? null; +} + +/** + * Get all comparison preset configurations (id + label). + * + * @return Array of comparison preset configs. + */ +export function getComparisonPresetConfigs(): { + id: ComparisonPresetId; + label: string; +}[] { + return COMPARISON_PRESET_LABELS.map( ( { id, getLabel } ) => ( { + id, + label: getLabel(), + } ) ); +} diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts new file mode 100644 index 000000000000..d3e4f17ca93b --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts @@ -0,0 +1,27 @@ +export { + SELECTABLE_PRESETS, + PRESET_TODAY, + PRESET_YESTERDAY, + PRESET_LAST_7_DAYS, + PRESET_LAST_30_DAYS, + PRESET_LAST_90_DAYS, + PRESET_LAST_365_DAYS, + PRESET_LAST_MONTH, + PRESET_LAST_12_MONTHS, + PRESET_LAST_YEAR, + PRESET_CUSTOM, + isSelectablePreset, + isPrimaryPreset, + type SelectablePresetId, + type PrimaryPresetId, +} from './types'; + +export { + PRESET_DEFINITIONS, + getPresetLabel, + getDefaultDateRangePresets, + computePrimaryRange, + type DateRangePreset, +} from './primary'; + +export { getComparisonPresetLabel, getComparisonPresetConfigs } from './comparison'; diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts new file mode 100644 index 000000000000..8807338f6712 --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts @@ -0,0 +1,220 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + startOfDay, + endOfDay, + subDays, + subMonths, + subYears, + startOfMonth, + endOfMonth, + startOfYear, + endOfYear, +} from 'date-fns'; +/** + * Internal dependencies + */ +import { toLocalTZ } from '../tz'; +import { + PRESET_TODAY, + PRESET_YESTERDAY, + PRESET_LAST_7_DAYS, + PRESET_LAST_30_DAYS, + PRESET_LAST_90_DAYS, + PRESET_LAST_365_DAYS, + PRESET_LAST_MONTH, + PRESET_LAST_12_MONTHS, + PRESET_LAST_YEAR, + PRESET_CUSTOM, + type SelectablePresetId, + type PrimaryPresetId, +} from './types'; +import type { DateRange } from '../get-comparison-range'; + +/** + * Shared date calculations used by multiple presets. + */ +type DateContext = { + initOfToday: Date; + endOfToday: Date; + endOfYesterday: Date; + lastMonth: Date; + endOfLastMonth: Date; + lastYear: Date; +}; + +/** + * Preset definition with label getter and range calculator. + */ +type PresetDefinition = { + id: SelectablePresetId; + getLabel: () => string; + getRange: ( ctx: DateContext ) => Required< DateRange >; +}; + +/** + * Canonical preset definitions with labels and range calculators. + * Labels are defined once here and reused by all consumers. + */ +export const PRESET_DEFINITIONS: ReadonlyArray< PresetDefinition > = [ + { + id: PRESET_TODAY, + getLabel: () => __( 'Today', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfToday } ) => ( { + from: initOfToday, + to: endOfToday, + } ), + }, + { + id: PRESET_YESTERDAY, + getLabel: () => __( 'Yesterday', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfYesterday } ) => ( { + from: subDays( initOfToday, 1 ), + to: endOfYesterday, + } ), + }, + { + id: PRESET_LAST_7_DAYS, + getLabel: () => __( 'Last 7 days', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfYesterday } ) => ( { + from: subDays( initOfToday, 7 ), + to: endOfYesterday, + } ), + }, + { + id: PRESET_LAST_30_DAYS, + getLabel: () => __( 'Last 30 days', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfYesterday } ) => ( { + from: subDays( initOfToday, 30 ), + to: endOfYesterday, + } ), + }, + { + id: PRESET_LAST_90_DAYS, + getLabel: () => __( 'Last 90 days', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfYesterday } ) => ( { + from: subDays( initOfToday, 90 ), + to: endOfYesterday, + } ), + }, + { + id: PRESET_LAST_365_DAYS, + getLabel: () => __( 'Last 365 days', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfYesterday } ) => ( { + from: subDays( initOfToday, 365 ), + to: endOfYesterday, + } ), + }, + { + id: PRESET_LAST_MONTH, + getLabel: () => __( 'Last month', 'jetpack-premium-analytics' ), + getRange: ( { lastMonth, endOfLastMonth } ) => ( { + from: startOfMonth( lastMonth ), + to: endOfLastMonth, + } ), + }, + { + id: PRESET_LAST_12_MONTHS, + getLabel: () => __( 'Last 12 months', 'jetpack-premium-analytics' ), + getRange: ( { initOfToday, endOfLastMonth } ) => ( { + from: startOfMonth( subMonths( initOfToday, 12 ) ), + to: endOfLastMonth, + } ), + }, + { + id: PRESET_LAST_YEAR, + getLabel: () => __( 'Last year', 'jetpack-premium-analytics' ), + getRange: ( { lastYear } ) => ( { + from: startOfYear( lastYear ), + to: endOfYear( lastYear ), + } ), + }, +]; + +/** + * Get the label for a preset without calculating date ranges. + * + * @param id - The preset identifier + * @return The preset label, or null if not found or custom + */ +export function getPresetLabel( id: PrimaryPresetId | null | undefined ): string | null { + if ( ! id || id === PRESET_CUSTOM ) { + return null; + } + + const preset = PRESET_DEFINITIONS.find( p => p.id === id ); + return preset?.getLabel() ?? null; +} + +/** + * Build a DateContext for a given timezone. + * @param timeZone + */ +function buildDateContext( timeZone: string ): DateContext { + const nowWithTZ = toLocalTZ( undefined, timeZone ); + const initOfToday = startOfDay( nowWithTZ ); + const endOfToday = endOfDay( nowWithTZ ); + const endOfYesterday = endOfDay( subDays( initOfToday, 1 ) ); + const lastMonth = subMonths( initOfToday, 1 ); + const endOfLastMonth = endOfMonth( lastMonth ); + const lastYear = subYears( initOfToday, 1 ); + + return { + initOfToday, + endOfToday, + endOfYesterday, + lastMonth, + endOfLastMonth, + lastYear, + }; +} + +/** + * Represents a date range preset option. + * Preset ranges always have both `from` and `to` defined. + */ +export type DateRangePreset = { + id: PrimaryPresetId; + label: string; + range: Required< DateRange >; +}; + +/** + * Get the default date range presets with computed ranges. + * + * @param timeZone - IANA timezone string (e.g., 'America/New_York') + * @return The default date range presets. + */ +export function getDefaultDateRangePresets( timeZone: string ): DateRangePreset[] { + const ctx = buildDateContext( timeZone ); + + return PRESET_DEFINITIONS.map( ( { id, getLabel, getRange } ) => ( { + id, + label: getLabel(), + range: getRange( ctx ), + } ) ); +} + +/** + * Compute the absolute date range (as Date objects) for a given + * selectable preset ID in the specified timezone. + * + * @param presetId - A valid selectable preset identifier. + * @param timeZone - IANA timezone string. + * @return The computed { from, to } Date range, or undefined + * if the preset is not recognized. + */ +export function computePrimaryRange( + presetId: SelectablePresetId, + timeZone: string +): DateRange | undefined { + const def = PRESET_DEFINITIONS.find( p => p.id === presetId ); + if ( ! def ) { + return undefined; + } + + const ctx = buildDateContext( timeZone ); + return def.getRange( ctx ); +} diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts new file mode 100644 index 000000000000..03023e6f677c --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts @@ -0,0 +1,63 @@ +/** + * Named constants for selectable date-range presets. + */ +export const PRESET_TODAY = 'today' as const; +export const PRESET_YESTERDAY = 'yesterday' as const; +export const PRESET_LAST_7_DAYS = 'last-7-days' as const; +export const PRESET_LAST_30_DAYS = 'last-30-days' as const; +export const PRESET_LAST_90_DAYS = 'last-90-days' as const; +export const PRESET_LAST_365_DAYS = 'last-365-days' as const; +export const PRESET_LAST_MONTH = 'last-month' as const; +export const PRESET_LAST_12_MONTHS = 'last-12-months' as const; +export const PRESET_LAST_YEAR = 'last-year' as const; + +/** + * All selectable (non-custom) preset IDs, in display order. + */ +export const SELECTABLE_PRESETS = [ + PRESET_TODAY, + PRESET_YESTERDAY, + PRESET_LAST_7_DAYS, + PRESET_LAST_30_DAYS, + PRESET_LAST_90_DAYS, + PRESET_LAST_365_DAYS, + PRESET_LAST_MONTH, + PRESET_LAST_12_MONTHS, + PRESET_LAST_YEAR, +] as const; + +/** + * Union of the 9 selectable preset identifiers. + */ +export type SelectablePresetId = ( typeof SELECTABLE_PRESETS )[ number ]; + +/** + * The custom marker — not user-selectable, used as a disabled state. + */ +export const PRESET_CUSTOM = 'custom' as const; + +/** + * Primary preset: one of the 9 selectable presets, or 'custom'. + */ +export type PrimaryPresetId = SelectablePresetId | typeof PRESET_CUSTOM; + +/** + * Type guard to check if a value is a selectable preset ID. + * + * @param value - The value to check. + * @return True if the value is a valid SelectablePresetId. + */ +export function isSelectablePreset( value: unknown ): value is SelectablePresetId { + return typeof value === 'string' && ( SELECTABLE_PRESETS as readonly string[] ).includes( value ); +} + +/** + * Type guard to check if a value is any primary preset ID + * (selectable or custom). + * + * @param value - The value to check. + * @return True if the value is a valid PrimaryPresetId. + */ +export function isPrimaryPreset( value: unknown ): value is PrimaryPresetId { + return isSelectablePreset( value ) || value === PRESET_CUSTOM; +} diff --git a/projects/packages/premium-analytics/packages/datetime/src/tz.ts b/projects/packages/premium-analytics/packages/datetime/src/tz.ts new file mode 100644 index 000000000000..64faf355a796 --- /dev/null +++ b/projects/packages/premium-analytics/packages/datetime/src/tz.ts @@ -0,0 +1,129 @@ +/** + * External dependencies + */ +import { tz, TZDate, TZDateMini } from '@date-fns/tz'; +import { format, isValid, startOfDay, endOfDay } from 'date-fns'; + +type GrowTuple< T extends unknown[], Max extends number > = T[ 'length' ] extends Max + ? T + : T | GrowTuple< [ ...T, number ], Max >; +/** + * Date parts tuple in the same order as the native `Date` constructor: + * [ year, month, day, hours, minutes, seconds, milliseconds ] + * + * Positions: + * - year: full year, e.g. 2025 + * - month: month index 0–11 (0=January, 11=December) + * - day: day of month 1–31 (default 1 if omitted) + * - hours: 0–23 (default 0) + * - minutes: 0–59 (default 0) + * - seconds: 0–59 (default 0) + * - milliseconds: 0–999 (default 0) + * + * Rules: + * - Valid lengths: 2 to 7 elements (must always start with [year, month]). + * - Do not skip intermediate positions: contiguous prefixes only (trimmed at the first `undefined`). + * - Time zone is applied when creating the date (see `createTZDateFromParts`). + * + * Examples: + * - [ 2025, 0 ] → 2025-01-01T00:00:00.000 (January is 0) + * - [ 2025, 6, 15, 14, 30 ] → 2025-07-15T14:30:00.000 + */ +type DateParts = GrowTuple< [ number, number ], 7 >; + +/** + * + * @param root0 + * @param root0."0" + * @param root0."1" + * @param root0."2" + * @param root0."3" + * @param root0."4" + * @param root0."5" + * @param root0."6" + * @param timeZone + */ +export function createTZDateFromParts( + [ year, month, day, hours, minutes, seconds, milliseconds ]: DateParts, + timeZone?: string +): TZDate { + const tzid = timeZone ?? '+00:00'; + + const dateParts = [ year, month, day, hours, minutes, seconds, milliseconds ]; + + // Trim until first undefined, to match one of the DateParts types. + const idx = dateParts.indexOf( undefined ); + const datePartsTrimmed = idx === -1 ? dateParts : dateParts.slice( 0, idx ); + + // @ts-expect-error: We know datePartsTrimmed is a tuple of numbers, spreading is safe. + return new TZDateMini( ...datePartsTrimmed, tzid ); +} + +/** + * Create a TZDate in the provided timezone. + * Mirrors your current localTZDate, applies UTC to be default TZ. + * @param value + * @param timeZone + */ +export function toLocalTZ( value?: number | string | Date, timeZone?: string ): TZDate { + const tzid = timeZone ?? '+00:00'; + if ( value !== undefined ) { + return new TZDateMini( value as number, tzid ); + } + + return TZDateMini.tz( tzid ); +} + +/** + * Format a date to a timezone-naive ISO string (no offset), + * using the given timezone for interpretation. + * Example: TZDateMini("...+01:00") -> "YYYY-MM-DDTHH:mm:ss.SSS" + * @param date + * @param timezone + */ +export function formatToTimezoneNaiveString( date: Date, timezone: string ): string { + if ( ! isValid( date ) ) { + throw new Error( 'Invalid date provided' ); + } + return format( date, "yyyy-MM-dd'T'HH:mm:ss.SSS", { in: tz( timezone ) } ); +} + +/** + * Convert a date to ISO string with the timezone offset applied. + * Example output: "YYYY-MM-DDTHH:mm:ss.SSS±hh:mm" + * @param date + * @param timezone + */ +export function dateToISOStringWithTZ( date: Date, timezone: string ): string { + return format( date, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", { + in: tz( timezone ), + } ); +} + +/** + * Returns the start of day (00:00:00) for the given date in the specified timezone. + * + * @param date - The date to get the start of day for + * @param timeZone - Timezone string (e.g., 'America/New_York', 'UTC', '+08:00') + * @return A Date object representing midnight in the specified timezone + */ +export function startOfDayTZ( date: Date | number, timeZone: string ): Date { + // Create TZDate in the target timezone - this interprets the input date in that timezone + const tzDate = new TZDateMini( new Date( date ).getTime(), timeZone ); + // startOfDay from date-fns respects the timezone context in TZDate + return startOfDay( tzDate ); +} + +/** + * Returns the end of day (23:59:59.999) for the given date in the specified timezone. + * + * @param date - The date to get the end of day for + * @param timeZone - Timezone string (e.g., 'America/New_York', 'UTC', '+08:00') + * @return A Date object representing the last millisecond of the day in the specified timezone + */ +export function endOfDayTZ( date: Date | number, timeZone: string ): Date { + // Create TZDate in the target timezone - this interprets the input date in that timezone + const tzDate = new TZDateMini( new Date( date ).getTime(), timeZone ); + // endOfDay from date-fns respects the timezone context in TZDate + return endOfDay( tzDate ); +} From c442ab7a2f2467f42d3f27da0c168d26844138e6 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 28 May 2026 14:58:25 +0800 Subject: [PATCH 06/15] changelog: add entry for premium-analytics datetime port Refs WOOA7S-1312 --- .../wooa7s-1312-integrate-datetime-package-into-analytics | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1312-integrate-datetime-package-into-analytics diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1312-integrate-datetime-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1312-integrate-datetime-package-into-analytics new file mode 100644 index 000000000000..56b6c3ea2da2 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1312-integrate-datetime-package-into-analytics @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Port datetime package (timezone-aware date helpers and date-range presets) as an internal package from next-woocommerce-analytics. From 9e3b7a191e38d2ddbc67c598c5af101872a48c28 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 28 May 2026 15:05:20 +0800 Subject: [PATCH 07/15] docs(premium-analytics): rewrite datetime README header for monorepo context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Title matches the import specifier consumers actually type. - Description swaps WooCommerce Analytics → Jetpack Premium Analytics. - Notes it's an internal (non-published) package and points to the parent README for the dual-naming convention. Formatter normalized list bullet spacing and one type-union wrap as a side-effect of saving the file. Refs WOOA7S-1312 --- .../packages/datetime/README.md | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/projects/packages/premium-analytics/packages/datetime/README.md b/projects/packages/premium-analytics/packages/datetime/README.md index 1570fe08a4de..2a8f72108714 100644 --- a/projects/packages/premium-analytics/packages/datetime/README.md +++ b/projects/packages/premium-analytics/packages/datetime/README.md @@ -1,11 +1,15 @@ -# @next-woo-analytics/datetime +# @jetpack-premium-analytics/datetime -Date and timezone utilities for WooCommerce Analytics. +Date and timezone utilities for Jetpack Premium Analytics. + +Internal package — bundled with `@automattic/jetpack-premium-analytics`, +not published to npm. Imported as `@jetpack-premium-analytics/datetime` +(see the parent `README.md` → "Internal packages" for the naming convention). ## Overview -This package provides timezone-aware date handling and comparison range -calculations for the WooCommerce Analytics dashboard. +Provides timezone-aware date handling and comparison range calculations +for analytics widgets and date-range pickers. ## Functions @@ -23,8 +27,8 @@ const date = toLocalTZ( [ 2025, 9, 9 ], 'America/New_York' ); **Parameters:** -- `dateParts` : `number[]` - Date value to convert -- `timezone` (optional): `string` - Target timezone, default is GMT +- `dateParts` : `number[]` - Date value to convert +- `timezone` (optional): `string` - Target timezone, default is GMT **Returns:** `TZDate` - Timezone-aware date object @@ -39,8 +43,8 @@ const now = toLocalTZ( undefined, '+05:30' ); // Current time in +05:30 **Parameters:** -- `value` (optional): `number | string | Date` - Date value to convert -- `timezone` (optional): `string` - Target timezone +- `value` (optional): `number | string | Date` - Date value to convert +- `timezone` (optional): `string` - Target timezone **Returns:** `TZDate` - Timezone-aware date object @@ -55,8 +59,8 @@ const naive = formatToTimezoneNaiveString( new Date(), 'Europe/London' ); **Parameters:** -- `date`: `Date` - Date to format -- `timezone`: `string` - Timezone for interpretation +- `date`: `Date` - Date to format +- `timezone`: `string` - Timezone for interpretation **Returns:** `string` - ISO string without timezone offset @@ -71,8 +75,8 @@ const withTZ = dateToISOStringWithTZ( new Date(), 'America/New_York' ); **Parameters:** -- `date`: `Date` - Date to convert -- `timezone`: `string` - Target timezone +- `date`: `Date` - Date to convert +- `timezone`: `string` - Target timezone **Returns:** `string` - ISO string with timezone offset @@ -93,18 +97,18 @@ const comparison = getComparisonRangeFromPreset( reference, 'previous-week' ); **Parameters:** -- `reference`: `DateRange` - Reference date range with `from` and `to` -- `presetId`: `ComparisonPresetId` - One of the supported preset identifiers +- `reference`: `DateRange` - Reference date range with `from` and `to` +- `presetId`: `ComparisonPresetId` - One of the supported preset identifiers **Returns:** `DateRange | undefined` - Comparison date range or undefined if inputs are invalid **Supported presets:** -- `previous-period` - Same duration, immediately before reference -- `previous-week` - One week before reference dates -- `previous-month` - One month before reference dates -- `previous-year` - One year before reference dates +- `previous-period` - Same duration, immediately before reference +- `previous-week` - One week before reference dates +- `previous-month` - One month before reference dates +- `previous-year` - One year before reference dates ## Types @@ -120,14 +124,10 @@ type DateRange = { ### `ComparisonPresetId` ```typescript -type ComparisonPresetId = - | 'previous-period' - | 'previous-week' - | 'previous-month' - | 'previous-year'; +type ComparisonPresetId = 'previous-period' | 'previous-week' | 'previous-month' | 'previous-year'; ``` ## Dependencies -- `date-fns` - Date manipulation functions -- `@date-fns/tz` - Timezone support for date-fns +- `date-fns` - Date manipulation functions +- `@date-fns/tz` - Timezone support for date-fns From df8a8390a4e8cbbfec1c7a059eec2e85d3e44abc Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 28 May 2026 15:05:49 +0800 Subject: [PATCH 08/15] chore(premium-analytics): reframe datetime eslint override as temporary Drop the upstream-path reference; note that backfilling JSDoc on the ported helpers should let us remove this whole config file. Refs WOOA7S-1312 --- projects/packages/premium-analytics/eslint.config.mjs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index f6c971e27ddb..4d905c2efea4 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -1,11 +1,9 @@ import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs'; /** - * `packages/datetime/` is a near-verbatim port of - * `next-woocommerce-analytics/packages/datetime/`. Forcing full JSDoc - * on the port would add churn on each upstream re-sync without adding - * clarity (param names match signatures). Soften the Jetpack profile - * for the ported sources only. + * Soften JSDoc rules for `packages/datetime/**` so the initial port can + * land. Temporary — backfill proper descriptions on the helpers and + * remove this override (at which point this whole file can go away). */ export default defineConfig( makeBaseConfig( import.meta.url ), { files: [ 'packages/datetime/**' ], From fd81f0c1f04b19caaae7ab58f709da63e0aeae86 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 28 May 2026 15:07:40 +0800 Subject: [PATCH 09/15] docs(premium-analytics): drop internal-package note from datetime README Refs WOOA7S-1312 --- .../packages/premium-analytics/packages/datetime/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/projects/packages/premium-analytics/packages/datetime/README.md b/projects/packages/premium-analytics/packages/datetime/README.md index 2a8f72108714..ab3c31552b12 100644 --- a/projects/packages/premium-analytics/packages/datetime/README.md +++ b/projects/packages/premium-analytics/packages/datetime/README.md @@ -2,10 +2,6 @@ Date and timezone utilities for Jetpack Premium Analytics. -Internal package — bundled with `@automattic/jetpack-premium-analytics`, -not published to npm. Imported as `@jetpack-premium-analytics/datetime` -(see the parent `README.md` → "Internal packages" for the naming convention). - ## Overview Provides timezone-aware date handling and comparison range calculations From feeb212fd34ef003d5e873ad87c6b33ec013f733 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 10:15:02 +0800 Subject: [PATCH 10/15] 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 11/15] 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" From c5d75bff60523ac6b9d938f2a1b700331fa76124 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 11:14:54 +0800 Subject: [PATCH 12/15] docs(premium-analytics): use canonical package name in datetime README heading --- projects/packages/premium-analytics/packages/datetime/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/packages/datetime/README.md b/projects/packages/premium-analytics/packages/datetime/README.md index ab3c31552b12..f94054f304b5 100644 --- a/projects/packages/premium-analytics/packages/datetime/README.md +++ b/projects/packages/premium-analytics/packages/datetime/README.md @@ -1,4 +1,4 @@ -# @jetpack-premium-analytics/datetime +# @automattic/jetpack-premium-analytics-datetime Date and timezone utilities for Jetpack Premium Analytics. From a50d4fe8181eefa0543bfda290ea214dc84b9443 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:00:08 +0800 Subject: [PATCH 13/15] Update projects/packages/premium-analytics/packages/datetime/README.md Co-authored-by: Dognose --- projects/packages/premium-analytics/packages/datetime/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/packages/datetime/README.md b/projects/packages/premium-analytics/packages/datetime/README.md index f94054f304b5..ce284079bfee 100644 --- a/projects/packages/premium-analytics/packages/datetime/README.md +++ b/projects/packages/premium-analytics/packages/datetime/README.md @@ -18,7 +18,7 @@ Creates a timezone-aware date in the specified timezone using the provided date ```ts // October 09, 2025 00:00 AM in America/New_York time -const date = toLocalTZ( [ 2025, 9, 9 ], 'America/New_York' ); +const date = createTZDateFromParts( [ 2025, 9, 9 ], 'America/New_York' ); ``` **Parameters:** From ff83ab54444fa21888ed3539cd4431a847dda3ba Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:00:16 +0800 Subject: [PATCH 14/15] Update projects/packages/premium-analytics/packages/datetime/src/tz.ts Co-authored-by: Dognose --- projects/packages/premium-analytics/packages/datetime/src/tz.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/packages/datetime/src/tz.ts b/projects/packages/premium-analytics/packages/datetime/src/tz.ts index 64faf355a796..dce262f117dc 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/tz.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/tz.ts @@ -61,7 +61,7 @@ export function createTZDateFromParts( /** * Create a TZDate in the provided timezone. - * Mirrors your current localTZDate, applies UTC to be default TZ. + * Create a TZDate in the provided timezone. Defaults to UTC when no timezone is given. * @param value * @param timeZone */ From dfb5e5150d8e4262de6cc51aaf97ccc8b4666b1e Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 14:05:54 +0800 Subject: [PATCH 15/15] Update projects/packages/premium-analytics/packages/datetime/src/tz.ts Co-authored-by: Dognose --- projects/packages/premium-analytics/packages/datetime/src/tz.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/packages/datetime/src/tz.ts b/projects/packages/premium-analytics/packages/datetime/src/tz.ts index dce262f117dc..fae1a696e4e4 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/tz.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/tz.ts @@ -61,7 +61,7 @@ export function createTZDateFromParts( /** * Create a TZDate in the provided timezone. - * Create a TZDate in the provided timezone. Defaults to UTC when no timezone is given. + * Defaults to UTC when no timezone is given. * @param value * @param timeZone */