Skip to main content

We've added knip for automated dependency management. It helps identify and manage unused dependencies, exports, and files across our codebase. Among many others, this tool performs static analysis to detect:

  • Unused dependencies in packages
  • Unused exports from modules
  • Unused files and entry points

Removing these should reduce bundle size by identifying truly unused code, prevents dependency creep, improves code maintainability by removing dead code paths and automates what was previously a manual cleanup process.

Configuration and related scripts are managed as part of the infrastructure/linters package.

To run checks:

pnpm run knip

To fix auto-fixable issues:

pnpm run knip --fix

For more information, you can read the full documentation.

Rhapsody now uses the latest v7 release of PNPM. This upgrade includes some security and performance improvements as well as some additional configuration that will allow us to manage the growing Rhapsody monorepo more effectively.

Run npm i -g pnpm@7 in the Rhapsody directory to upgrade.

In CI, Rhapsody's test runner - Jest - collects the list of packages that have changed plus their dependents. It then runs tests just for those packages. This change should cut down the amount of tests run for a given PR and will continue to decrease as we move more code out of the legacy src directory into apps and shared packages.

For VS Code users, Rhapsody has a CLI tool to configure the workspace with Rhapsody-specific settings. Currently, this includes getting ESLint working in our monorepo setup as well as configuring the Private Extensions extension to list the Rhapsody VS Code extension.

Run pnpm setup-ide:vscode to set up your workspace.

Settings pages can be scaffolded out using the @shared/settings-kit package. This initial release contains components to build settings cards, a pre-configured save button, and a component to include actions (such as a save button) in the header bar for full page settings. All settings pages should be built using these components.

See the package README for documentation and examples.

Sourcemaps in production and QA have been restored πŸ™Œ Accessing them requires an additional server running on your local machine. This has been built into dev-o-nomicon and can be set up using the following commands:

cd ~/dev-o-nomicon
git pull
cd services/sourcemaps
make
docker-compose up -d

Once the server is running, sourcemaps in production and QA will show in the browser's Sources tab.

Rhapsody's production server is now an nginx instance. Now, instead of Express serving server-side rendered templates, we generate static HTML from those same templates when Rhapsody first boots. The static HTML is then served by nginx using a fairly standard nginx configuration. This migration from Express to Nginx simplifies our production environments as well as reduces the resources Rhapsody consumes in production. It will unlock additional developer experience improvements in the near future as well.

Rhapsody server templates are now written in EJS. If you are working with Rhapsody's server templates, you will immediately notice that the Pug files are gone. EJS is a templating language (much like ERB in Ruby or EEX files in Elixir) that embeds JS expressions inside other file types, HTML files in Rhapsody's case. Rhapsody's templates are now much more similar in syntax to the HTML files they will eventually become. This change also simplifies some upcoming server infrastructure work as we continue to simplify Rhapsody's production environment.

See the EJS website for more information on EJS itself.

The Rhapsody platform now handles syncing non-Angular routing events with the Angular digest cycle. Manual invocations of document.body.dispatchEvent(new CustomEvent('requestDigest')) can safely be removed.

The documentation on Forcing a Digest Cycle has been updated to reflect this change.

Previously, if a .env existed, Rhapsody would use it to provide environment variable overrides only when running Rhapsody locally (pnpm local). Now, Rhapsody will use the .env file for environment variable overrides when running Rhapsody against a QA environment as well (pnpm qa 1).

All packages in Rhapsody now own their own dependencies. We’re treating the src directory like a package too so it has a package.json file with all the dependencies used within specified. Some directories were relocated, including moving /styles to /src/_angular-styles, /templates to /src/_angular-templates, /test to /src/_angular-tests, and /vendor to /src/_angular-vendor to better communicate the relationship these directories have to our Angular code. Every package now also has an owner specified in its package.json file and scaffolding scripts for new packages have been updated to require an owner entry. These changes unlock a whole new level of team autonomy and ownership in Rhapsody as well as lays the groundwork for teams to more rapidly transition over to the app folder structure.

Rhapsody now uses the pnpm package manager to manage dependencies. PNPM is a battle-tested NPM alternative with first-class support for workspaces and monorepos. In fact, we've been using it at Salesloft for years in the Design System repo (via rush). It unlocks many much needed improvements as we continue to scale the Rhapsody codebase.

See the How to Work with PNPM doc for more information.

