Skip to content

Des 1325/split root exports#27071

Merged
peterzimon merged 14 commits intomainfrom
DES-1325/split-root-exports
Apr 2, 2026
Merged

Des 1325/split root exports#27071
peterzimon merged 14 commits intomainfrom
DES-1325/split-root-exports

Conversation

@peterzimon
Copy link
Copy Markdown
Contributor

ref https://linear.app/ghost/issue/DES-1325/split-root-exports

  • This change introduces clear, layered @tryghost/shade entrypoints (tokens, primitives, components, patterns, app, utils) so consumers import from explicit boundaries instead of a monolithic root barrel. We’re doing this to reduce API drift and accidental coupling, enable staged migration with soft deprecations, and give engineers/agents stronger guardrails for consistent reuse and long-term maintainability.

ref DES-1325
Added layered subpath exports, token surfaces, docs rules, and root deprecation markers.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Important

Review skipped

Too many files!

This PR contains 229 files, which is 79 over the limit of 150.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18ad7ff5-c324-4bfc-94b1-f5333c8bbe28

📥 Commits

Reviewing files that changed from the base of the PR and between b848107 and 3a687d7.

📒 Files selected for processing (229)
  • apps/activitypub/.eslintrc.cjs
  • apps/activitypub/src/components/feed/deleted-feed-item.tsx
  • apps/activitypub/src/components/feed/feed-item-menu.tsx
  • apps/activitypub/src/components/feed/feed-item-stats.tsx
  • apps/activitypub/src/components/feed/feed-item.tsx
  • apps/activitypub/src/components/feed/table-of-contents.tsx
  • apps/activitypub/src/components/global/ap-avatar.tsx
  • apps/activitypub/src/components/global/back-button.tsx
  • apps/activitypub/src/components/global/follow-button.tsx
  • apps/activitypub/src/components/global/image-lightbox.tsx
  • apps/activitypub/src/components/global/profile-preview-hover-card.tsx
  • apps/activitypub/src/components/global/show-replies-button.tsx
  • apps/activitypub/src/components/global/suggested-profiles.tsx
  • apps/activitypub/src/components/layout/error/error.tsx
  • apps/activitypub/src/components/layout/header/header.tsx
  • apps/activitypub/src/components/layout/header/search-input.tsx
  • apps/activitypub/src/components/layout/onboarding/step-1.tsx
  • apps/activitypub/src/components/layout/onboarding/step-2.tsx
  • apps/activitypub/src/components/layout/onboarding/step-3.tsx
  • apps/activitypub/src/components/layout/sidebar/feedback-box.tsx
  • apps/activitypub/src/components/layout/sidebar/recommendations.tsx
  • apps/activitypub/src/components/layout/sidebar/sidebar-menu-link.tsx
  • apps/activitypub/src/components/layout/sidebar/sidebar.tsx
  • apps/activitypub/src/components/modals/new-note-modal.tsx
  • apps/activitypub/src/components/modals/search.tsx
  • apps/activitypub/src/components/topic-filter.tsx
  • apps/activitypub/src/views/explore/explore.tsx
  • apps/activitypub/src/views/feed/components/feed-list.tsx
  • apps/activitypub/src/views/feed/components/suggested-profiles.tsx
  • apps/activitypub/src/views/feed/note.tsx
  • apps/activitypub/src/views/inbox/components/customizer.tsx
  • apps/activitypub/src/views/inbox/components/inbox-list.tsx
  • apps/activitypub/src/views/inbox/components/reader.tsx
  • apps/activitypub/src/views/notifications/components/notification-icon.tsx
  • apps/activitypub/src/views/notifications/notifications.tsx
  • apps/activitypub/src/views/preferences/components/bluesky-sharing.tsx
  • apps/activitypub/src/views/preferences/components/edit-profile.tsx
  • apps/activitypub/src/views/preferences/components/moderation.tsx
  • apps/activitypub/src/views/preferences/components/profile.tsx
  • apps/activitypub/src/views/preferences/components/settings.tsx
  • apps/activitypub/src/views/profile/components/actor-list.tsx
  • apps/activitypub/src/views/profile/components/likes.tsx
  • apps/activitypub/src/views/profile/components/posts.tsx
  • apps/activitypub/src/views/profile/components/profile-menu.tsx
  • apps/activitypub/src/views/profile/components/profile-page.tsx
  • apps/activitypub/src/views/profile/components/unblock-button.tsx
  • apps/activitypub/src/views/profile/components/unblock-dialog.tsx
  • apps/admin-x-framework/.eslintrc.cjs
  • apps/admin-x-framework/src/providers/router-provider.tsx
  • apps/admin-x-settings/.eslintrc.cjs
  • apps/admin-x-settings/src/components/settings/email-design/color-picker-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/body-font-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/button-corners-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/button-style-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/heading-font-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/heading-weight-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/image-corners-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/link-style-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/design-fields/title-alignment-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx
  • apps/admin-x-settings/src/components/settings/email-design/email-preview.tsx
  • apps/admin-x-settings/src/components/settings/email-design/header-image-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/show-badge-field.tsx
  • apps/admin-x-settings/src/components/settings/email-design/welcome-email-preview-content.tsx
  • apps/admin-x-settings/src/components/settings/growth/explore.tsx
  • apps/admin-x-settings/src/components/settings/growth/offers/offers-index.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
  • apps/admin/eslint.config.js
  • apps/admin/src/layout/admin-layout.tsx
  • apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx
  • apps/admin/src/layout/app-sidebar/app-sidebar-footer.tsx
  • apps/admin/src/layout/app-sidebar/app-sidebar-header.tsx
  • apps/admin/src/layout/app-sidebar/app-sidebar.tsx
  • apps/admin/src/layout/app-sidebar/mobile-nav-bar.tsx
  • apps/admin/src/layout/app-sidebar/nav-content.tsx
  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
  • apps/admin/src/layout/app-sidebar/nav-main.tsx
  • apps/admin/src/layout/app-sidebar/nav-menu-item.tsx
  • apps/admin/src/layout/app-sidebar/nav-settings.tsx
  • apps/admin/src/layout/app-sidebar/theme-errors-banner.tsx
  • apps/admin/src/layout/app-sidebar/theme-errors-dialog.tsx
  • apps/admin/src/layout/app-sidebar/upgrade-banner.tsx
  • apps/admin/src/layout/app-sidebar/user-menu-avatar.tsx
  • apps/admin/src/layout/app-sidebar/user-menu-item.tsx
  • apps/admin/src/layout/app-sidebar/user-menu.tsx
  • apps/admin/src/whats-new/components/whats-new-banner.tsx
  • apps/admin/src/whats-new/components/whats-new-dialog.tsx
  • apps/posts/.eslintrc.cjs
  • apps/posts/src/components/label-picker/edit-row.tsx
  • apps/posts/src/components/label-picker/label-filter-renderer.tsx
  • apps/posts/src/components/label-picker/label-picker.tsx
  • apps/posts/src/components/member-avatar.tsx
  • apps/posts/src/components/virtual-table/load-more-button.tsx
  • apps/posts/src/hooks/filter-sources/create-combined-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-hybrid-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-local-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-remote-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-email-post-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-label-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-member-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-tier-value-source.ts
  • apps/posts/src/hooks/filter-sources/utils.ts
  • apps/posts/src/hooks/use-filter-params.ts
  • apps/posts/src/hooks/use-label-picker.ts
  • apps/posts/src/hooks/with-feature-flag.tsx
  • apps/posts/src/routes.tsx
  • apps/posts/src/utils/kpi-helpers.ts
  • apps/posts/src/views/PostAnalytics/Growth/components/growth-sources.tsx
  • apps/posts/src/views/PostAnalytics/Growth/growth.tsx
  • apps/posts/src/views/PostAnalytics/Newsletter/components/feedback.tsx
  • apps/posts/src/views/PostAnalytics/Newsletter/components/newsletter-radial-chart.tsx
  • apps/posts/src/views/PostAnalytics/Newsletter/newsletter.tsx
  • apps/posts/src/views/PostAnalytics/Overview/components/newsletter-overview.tsx
  • apps/posts/src/views/PostAnalytics/Overview/components/web-overview.tsx
  • apps/posts/src/views/PostAnalytics/Overview/overview.tsx
  • apps/posts/src/views/PostAnalytics/Web/components/kpis.tsx
  • apps/posts/src/views/PostAnalytics/Web/components/locations.tsx
  • apps/posts/src/views/PostAnalytics/Web/components/sources.tsx
  • apps/posts/src/views/PostAnalytics/Web/web.tsx
  • apps/posts/src/views/PostAnalytics/components/date-range-select.tsx
  • apps/posts/src/views/PostAnalytics/components/disabled-sources-indicator.tsx
  • apps/posts/src/views/PostAnalytics/components/empty-stat-view.tsx
  • apps/posts/src/views/PostAnalytics/components/kpi-card.tsx
  • apps/posts/src/views/PostAnalytics/components/post-analytics-content.tsx
  • apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
  • apps/posts/src/views/PostAnalytics/components/sidebar.tsx
  • apps/posts/src/views/PostAnalytics/components/source-icon.tsx
  • apps/posts/src/views/PostAnalytics/components/stats-filter.tsx
  • apps/posts/src/views/PostAnalytics/modals/share-modal.tsx
  • apps/posts/src/views/PostAnalytics/post-analytics.tsx
  • apps/posts/src/views/Tags/components/tags-content.tsx
  • apps/posts/src/views/Tags/components/tags-header.tsx
  • apps/posts/src/views/Tags/components/tags-list.tsx
  • apps/posts/src/views/Tags/tags.tsx
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comment-content.tsx
  • apps/posts/src/views/comments/components/comment-header.tsx
  • apps/posts/src/views/comments/components/comment-likes-modal.tsx
  • apps/posts/src/views/comments/components/comment-menu.tsx
  • apps/posts/src/views/comments/components/comment-metrics.tsx
  • apps/posts/src/views/comments/components/comment-reports-modal.tsx
  • apps/posts/src/views/comments/components/comment-thread-list.tsx
  • apps/posts/src/views/comments/components/comment-thread-sidebar.tsx
  • apps/posts/src/views/comments/components/comments-content.tsx
  • apps/posts/src/views/comments/components/comments-filters.tsx
  • apps/posts/src/views/comments/components/comments-header.tsx
  • apps/posts/src/views/comments/components/comments-list.tsx
  • apps/posts/src/views/comments/components/disable-commenting-dialog.tsx
  • apps/posts/src/views/comments/hooks/use-filter-state.ts
  • apps/posts/src/views/members/components/bulk-action-modals/add-label-modal.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/delete-modal.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/import-members-modal.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/import-members/components/complete-step.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/import-members/components/error-step.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/import-members/components/init-step.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/import-members/components/mapping-step.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/import-members/components/processing-step.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/remove-label-modal.tsx
  • apps/posts/src/views/members/components/bulk-action-modals/unsubscribe-modal.tsx
  • apps/posts/src/views/members/components/manage-view-popover.tsx
  • apps/posts/src/views/members/components/member-table-chrome.tsx
  • apps/posts/src/views/members/components/members-actions.tsx
  • apps/posts/src/views/members/components/members-content.tsx
  • apps/posts/src/views/members/components/members-filters.tsx
  • apps/posts/src/views/members/components/members-header-search.tsx
  • apps/posts/src/views/members/components/members-header.tsx
  • apps/posts/src/views/members/components/members-list-item.tsx
  • apps/posts/src/views/members/components/members-list.tsx
  • apps/posts/src/views/members/hooks/use-members-filter-state.ts
  • apps/posts/src/views/members/members.tsx
  • apps/posts/src/views/members/use-member-filter-fields.test.ts
  • apps/posts/src/views/members/use-member-filter-fields.ts
  • apps/posts/test/unit/hooks/create-hybrid-value-source.test.tsx
  • apps/shade/README.md
  • apps/shade/src/app.ts
  • apps/shade/src/components.ts
  • apps/shade/src/docs/architecture.mdx
  • apps/shade/src/docs/contributing.mdx
  • apps/shade/src/docs/introduction.mdx
  • apps/shade/src/docs/migration-root-imports.mdx
  • apps/shade/src/index.ts
  • apps/shade/src/patterns.ts
  • apps/stats/.eslintrc.cjs
  • apps/stats/src/components/chart/custom-tooltip-content.tsx
  • apps/stats/src/hooks/use-filter-params.ts
  • apps/stats/src/hooks/use-growth-stats.ts
  • apps/stats/src/hooks/use-newsletter-stats-with-range.ts
  • apps/stats/src/hooks/use-top-posts-stats-with-range.ts
  • apps/stats/src/hooks/use-top-sources-growth.ts
  • apps/stats/src/views/Stats/Growth/components/growth-kpis.tsx
  • apps/stats/src/views/Stats/Growth/components/growth-sources.tsx
  • apps/stats/src/views/Stats/Growth/components/new-subscribers-cadence.tsx
  • apps/stats/src/views/Stats/Growth/components/paid-subscription-change-chart.tsx
  • apps/stats/src/views/Stats/Growth/growth.tsx
  • apps/stats/src/views/Stats/Locations/components/locations-card.tsx
  • apps/stats/src/views/Stats/Newsletters/components/newsletters-kpis.tsx
  • apps/stats/src/views/Stats/Newsletters/newsletters.tsx
  • apps/stats/src/views/Stats/Overview/components/latest-post.tsx
  • apps/stats/src/views/Stats/Overview/components/overview-kpis.tsx
  • apps/stats/src/views/Stats/Overview/components/top-posts.tsx
  • apps/stats/src/views/Stats/Overview/overview.tsx
  • apps/stats/src/views/Stats/Web/components/sources-card.tsx
  • apps/stats/src/views/Stats/Web/components/top-content.tsx
  • apps/stats/src/views/Stats/Web/components/web-kpis.tsx
  • apps/stats/src/views/Stats/Web/web.tsx
  • apps/stats/src/views/Stats/components/date-range-select.tsx
  • apps/stats/src/views/Stats/components/disabled-sources-indicator.tsx
  • apps/stats/src/views/Stats/components/feature-image-placeholder.tsx
  • apps/stats/src/views/Stats/components/newsletter-select.tsx
  • apps/stats/src/views/Stats/components/post-menu.tsx
  • apps/stats/src/views/Stats/components/sort-button.tsx
  • apps/stats/src/views/Stats/components/source-icon.tsx
  • apps/stats/src/views/Stats/components/stats-filter.tsx
  • apps/stats/src/views/Stats/layout/empty-stat-view.tsx
  • apps/stats/src/views/Stats/layout/stats-content.tsx
  • apps/stats/src/views/Stats/layout/stats-header.tsx
  • apps/stats/src/views/Stats/layout/stats-view.tsx
  • apps/stats/test/unit/components/chart/custom-tooltip-content.test.tsx
  • apps/stats/test/unit/components/growth/new-subscribers-cadence.test.tsx
  • apps/stats/test/unit/components/growth/paid-subscription-change-chart.test.tsx
  • apps/stats/test/unit/hooks/use-growth-stats.test.tsx
  • apps/stats/test/unit/hooks/use-newsletter-stats-with-range.test.tsx
  • apps/stats/test/unit/hooks/use-top-posts-stats-with-range.test.tsx
  • apps/stats/test/unit/hooks/use-top-sources-growth.test.tsx
  • apps/stats/test/utils/date-testing-utils.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR restructures the Shade package into layered entrypoints by adding tokens.ts, primitives.ts, components.ts, patterns.ts, app.ts, and utils.ts. package.json gains an explicit exports map and expands published files to include multiple CSS artifacts. Core utilities are split: DS-safe helpers move to ds-utils, domain/transitional helpers move to app-utils, and index.ts now re-exports and deprecates certain app-layer APIs. Theme CSS variables are extracted to theme-variables.css (imported from styles.css and included in tokens.css), and documentation files are added/updated.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Des 1325/split root exports' clearly describes the main change: splitting the monolithic @tryghost/shade root exports into layered entrypoints per the PR objectives.
