From 171a7702357ed07dfc678d05bf181a88f6dbac72 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Mon, 21 Jul 2025 10:24:52 -0700 Subject: [PATCH] feat: enable the use of TypeScript in this repo (#604) * feat: enable Typescript in this repo * refactor: rename studio-header files to .ts[x] * chore: fix minor type warnings * chore: add types for frontend-platform * chore: fix type issues * chore: update name of suppressed lint check --- .github/workflows/ci.yml | 2 + .github/workflows/release.yml | 2 + package.json | 3 +- src/desktop-header/DesktopHeader.jsx | 2 +- src/frontend-platform.d.ts | 41 +++++++++++++++++++ src/mobile-header/MobileHeader.jsx | 2 +- .../{BrandNav.test.jsx => BrandNav.test.tsx} | 2 +- .../{BrandNav.jsx => BrandNav.tsx} | 0 ...eLockUp.test.jsx => CourseLockUp.test.tsx} | 5 ++- .../{CourseLockUp.jsx => CourseLockUp.tsx} | 0 ...eaderBody.test.jsx => HeaderBody.test.tsx} | 2 +- .../{HeaderBody.jsx => HeaderBody.tsx} | 1 + .../{MobileHeader.jsx => MobileHeader.tsx} | 1 + ...obileMenu.test.jsx => MobileMenu.test.tsx} | 0 .../{MobileMenu.jsx => MobileMenu.tsx} | 0 ...Menu.test.jsx => NavDropdownMenu.test.tsx} | 0 ...avDropdownMenu.jsx => NavDropdownMenu.tsx} | 0 ...oHeader.test.jsx => StudioHeader.test.tsx} | 8 +++- .../{StudioHeader.jsx => StudioHeader.tsx} | 1 + .../{UserMenu.jsx => UserMenu.tsx} | 0 src/studio-header/{index.js => index.ts} | 0 .../{messages.js => messages.ts} | 0 src/studio-header/{utils.js => utils.ts} | 0 tsconfig.json | 12 ++++++ 24 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/frontend-platform.d.ts rename src/studio-header/{BrandNav.test.jsx => BrandNav.test.tsx} (93%) rename src/studio-header/{BrandNav.jsx => BrandNav.tsx} (100%) rename src/studio-header/{CourseLockUp.test.jsx => CourseLockUp.test.tsx} (88%) rename src/studio-header/{CourseLockUp.jsx => CourseLockUp.tsx} (100%) rename src/studio-header/{HeaderBody.test.jsx => HeaderBody.test.tsx} (98%) rename src/studio-header/{HeaderBody.jsx => HeaderBody.tsx} (99%) rename src/studio-header/{MobileHeader.jsx => MobileHeader.tsx} (95%) rename src/studio-header/{MobileMenu.test.jsx => MobileMenu.test.tsx} (100%) rename src/studio-header/{MobileMenu.jsx => MobileMenu.tsx} (100%) rename src/studio-header/{NavDropdownMenu.test.jsx => NavDropdownMenu.test.tsx} (100%) rename src/studio-header/{NavDropdownMenu.jsx => NavDropdownMenu.tsx} (100%) rename src/studio-header/{StudioHeader.test.jsx => StudioHeader.test.tsx} (95%) rename src/studio-header/{StudioHeader.jsx => StudioHeader.tsx} (96%) rename src/studio-header/{UserMenu.jsx => UserMenu.tsx} (100%) rename src/studio-header/{index.js => index.ts} (100%) rename src/studio-header/{messages.js => messages.ts} (100%) rename src/studio-header/{utils.js => utils.ts} (100%) create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23bb122..4935fdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: run: make validate-no-uncommitted-package-lock-changes - name: Lint run: npm run lint + - name: Type check + run: npm run types - name: Test run: npm run test - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6df0cc..499db8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,8 @@ jobs: run: make validate-no-uncommitted-package-lock-changes - name: Lint run: npm run lint + - name: Type check + run: npm run types - name: Test run: npm run test - name: i18n_extract diff --git a/package.json b/package.json index b15299f..16a0ef3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "snapshot": "fedx-scripts jest --updateSnapshot", "start": "fedx-scripts webpack-dev-server --progress", - "test": "fedx-scripts jest --coverage" + "test": "fedx-scripts jest --coverage", + "types": "tsc --noEmit" }, "files": [ "/dist" diff --git a/src/desktop-header/DesktopHeader.jsx b/src/desktop-header/DesktopHeader.jsx index 4b5e493..9982950 100644 --- a/src/desktop-header/DesktopHeader.jsx +++ b/src/desktop-header/DesktopHeader.jsx @@ -22,7 +22,7 @@ import messages from '../Header.messages'; import { CaretIcon } from '../Icons'; class DesktopHeader extends React.Component { - constructor(props) { // eslint-disable-line no-useless-constructor + constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor super(props); } diff --git a/src/frontend-platform.d.ts b/src/frontend-platform.d.ts new file mode 100644 index 0000000..d9b2f7f --- /dev/null +++ b/src/frontend-platform.d.ts @@ -0,0 +1,41 @@ +// frontend-platform currently doesn't provide types... do it ourselves for i18n module at least. +// We can remove this in the future when we migrate to frontend-shell, or when frontend-platform gets types +// (whichever comes first). + +declare module '@edx/frontend-platform/i18n' { + // eslint-disable-next-line import/no-extraneous-dependencies + import { injectIntl as _injectIntl } from 'react-intl'; + /** @deprecated Use useIntl() hook instead. */ + export const injectIntl: typeof _injectIntl; + /** @deprecated Use useIntl() hook instead. */ + export const intlShape: any; + + // eslint-disable-next-line import/no-extraneous-dependencies + export { + createIntl, + FormattedDate, + FormattedTime, + FormattedRelativeTime, + FormattedNumber, + FormattedPlural, + FormattedMessage, + defineMessages, + IntlProvider, + useIntl, + } from 'react-intl'; + + // Other exports from the i18n module: + export const configure: any; + export const getPrimaryLanguageSubtag: (code: string) => string; + export const getLocale: (locale?: string) => string; + export const getMessages: any; + export const isRtl: (locale?: string) => boolean; + export const handleRtl: any; + export const mergeMessages: any; + export const LOCALE_CHANGED: any; + export const LOCALE_TOPIC: any; + export const getCountryList: any; + export const getCountryMessages: any; + export const getLanguageList: any; + export const getLanguageMessages: any; +} diff --git a/src/mobile-header/MobileHeader.jsx b/src/mobile-header/MobileHeader.jsx index 7a04ec7..70d808e 100644 --- a/src/mobile-header/MobileHeader.jsx +++ b/src/mobile-header/MobileHeader.jsx @@ -21,7 +21,7 @@ import messages from '../Header.messages'; import { MenuIcon } from '../Icons'; class MobileHeader extends React.Component { - constructor(props) { // eslint-disable-line no-useless-constructor + constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor super(props); } diff --git a/src/studio-header/BrandNav.test.jsx b/src/studio-header/BrandNav.test.tsx similarity index 93% rename from src/studio-header/BrandNav.test.jsx rename to src/studio-header/BrandNav.test.tsx index 7ea2d3e..5c9be68 100644 --- a/src/studio-header/BrandNav.test.jsx +++ b/src/studio-header/BrandNav.test.tsx @@ -34,7 +34,7 @@ describe('BrandNav Component', () => { it('displays a link that navigates to studioBaseUrl', () => { render(); - const link = screen.getByRole('link'); + const link = screen.getByRole('link') as HTMLAnchorElement; expect(link.href).toBe(studioBaseUrl); }); }); diff --git a/src/studio-header/BrandNav.jsx b/src/studio-header/BrandNav.tsx similarity index 100% rename from src/studio-header/BrandNav.jsx rename to src/studio-header/BrandNav.tsx diff --git a/src/studio-header/CourseLockUp.test.jsx b/src/studio-header/CourseLockUp.test.tsx similarity index 88% rename from src/studio-header/CourseLockUp.test.jsx rename to src/studio-header/CourseLockUp.test.tsx index 5dfc48f..d5a48d6 100644 --- a/src/studio-header/CourseLockUp.test.jsx +++ b/src/studio-header/CourseLockUp.test.tsx @@ -16,7 +16,7 @@ const mockProps = { const RootWrapper = (props) => ( - + @@ -52,7 +52,8 @@ describe('CourseLockUp Component', () => { it('navigates to an absolute URL when clicked', () => { render(); - const link = screen.getByTestId('course-lock-up-block'); + // FIXME: don't use testId - https://testing-library.com/docs/queries/about#priority + const link = screen.getByTestId('course-lock-up-block') as HTMLAnchorElement; expect(link.href).toBe(mockProps.outlineLink); }); }); diff --git a/src/studio-header/CourseLockUp.jsx b/src/studio-header/CourseLockUp.tsx similarity index 100% rename from src/studio-header/CourseLockUp.jsx rename to src/studio-header/CourseLockUp.tsx diff --git a/src/studio-header/HeaderBody.test.jsx b/src/studio-header/HeaderBody.test.tsx similarity index 98% rename from src/studio-header/HeaderBody.test.jsx rename to src/studio-header/HeaderBody.test.tsx index 6e5a5e6..3223f60 100644 --- a/src/studio-header/HeaderBody.test.jsx +++ b/src/studio-header/HeaderBody.test.tsx @@ -35,7 +35,7 @@ const defaultProps = { const RootWrapper = (props) => ( - + diff --git a/src/studio-header/HeaderBody.jsx b/src/studio-header/HeaderBody.tsx similarity index 99% rename from src/studio-header/HeaderBody.jsx rename to src/studio-header/HeaderBody.tsx index 3ed7403..598aec3 100644 --- a/src/studio-header/HeaderBody.jsx +++ b/src/studio-header/HeaderBody.tsx @@ -135,6 +135,7 @@ const HeaderBody = ({ logoutUrl, authenticatedUserAvatar, isAdmin, + isMobile, }} /> diff --git a/src/studio-header/MobileHeader.jsx b/src/studio-header/MobileHeader.tsx similarity index 95% rename from src/studio-header/MobileHeader.jsx rename to src/studio-header/MobileHeader.tsx index 44d6d21..bec1d8b 100644 --- a/src/studio-header/MobileHeader.jsx +++ b/src/studio-header/MobileHeader.tsx @@ -13,6 +13,7 @@ const MobileHeader = ({ return ( <> + {/* @ts-expect-error The type of 'props' is any until we convert from propTypes to TypeScript interface/types */} { +}: React.ComponentProps) => { const appContextValue = useMemo(() => ({ authenticatedUser: currentUser, config: { @@ -55,7 +55,7 @@ const RootWrapper = ({ ); }; -const props = { +const props: React.ComponentProps = { number: '123', org: 'Ed', title: 'test', @@ -74,6 +74,10 @@ const props = { outlineLink: 'tEsTLInK', searchButtonAction: null, isNewHomePage: true, + // These default values shouldn't be needed but typescript is confused by propTypes; can remove after converting + // from propTypes to TypeScript: + containerProps: {}, + isHiddenMainMenu: false, }; describe('Header', () => { diff --git a/src/studio-header/StudioHeader.jsx b/src/studio-header/StudioHeader.tsx similarity index 96% rename from src/studio-header/StudioHeader.jsx rename to src/studio-header/StudioHeader.tsx index 6ad0823..735cfa5 100644 --- a/src/studio-header/StudioHeader.jsx +++ b/src/studio-header/StudioHeader.tsx @@ -19,6 +19,7 @@ const StudioHeader = ({ number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns, outlineLink, searchButtonAction, isNewHomePage, }) => { + // @ts-expect-error - frontend-platform doesn't yet have type information :/ const { authenticatedUser, config } = useContext(AppContext); const props = { logo: config.LOGO_URL, diff --git a/src/studio-header/UserMenu.jsx b/src/studio-header/UserMenu.tsx similarity index 100% rename from src/studio-header/UserMenu.jsx rename to src/studio-header/UserMenu.tsx diff --git a/src/studio-header/index.js b/src/studio-header/index.ts similarity index 100% rename from src/studio-header/index.js rename to src/studio-header/index.ts diff --git a/src/studio-header/messages.js b/src/studio-header/messages.ts similarity index 100% rename from src/studio-header/messages.js rename to src/studio-header/messages.ts diff --git a/src/studio-header/utils.js b/src/studio-header/utils.ts similarity index 100% rename from src/studio-header/utils.js rename to src/studio-header/utils.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d01a9c8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@edx/typescript-config", + "compilerOptions": { + "noEmit": true, + "baseUrl": "./src", + "paths": { + "*": ["*"] + } + }, + "include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"], + "exclude": ["dist", "node_modules"] +}