All code for creating configurable layouts has been migrated to the @shared/configurable-layouts package. This migration simplifies some internal complexity and lays the groundwork for clearer boundaries between configurable layout code and dashboard panels themselves. @shared/configurable-layouts is written in TypeScript so devs can utilize it seamlessly in TypeScript files.

See the package README for documentation and examples.

All code for interacting with our push notifications (pushex) has been migrated to the @rhapsody/push-notifications package. This allows us to handle all initialization code in one location. @rhapsody/push-notifications is written in TypeScript so devs can utilize it seamlessly in TypeScript files.

See the package README for documentation and examples.

Business logic that's not tied to any specific UI can now use the App Registry to register itself as a "task" that will run immediately after the application boots. The App Registry README has been updated to include a section on how to add tasks to the app registry.

This is another important step toward decoupling the Rhapsody platform from domain-specific code as well as continuing to decrease our reliance on Angular.

Previously, mocking the Auth Context was entirely manual and one had to specify what data to populate the Auth Context with. Now, mocking the Auth Context is as simple as calling jest.mock('@rhapsody/auth-context'). The Auth Context is populated with fixture data that can be overridden at any time by the developer. The snippet below shows how the Auth Context can now be mocked:

import { _setMockData } from '@rhapsody/auth-context'

// BEGIN CODE REQUIRED TO MOCK AUTH CONTEXT
jest.mock('@rhapsody/auth-context')

beforeEach(() => {
// Using this function without any overrides resets the mock data back to its
// default state
_setMockData()
})

Note: the _setMockData export is only available when mocking the auth context. It is not a part of the regular auth context package and does not work unless jest.mock('@rhapsody/auth-context') is invoked.

A full testing example is included in the package README.

Icons in the sidebar menu now use the App Registry. The App Registry README has been updated to include a section on how to add menu icons to the app registry.

This is another important step toward decoupling the Rhapsody platform from domain-specific code as well as continuing to decrease our reliance on Angular.

The @shared/module-allotments package is now available and replaces the ModuleAllotments Angular module. The service exposes multiple functions to get user and team module allotments. The auth context is a huge step toward getting off of Angular as most code today, Angular or not, relies on Devise. All non-Angular code should use the auth context service instead of Devise from now on.

See the package README for documentation and examples.

The auth context service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code referencing the ModuleAllotments module can be left as-is since the module uses this service under the hood.

Icons in the top right corner of the nav shell now use the App Registry. The App Registry README has been updated to include a section on how to add nav icons to the app registry.

This is another important step toward decoupling the Rhapsody platform from domain-specific code as well as continuing to decrease our reliance on Angular.

The @rhapsody/events package is now available and replaces most use-cases of $rootScope in Angular. The service exposes multiple functions to register, publish, and subscribe to events.

See the Event service README for documentation and examples. See the Messenger entry file for an example of registering events and TopNavMessengerIcon.tsx for an example of both publishing and subscribing to events. See task_mode.es6 for an example of using this in Angular.

The events service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code may need to be refactored as well.

As we move off of Angular, Rhapsody's architecture needs to evolve to support the large and ever-growing amount of code that it will need to support. One large aspect of this is inverting how Rhapsody the platform runs and displays domain-specific code (the Dialer, settings pages, nav icons, etc.). The app registry is the mechanism by which domain-specific code will register itself to be run and displayed by the platform.

Be on the lookout for future updates regarding different aspects of the application using the app registry.

This change affects any code using Devise.current_user.team.accounts or Devise.current_team.accounts.

The @shared/legacy-users package is now available. The service exposes multiple functions to get the list of users previously located at Devise.current_user.team.accounts and Devise.current_team.accounts (they were the same list). We asynchronously load this data as soon as Devise loads and populate Devise.current_user.team.accounts and Devise.current_team.accounts. Consequently, when Angular first boots, Devise.current_user.team.accounts and Devise.current_team.accounts are both empty arrays. After the network request is fulfilled, we populate Devise.current_user.team.accounts and Devise.current_team.accounts with the request payload. Practically, this has not had an effect on the system, but it is something to note.

Example​

import { getLegacyUsers, useLegacyUsers } from '@shared/legacy-users'

async function getOwnerName(ownerId: number) {
const legacyUsers = await getLegacyUsers()
return legacyUsers.find((user) => user.id === ownerId)
}