Description check ✅ Passed The description directly relates to the changeset, explaining the motivation for introducing layered entrypoints (tokens, primitives, components, patterns, app, utils) and referencing the Linear issue DES-1325.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DES-1325/split-root-exports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
apps/shade/src/tokens.ts (1)

2-2: Rename exported token list to camelCase to match repo convention.

SHADE_TOKEN_NAMES should be camelCase in apps/shade/src files.

Proposed change
-export const SHADE_TOKEN_NAMES = [
+export const shadeTokenNames = [
...
-] as const;
+] as const;
 
-export type ShadeTokenName = (typeof SHADE_TOKEN_NAMES)[number];
+export type ShadeTokenName = (typeof shadeTokenNames)[number];

As per coding guidelines, "apps/shade/src/**/*.{ts,tsx,js}: Use camelCase for function and variable names".

Also applies to: 287-287

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/tokens.ts` at line 2, Rename the exported constant
SHADE_TOKEN_NAMES to camelCase (e.g., shadeTokenNames) across the codebase:
update the export in apps/shade/src/tokens.ts and replace all references/imports
(search for SHADE_TOKEN_NAMES) to the new identifier so imports, type
annotations, and usages in functions/components continue to work; ensure any
re-exports or tests referencing SHADE_TOKEN_NAMES are updated and run the type
checker to catch missed occurrences.
apps/shade/src/primitives.ts (1)

2-7: Use @ alias for internal re-exports instead of relative paths.

Please switch these re-exports to the project alias form (e.g. @/components/layout/page) for consistency with the apps/shade/src import convention.

As per coding guidelines, "Use the @ alias for internal imports (e.g., @/lib/utils)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/primitives.ts` around lines 2 - 7, Update the internal
re-exports in primitives.ts to use the project alias instead of relative paths:
replace './components/...' imports with '@/components/...' for each export
(e.g., export * from '@/components/layout/page', export {ErrorPage} from
'@/components/layout/error-page', export * from '@/components/layout/heading',
export * from '@/components/layout/header', export * from
'@/components/layout/list-header', export * from
'@/components/layout/view-header') so the file follows the apps/shade/src alias
convention.
apps/shade/src/components.ts (1)