function OwnerName({ ownerId }: { ownerId: number }) {
const { data: legacyUsers, isLoading } = useLegacyUsers()

if (isLoading) return null

const owner = legacyUsers.find((user) => user.id === ownerId)
return <strong>{owner.name}</strong>
}

The legacy user service is written in Typescript and can be used in JavaScript and Typescript code. New code relying on Devise.current_user.team.accounts or Devise.current_team.accounts should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code can be left as-is since Devise uses this service under the hood.

Rhapsody now provides a CLI tool for running codemods against files. npm run codemod will list the codemods available for use as well as usage instructions. The CLI tool runs on files one at a time and makes changes on a best effort basis. Please double-check all changes made by a codemod before committing them.

Along with this change, we have added a rtl-screen codemod. This codemod is for React Testing Library tests and converts queries returned from render calls to use the screen RTL import instead.

ESLint errors are now limited to only the lines that were changed in a PR. Prior to this, changing a file caused ESLint to register errors for any rule violations in the entire file. This was particularly painful when updating a line or two in old files. Now, we filter ESLint errors down to only those related to lines changed. This makes small refactors easier and prevents scope creep when working with existing code.

The @rhapsody/auth-context package is now available and replaces the Devise Angular module. The service exposes multiple functions to get information on the currently authenticated user and their team. The auth context is a huge step toward getting off of Angular as most code today, Angular or not, relies on Devise. All non-Angular code should use the auth context service instead of Devise from now on.

See the AuthContext README for documentation and examples.

The auth context service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code referencing the Devise module can be left as-is.

New ESLint rules have been added to Rhapsody. They are more opinionated, but provide the necessary structure and guardrails for us to write higher quality code by an increasing number of developers. Our ruleset is based off AirBnB's config for Typescript with additions and customizations made to follow SalesLoft best practices.

See .eslint.js for a full list of enabled rules.

Note: We highly encourage developers to use the ESLint integration for their IDE of choice.

All uses of QuestProvider and useQuest should be removed and replaced with the shared package @shared/requests. The shared requests package contains more clients than useQuest did, can be used more easily in vanilla JavaScript/Typescript, and doesn't require a provider to be in the React component tree to access it.