2-59: Use @ alias for internal component/asset exports.

Please convert internal ./components/... and ./assets/... export paths to @/... aliases for consistency with the Shade src import convention.

As per coding guidelines, "Use the @ alias for internal imports (e.g., @/lib/utils)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/components.ts` around lines 2 - 59, Update all internal
relative export paths to use the project alias: replace every export from
'./components/ui/...' with '@/components/ui/...' (including the named Icon
export: "IconComponents as Icon") and replace the SVG asset exports like
"ReactComponent as FacebookLogo", "GhostLogo", "GhostOrb", "GoogleLogo",
"TwitterLogo", "XLogo" to import from '@/assets/images/...' instead of
'./assets/images/...'; leave external package exports (e.g.,
DropdownMenuCheckboxItemProps from '@radix-ui/react-dropdown-menu') unchanged.
apps/shade/src/utils.ts (1)

5-21: Switch local re-exports to @ alias paths.

For useGlobalDirtyState, useSimplePagination, and ./lib/utils re-exports, use @/... paths instead of relative imports.

As per coding guidelines, "Use the @ alias for internal imports (e.g., @/lib/utils)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/utils.ts` around lines 5 - 21, Replace the local relative
re-exports with the project alias paths: change the default export for
useGlobalDirtyState to export from "@/hooks/use-global-dirty-state", change
useSimplePagination to export from "@/hooks/use-simple-pagination", and change
the collective re-exports (cn, debounce, kebabToPascalCase, formatTimestamp,
formatNumber, formatDuration, formatPercentage, formatDisplayDate,
formatDisplayTime, getCountryFlag, stringToHslColor, abbreviateNumber) to export
from "@/lib/utils" so all three re-export statements use the `@` alias instead
of relative `./` paths.
apps/shade/src/patterns.ts (1)

2-7: Use @ alias for internal exports in this entrypoint.

Please replace ./components/... paths with @/... paths to keep import style consistent across apps/shade/src.

As per coding guidelines, "Use the @ alias for internal imports (e.g., @/lib/utils)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/patterns.ts` around lines 2 - 7, The export entries in
patterns.ts use relative paths; update each to the project alias form starting
with "@/". Replace the module specifiers for ColorPicker (default and
ColorPickerProps), PostShareModal, table-filter-tabs, and utm-campaign-tabs
(including the type export for CampaignType and TabType) to use
"@/components/..." instead of "./components/..." so the file exports:
ColorPicker, ColorPickerProps, PostShareModal, table-filter-tabs exports, and
utm-campaign-tabs exports all import via the @ alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/shade/src/app.ts`:
- Around line 2-4: The barrel currently re-exports internal modules using
relative paths; update the re-export sources to use the project alias (@"...")
instead of "./..." so imports follow the coding guideline. Specifically, change
the export sources for the symbols ShadeApp (default export) and ShadeAppProps
(type) from './shade-app' to the corresponding alias path (e.g., '@/shade-app'
or the correct rooted alias that points to that module), and change the export
source for useFocusContext from './providers/shade-provider' to the aliased path
(e.g., '@/providers/shade-provider'); keep the exported symbol names identical
and only replace the module specifiers with the `@-alias`.

In `@apps/shade/src/index.ts`:
- Around line 87-93: The wildcard export ("export * from '@/lib/utils'") is
leaking the entire utils surface and bypassing your curated exports (cn,
debounce, kebabToPascalCase, formatTimestamp, etc.) and the deprecated
domain/transitional helpers (formatUrl, formatQueryDate, isValidDomain, ...).
Remove the wildcard export and only re-export the explicit curated symbols you
intend to expose (the two export lines already shown), ensuring both the primary
helpers (cn, debounce, ...) and the deprecated helpers (formatUrl,
formatQueryDate, ...) are listed; then run type checks to confirm no missing
exports and update the explicit lists if any additional utilities must remain
public.

---

Nitpick comments:
In `@apps/shade/src/components.ts`:
- Around line 2-59: Update all internal relative export paths to use the project
alias: replace every export from './components/ui/...' with
'@/components/ui/...' (including the named Icon export: "IconComponents as
Icon") and replace the SVG asset exports like "ReactComponent as FacebookLogo",
"GhostLogo", "GhostOrb", "GoogleLogo", "TwitterLogo", "XLogo" to import from
'@/assets/images/...' instead of './assets/images/...'; leave external package
exports (e.g., DropdownMenuCheckboxItemProps from
'@radix-ui/react-dropdown-menu') unchanged.

In `@apps/shade/src/patterns.ts`:
- Around line 2-7: The export entries in patterns.ts use relative paths; update
each to the project alias form starting with "@/". Replace the module specifiers
for ColorPicker (default and ColorPickerProps), PostShareModal,
table-filter-tabs, and utm-campaign-tabs (including the type export for
CampaignType and TabType) to use "@/components/..." instead of
"./components/..." so the file exports: ColorPicker, ColorPickerProps,
PostShareModal, table-filter-tabs exports, and utm-campaign-tabs exports all
import via the @ alias.

In `@apps/shade/src/primitives.ts`:
- Around line 2-7: Update the internal re-exports in primitives.ts to use the
project alias instead of relative paths: replace './components/...' imports with
'@/components/...' for each export (e.g., export * from
'@/components/layout/page', export {ErrorPage} from
'@/components/layout/error-page', export * from '@/components/layout/heading',
export * from '@/components/layout/header', export * from
'@/components/layout/list-header', export * from
'@/components/layout/view-header') so the file follows the apps/shade/src alias
convention.

In `@apps/shade/src/tokens.ts`:
- Line 2: Rename the exported constant SHADE_TOKEN_NAMES to camelCase (e.g.,
shadeTokenNames) across the codebase: update the export in
apps/shade/src/tokens.ts and replace all references/imports (search for
SHADE_TOKEN_NAMES) to the new identifier so imports, type annotations, and
usages in functions/components continue to work; ensure any re-exports or tests
referencing SHADE_TOKEN_NAMES are updated and run the type checker to catch
missed occurrences.

In `@apps/shade/src/utils.ts`:
- Around line 5-21: Replace the local relative re-exports with the project alias
paths: change the default export for useGlobalDirtyState to export from
"@/hooks/use-global-dirty-state", change useSimplePagination to export from
"@/hooks/use-simple-pagination", and change the collective re-exports (cn,
debounce, kebabToPascalCase, formatTimestamp, formatNumber, formatDuration,
formatPercentage, formatDisplayDate, formatDisplayTime, getCountryFlag,
stringToHslColor, abbreviateNumber) to export from "@/lib/utils" so all three
re-export statements use the `@` alias instead of relative `./` paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2cb71748-3352-4e73-bb4e-30b72e6549b2

📥 Commits

Reviewing files that changed from the base of the PR and between c51c2de and 742fdcc.

📒 Files selected for processing (13)
  • apps/shade/package.json
  • apps/shade/src/app.ts
  • apps/shade/src/components.ts
  • apps/shade/src/docs/architecture.mdx
  • apps/shade/src/docs/contributing.mdx
  • apps/shade/src/index.ts
  • apps/shade/src/patterns.ts
  • apps/shade/src/primitives.ts
  • apps/shade/src/tokens.ts
  • apps/shade/src/utils.ts
  • apps/shade/styles.css
  • apps/shade/theme-variables.css
  • apps/shade/tokens.css

ref DES-1325
Replaced per-app lint deny-lists with physical utility module boundaries.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
apps/shade/src/lib/app-utils.ts (5)

311-311: Avoid mutating moment objects; use .clone() before .startOf().

moment.startOf() mutates the moment object in place. While this currently works because itemDate is recreated each iteration, it's a fragile pattern that can introduce subtle bugs if the code is modified later.

Proposed fix
-                currentWeek = itemDate.startOf('week');
+                currentWeek = itemDate.clone().startOf('week');
-                currentMonth = itemDate.startOf('month');
+                currentMonth = itemDate.clone().startOf('month');

Also applies to: 355-355

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/lib/app-utils.ts` at line 311, Avoid mutating the original
moment by cloning before calling startOf: replace uses where you call
itemDate.startOf('week') (assigning to currentWeek) with
itemDate.clone().startOf('week') so itemDate remains unchanged; apply the same
change for the second occurrence referenced around the same block (the other
itemDate.startOf('week') at ~355) to ensure both currentWeek assignments use
clone().