Below is a diff showing how to replace useQuest with @shared/requests:

 import React, { useState, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
-import { useQuest } from '@rhythm/react-quest'
+import { melodyApi } from '@shared/requests'
import { TinyMcePreview } from '../../../../../../../../../../../components/TinyMce/TinyMcePreview'

export const ContentPreview = (props) => {
const { content, editorHeight = '356px' } = props

- const { melodyApi } = useQuest()
const [preview, setPreview] = useState('')
const isMounted = useRef(true)

Once useQuest is no longer used, QuestProvider can be removed from the component tree.

Rhapsody has been upgraded to styled-components v5. This upgrade brings significant performance improvements for styled-components as well as many DX improvements when writing styled-components.

Changes in v5 include:

  • Significantly faster performance mounting and updating component styles
  • New feature: the as prop allows you to swap out the underlying element in a styled-component, e.g. <StyledLink as={ThirdPartyComponent}>Click me</StyledLink>
  • New feature: Transient props allow you to pass props to the styled-component without passing them to the underlying DOM node, e.g. <StyledInput type="text" $size="large" />
  • createGlobalStyle is used to add global styles instead of injectGlobal. injectGlobal has been removed.
  • Use ref when passing a ref to a styled-component instead of the old innerRef prop. innerRef has been removed.
  • If you use a function with the .attrs syntax, the function needs to be at the top level, not nested at each props. See the .attrs docs for more information.
  • The .extend syntax has been removed. For some time, it has been possible to extend styled-components using styled(SomeComponent) and that is now the only way to do so.

See the styled-components documentation for more information on these changes and other styled-component features.

The @rhapsody/permissions package is now available and replaces both calling the Devise.hasPermission/Devise.hasAnyPermission methods and directly accessing devise.current_account.permission_names. The service exposes the hasPermission and hasAnyPermission functions to check a user's assigned permissions.

Example​

import { hasAnyPermission, hasPermission } from '@rhapsody/permissions'

function AdminLink() {
if (hasPermission('permission_a')) {
return <a href="/settings/team">Team Settings</a>
} else if (hasAnyPermission(['permission_b', 'permission_c'])) {
return <a href="/settings/superadmin">Superadmin Settings</a>
} else {
return null
}
}

The permissions service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code referencing Devise.hasPermission/Devise.hasAnyPermission or devise.current_account.permission_names can be left as-is since all of these permission checking mechanisms use the permissions service under the hood.

The @rhapsody/feature-flags package is now available and replaces both calling the Devise.hasFeature method and directly accessing Devise.current_user.features. The service exposes the hasFeatureFlag function to check if a user has a specific feature flag enabled.

Example​

import { hasFeatureFlag } from '@rhapsody/feature-flags'

function greeting() {
if (hasFeatureFlag('excited_greeting')) {
return 'HOWDY!!! πŸ‘‹'
} else {
return 'Hello'
}
}

The feature flag service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code referencing Devise.hasFeature or Devise.current_user.features can be left as-is since both of these feature flag checking mechanisms use the feature flags service under the hood.

The @rhapsody/tokens package is now available and replaces both services/tokenService.js and the User Angular module. The service exposes the getToken function to asynchronously fetch a valid JWT token for the currently authenticated user and cache it for a variable amount of time. The service also exposes a cacheToken function to manually populate the cache with a JWT.

Why cache the token for a variable amount of time?

User JWTs issued from our system expire after 10 minutes. To avoid the thundering herd problem, we cache the token for a randomly distribute time between 5 and 9 minutes. Distributing out when tokens are refetched allows for more stable, less spike-y traffic to the endpoint issuing the tokens.

For example, consider a scenario where all user JWTs for all logged in users are invalidated at once (this has happened before). Every user currently in the application would need their JWT refetched from the token endpoint. This results in a huge spike in traffic to that endpoint, quite probably causing performance degradation as well. If we did not randomly distribute out the token refetching for subsequent tokens (i.e. if we waited the full 10 minutes for everyone), there would be constant huge spikes in traffic to the token endpoint every 10 minutes. Distributing out the refetches allows us to avoid the subsequent traffic spikes.

Example​

import { getToken } from '@rhapsody/tokens'

async function makeAuthenticatedRequest() {
const { token } = await getToken()
const response = await fetch('/endpoint/requiring/token/auth', {
headers: {
Authentication: `Bearer ${token}`,
},
})
return response.status
}

The tokens service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code referencing the User module can be left as-is since the User module now uses the tokens service under the hood.

The @rhapsody/toasts package is now available and replaces the Notify Angular module. The service exposes the errorToast, infoToast, successToast, and warningToast functions to display different toast messages in the application. A generic createToast function is also available for advanced use-cases.

Example​

import { successToast } from '@rhapsody/toasts'

// Title only
successToast('Success')

// Title and subtitle
successToast('Success', 'The operation completed successfully')

// Title, subtitle, and action
successToast({
title: 'Success',
subtitle: 'The operation completed successfully',
actionCallback: () => {},
actionText: 'View',
})

The toast service is written in Typescript and can be used in JavaScript and Typescript code. New code should use this service and any existing non-Angular code should be refactored to use this service as well. Existing Angular code referencing the Notify module can be left as-is since the Notify module now uses the toast service under the hood.

Our use of snapshots in unit tests is deprecated. They should not be used in new tests, and existing uses should be refactored when come across. Snapshots are brittle and do not clearly communicate the test specification for a given component. Use explicit expect() assertions instead.

Update: This deprecation is now enforced via ESLint.

Using Enzyme for unit tests is fully deprecated. It should not be used for any new code, and existing uses should be refactored when come across.

Use React Testing Library instead (@testing-library/react). It encourages testing component output and provides helper functions to interact with our components in tests as our users would in the application.

The two legacy Rhythm packages, @rhythm/common and @rhythm/core, are fully deprecated. Legacy components are no longer maintained and do not conform to the latest SalesLoft design specifications. They should not be used for any new code, and existing instances should be actively refactored. The Legacy Imports spreadsheet lists all current instances of legacy components being used in Rhapsody.

Below is a table of the most commonly used legacy imports and what to use instead:

Legacy Component(s)What to Use Instead
ButtonButton in @rhythm/buttons
CheckboxCheckbox in @rhythm/inputs
Dropdown and Dropdown* componentsMenu and related components in @rhythm/menu
EmptyStateEmptyState in @rhythm/empty-state
LoadingLoading in @rhythm/loading
PlaceholderBarSkeleton in @rhythm/loading
RhythmSvgAll icons and images in @rhythm/svgs
SelectSelect in @rhythm/inputs
TooltipContainerTooltip in @rhythm/tooltips