281-378: Extract aggregation logic to reduce duplication and cognitive complexity.

The weekly and monthly aggregation blocks (lines 286-329 and 330-374) share nearly identical logic. SonarCloud flags cognitive complexity failures for these inner functions. Consider extracting a shared aggregation helper.

Proposed refactor sketch
type AggregationUnit = 'week' | 'month';

const aggregateByUnit = <T extends {date: string}>(
    data: T[],
    unit: AggregationUnit,
    fieldName: keyof T,
    aggregationType: 'sum' | 'avg' | 'exact'
): T[] => {
    const aggregatedData: T[] = [];
    let currentPeriod = moment(data[0].date).clone().startOf(unit);
    let total = 0;
    let count = 0;
    let lastValue = 0;

    const computeValue = () => {
        if (aggregationType === 'sum') return total;
        if (aggregationType === 'avg') return count > 0 ? total / count : 0;
        return lastValue;
    };

    data.forEach((item, index) => {
        const itemDate = moment(item.date);
        if (itemDate.isSame(currentPeriod, unit)) {
            total += Number(item[fieldName]);
            count += 1;
            lastValue = Number(item[fieldName]);
        } else {
            aggregatedData.push({
                ...data[index - 1],
                date: currentPeriod.format('YYYY-MM-DD'),
                [fieldName]: computeValue()
            } as T);
            currentPeriod = itemDate.clone().startOf(unit);
            total = Number(item[fieldName]);
            count = 1;
            lastValue = Number(item[fieldName]);
        }

        if (index === data.length - 1) {
            aggregatedData.push({
                ...item,
                date: currentPeriod.format('YYYY-MM-DD'),
                [fieldName]: computeValue()
            } as T);
        }
    });

    return aggregatedData;
};

Then sanitizeChartData becomes:

export const sanitizeChartData = <T extends {date: string}>(...) => {
    if (!data.length) return [];
    if (range >= 91 && range <= 356) {
        return aggregateByUnit(data, 'week', fieldName, aggregationType);
    } else if (range > 356) {
        return aggregateByUnit(data, 'month', fieldName, aggregationType);
    }
    return data;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/lib/app-utils.ts` around lines 281 - 378, The weekly and
monthly branches in sanitizeChartData duplicate aggregation logic and raise
cognitive complexity; extract that logic into a new helper (e.g.,
aggregateByUnit) that accepts (data, unit: 'week'|'month', fieldName,
aggregationType) and encapsulates currentPeriod, total/count/lastValue handling
and a computeValue() function, then replace the weekly and monthly branches to
call aggregateByUnit(data, 'week', ...) or aggregateByUnit(data, 'month', ...);
ensure sanitizeChartData still returns [] for empty data and delegates
aggregation to aggregateByUnit for range checks.

403-416: Minor improvements for optional chaining and array access.

Static analysis suggests using optional chaining and .at() for cleaner code.

Proposed fix
 export const formatMemberName = (member: {name?: string; email?: string}) => {
-    return (member.name && member.name.trim()) || member.email || 'Unknown Member';
+    return member.name?.trim() || member.email || 'Unknown Member';
 };
 
 export const getMemberInitials = (member: {name?: string}) => {
     const name = formatMemberName(member);
     const words = name.split(' ');
     if (words.length >= 2) {
-        return (words[0][0] + words[words.length - 1][0]).toUpperCase();
+        return (words[0][0] + words.at(-1)![0]).toUpperCase();
     }
     return name.substring(0, 2).toUpperCase();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/lib/app-utils.ts` around lines 403 - 416, Refactor
getMemberInitials to use optional chaining and safer array access: call
formatMemberName(member) as before, then split into words and use words.at(0)
and words.at(-1) with optional chaining to read first characters; guard against
undefined before accessing [0] and fallback to
name?.substring(0,2)?.toUpperCase(); ensure formatMemberName remains unchanged
and update getMemberInitials to avoid direct numeric indexing and possible
undefined property access.

12-95: Acknowledge high cognitive complexity in formatUrl.

SonarCloud flags this function with cognitive complexity of 23 (allowed 15). However, URL normalization inherently involves many edge cases (mailto, anchors, protocol-relative, relative paths, etc.). If refactoring is desired, consider extracting early-return cases into separate helper functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/lib/app-utils.ts` around lines 12 - 95, The formatUrl function
has high cognitive complexity (SonarCloud >15); to fix, extract discrete
early-return/edge-case blocks into small named helper functions (e.g.,
isNullableCase, normalizeEmail, isAnchorLink, isProtocolRelative,
ensureAbsoluteWithBase, looksLikeUrl) and move URL-parsing/relative-to-base
logic into a separate function (e.g., computeRelativeUrl or
normalizeRelativeToBase) called by formatUrl; keep formatUrl as an orchestrator
that calls these new helpers and returns early, preserving current behavior and
tests while reducing the complexity metric for formatUrl.

4-4: Use @ alias for internal imports.

The coding guidelines specify using the @ alias for internal imports.

Proposed fix
-import {formatDisplayDate} from './ds-utils';
+import {formatDisplayDate} from '@/lib/ds-utils';

As per coding guidelines: "Use the @ alias for internal imports (e.g., @/lib/utils)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/lib/app-utils.ts` at line 4, Replace the relative import of
formatDisplayDate in app-utils.ts with the project `@` alias: find the import
statement that references './ds-utils' and change it to use the alias (e.g.,
import { formatDisplayDate } from '@/lib/ds-utils') so internal modules follow
the coding guideline; update any other similar relative imports in this file to
use the `@` alias as well.
apps/shade/src/lib/ds-utils.ts (1)

128-130: Use Number.isNaN and Number.isFinite for stricter type checking.

Global isNaN and isFinite coerce their argument to a number before checking, which can produce unexpected results (e.g., isNaN("hello") returns true). The Number.* variants are stricter and only return true for actual NaN/finite number values.

Proposed fix
 export const formatTimestamp = (timestamp: string) => {
     const date = new Date(timestamp);
     const now = new Date();
 
     // Handle invalid dates
-    if (isNaN(date.getTime())) {
+    if (Number.isNaN(date.getTime())) {
         return 'Unknown';
     }
 export const formatNumber = (value: number): string => {
-    if (isNaN(value) || !isFinite(value)) {
+    if (Number.isNaN(value) || !Number.isFinite(value)) {
         return '0';
     }
     return new Intl.NumberFormat('en-US').format(Math.round(value));
 };

Also applies to: 166-169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/lib/ds-utils.ts` around lines 128 - 130, Replace the loose
global checks with the stricter Number.* variants: where the code currently uses
isNaN(date.getTime()) (and the similar check around lines 166-169), change to
Number.isNaN(date.getTime()) and also guard with Number.isFinite(date.getTime())
as appropriate; update any branches that rely on isFinite or isNaN to use
Number.isFinite and Number.isNaN on the value returned by date.getTime() (refer
to the date variable and its getTime() calls) so only actual NaN/finite number
values are evaluated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/shade/src/lib/ds-utils.ts`:
- Around line 242-249: Change the types of the saturation and lightness
parameters on stringToHslColor from string to number (they represent 0–100
percentages) and update any callers that pass quoted numerals to pass numeric
literals instead; specifically update the function signature for
stringToHslColor and change calls such as in member-avatar (component
MemberAvatar / file apps/posts/src/components/member-avatar.tsx) that currently
pass `'75'` and `'55'` to pass 75 and 55 as numbers so type checking and
concatenation in the return value remain correct.

---

Nitpick comments:
In `@apps/shade/src/lib/app-utils.ts`:
- Line 311: Avoid mutating the original moment by cloning before calling
startOf: replace uses where you call itemDate.startOf('week') (assigning to
currentWeek) with itemDate.clone().startOf('week') so itemDate remains
unchanged; apply the same change for the second occurrence referenced around the
same block (the other itemDate.startOf('week') at ~355) to ensure both
currentWeek assignments use clone().
- Around line 281-378: The weekly and monthly branches in sanitizeChartData
duplicate aggregation logic and raise cognitive complexity; extract that logic
into a new helper (e.g., aggregateByUnit) that accepts (data, unit:
'week'|'month', fieldName, aggregationType) and encapsulates currentPeriod,
total/count/lastValue handling and a computeValue() function, then replace the
weekly and monthly branches to call aggregateByUnit(data, 'week', ...) or
aggregateByUnit(data, 'month', ...); ensure sanitizeChartData still returns []
for empty data and delegates aggregation to aggregateByUnit for range checks.
- Around line 403-416: Refactor getMemberInitials to use optional chaining and
safer array access: call formatMemberName(member) as before, then split into
words and use words.at(0) and words.at(-1) with optional chaining to read first
characters; guard against undefined before accessing [0] and fallback to
name?.substring(0,2)?.toUpperCase(); ensure formatMemberName remains unchanged
and update getMemberInitials to avoid direct numeric indexing and possible
undefined property access.
- Around line 12-95: The formatUrl function has high cognitive complexity
(SonarCloud >15); to fix, extract discrete early-return/edge-case blocks into
small named helper functions (e.g., isNullableCase, normalizeEmail,
isAnchorLink, isProtocolRelative, ensureAbsoluteWithBase, looksLikeUrl) and move
URL-parsing/relative-to-base logic into a separate function (e.g.,
computeRelativeUrl or normalizeRelativeToBase) called by formatUrl; keep
formatUrl as an orchestrator that calls these new helpers and returns early,
preserving current behavior and tests while reducing the complexity metric for
formatUrl.
- Line 4: Replace the relative import of formatDisplayDate in app-utils.ts with
the project `@` alias: find the import statement that references './ds-utils'
and change it to use the alias (e.g., import { formatDisplayDate } from
'@/lib/ds-utils') so internal modules follow the coding guideline; update any
other similar relative imports in this file to use the `@` alias as well.

In `@apps/shade/src/lib/ds-utils.ts`:
- Around line 128-130: Replace the loose global checks with the stricter
Number.* variants: where the code currently uses isNaN(date.getTime()) (and the
similar check around lines 166-169), change to Number.isNaN(date.getTime()) and
also guard with Number.isFinite(date.getTime()) as appropriate; update any
branches that rely on isFinite or isNaN to use Number.isFinite and Number.isNaN
on the value returned by date.getTime() (refer to the date variable and its
getTime() calls) so only actual NaN/finite number values are evaluated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c0e457e0-9abc-4b93-abc2-825e12bebc83

📥 Commits

Reviewing files that changed from the base of the PR and between 742fdcc and be8ecb0.

📒 Files selected for processing (6)
  • apps/shade/src/app.ts
  • apps/shade/src/index.ts
  • apps/shade/src/lib/app-utils.ts
  • apps/shade/src/lib/ds-utils.ts
  • apps/shade/src/lib/utils.ts
  • apps/shade/src/utils.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/shade/src/app.ts
  • apps/shade/src/utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/shade/src/index.ts

ref https://linear.app/ghost/issue/DES-1325

Wave A migrates ShadeApp and ShadeAppProps consumption off the root entrypoint so app-shell APIs are consumed via the dedicated app lane.
ref https://linear.app/ghost/issue/DES-1325

Wave B moves consumer imports from the root Shade entrypoint to @tryghost/shade/components and @tryghost/shade/primitives to keep layer boundaries explicit during MS-1 migration.
ref https://linear.app/ghost/issue/DES-1325

Wave C migrates filter/value-source contracts and feature compositions off root and components entrypoints so pattern ownership stays explicit in the subpath API.
ref https://linear.app/ghost/issue/DES-1325

Wave D moves remaining DS-safe and domain helper imports from the root Shade entrypoint to @tryghost/shade/utils and @tryghost/shade/app for explicit lane ownership.
ref https://linear.app/ghost/issue/DES-1325

Phase 5 removes app/utils symbols from the root barrel, publishes migration mapping docs, and blocks new root imports in consuming apps via path-based lint guardrails.
…xports

# Conflicts:
#	apps/posts/src/components/label-picker/label-filter-renderer.tsx
#	apps/posts/src/components/label-picker/label-picker.tsx
#	apps/shade/src/index.ts
Kept DES-1325 focused on Shade export-surface and import migration only.
This keeps DES-1325 free of unrelated package version changes.
Aligned barrel re-exports with the @/* import guideline used in shade.
Updated stats tests to mock @tryghost/shade/app and /utils directly, aligned date mocks with Moment-based app helpers, and fixed newsletters type issues surfaced during CI build.
Resolved posts TS errors by aligning percentage and color helper types with Shade signatures after export-surface changes.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
7.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@peterzimon peterzimon added the preview Deploy a PR preview environment label Apr 2, 2026
@Ghost-Slimer Ghost-Slimer temporarily deployed to pr-preview-27071 April 2, 2026 14:06 Destroyed
@peterzimon peterzimon merged commit ffe3e5b into main Apr 2, 2026
40 of 41 checks passed
@peterzimon peterzimon deleted the DES-1325/split-root-exports branch April 2, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Deploy a PR preview environment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants