diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index d8146bc197..3349294cd3 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -66,6 +66,56 @@ jobs: name: ${{env.BUILD_LOGS}} path: ${{env.BUILD_LOGS}}.tar.br + React19: + # Forward-compatibility check: install React 19 + types into the cached + # node_modules (no save) and run typecheck + jest. Allowed to fail while + # the migration lands; flip to required once Repo 1 (backpack-web) is + # released and consumers can opt in. + runs-on: ubuntu-latest + needs: Build + continue-on-error: true + permissions: + statuses: write + pull-requests: write + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + + - name: Restore Cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: npm-cache + with: + path: | + node_modules/ + packages/node_modules/ + key: ${{ env.CACHE_NAME }}-${{ hashFiles('package-lock.json', 'packages/package-lock.json') }} + + - name: Override React to 19.2.5 + # The inner packages/node_modules tree has its own copy of react/react-dom + # populated from the cache. Overlay the root install into it so jest + # (which resolves react-dom from packages/) sees R19 too. + run: | + npm install --no-save react@19.2.5 react-dom@19.2.5 @types/react@19 @types/react-dom@19 @types/prop-types + rm -rf packages/node_modules/react packages/node_modules/react-dom + cp -r node_modules/react packages/node_modules/ + cp -r node_modules/react-dom packages/node_modules/ + + - name: Generate component code (icons, flare, spinners) + run: npm run build:gulp + + - name: Run typecheck (React 19) + run: npm run typecheck + + - name: Run jest (React 19) + run: TZ=Etc/UTC npm run jest + Danger: runs-on: ubuntu-latest needs: Build diff --git a/.storybook/bpk-storybook-utils/src/BpkDarkExampleWrapper.js b/.storybook/bpk-storybook-utils/src/BpkDarkExampleWrapper.js index c44d98ddf0..1e8e3eff17 100644 --- a/.storybook/bpk-storybook-utils/src/BpkDarkExampleWrapper.js +++ b/.storybook/bpk-storybook-utils/src/BpkDarkExampleWrapper.js @@ -24,7 +24,7 @@ import STYLES from './BpkDarkExampleWrapper.module.scss'; const getClassName = cssModules(STYLES); const BpkDarkExampleWrapper = (props: { padded: boolean }) => { - const { padded, ...rest } = props; + const { padded = false, ...rest } = props; return ( /* $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'. */
{ ); }; -BpkDarkExampleWrapper.defaultProps = { - padded: false, -}; - export default BpkDarkExampleWrapper; diff --git a/REACT_19_MIGRATION.md b/REACT_19_MIGRATION.md new file mode 100644 index 0000000000..956e2aff1e --- /dev/null +++ b/REACT_19_MIGRATION.md @@ -0,0 +1,73 @@ +# React 19 Migration — `@skyscanner/backpack-web` + +(This repo is **Repo 1 of 5** in the Skyscanner shared-library React 19 pre-release codemod pass.) + +## What this thread should do + +1. Read the master plan's "Repo 1" section. It contains the full audit baseline, recipe checklist, and empty result fields to fill in. +2. Execute the recipe inside this directory. +3. As you progress, update the master plan's Repo 1 section with results (codemod diff summary, test outcomes, surprises). +4. When done, append a one-paragraph "Surprises / runbook insights" entry, and open follow-up PRs against `web-documentation` and `web-migration-scripts` if any reusable insights came out. + +## Audit baseline (2026-05-05) — corrected after re-audit on 2026-05-05 + +The original baseline overstated several categories. Re-audit confirmed: + +- Lerna-style monorepo, published from `packages/package.json` +- React peerDep: `17.0.2 - 18.3.1` → narrowed-and-shifted to `18.3.1 - 19.2.5` (We shouldn't have any consumers below 18) +- `static defaultProps` / `defaultProps =`: **84 files** (manual sweep, no codemod) +- `forwardRef`: 14 files (none combined with `defaultProps`) +- `prop-types` imports: **67 files** (mostly `.js`; manual conversion — see runbook insight below) +- `act` from `react-dom/test-utils`: **0** (audit said 7; re-grep finds none) +- `ReactDOM.render` / `hydrate`: **0** (audit said 1; the only hit is example text inside a `` JSX literal in `BpkCode.stories.tsx`, not a real call) +- String refs (`ref="..."`): **0** +- `findDOMNode`: **0** +- Legacy context API (`childContextTypes` / `getChildContext`): **0** +- `useFormState`: **0** +- Zero-arg `useRef()`: 0 +- TS 5.9.2; `@types/react` 18.3.1 +- Tests: Jest + `@testing-library/react` 16.3.0 +- CI: `.github/workflows/{release,pr,main}.yml` — `npm publish` after transpile + +### Runbook insight: `react/19/migration-recipe` is destructive on this codebase + +Dry-running `react/19/migration-recipe` (codemod 1.9.1, which silently invokes legacy codemod 0.18.13) on backpack: + +- **Strips Apache 2.0 license headers** from every modified file. Backpack requires these. +- Generates **wrong TypeScript interfaces** when files already have proper TS types (e.g. `BpkBasicMapMarker.tsx` keeps the existing `type Props = { children: ReactNode, position: LatLong }` orphaned and points the component at a new redundant `interface { ...; position: unknown }`). +- Only matched **6 of 67** prop-types files in this repo despite the recipe including `prop-types-typescript`. + +We are skipping the bundled recipe. The four mechanical sub-codemods (`replace-act-import`, `replace-reactdom-render`, `replace-string-ref`, `replace-use-form-state`) all dry-run as 0-changes here, so they are also skipped. Only `types-react-codemod` (TS-types only, much narrower scope) is being run. + +The `prop-types` and `defaultProps` migrations are being done with a custom jscodeshift transform tailored to backpack, preserving license headers and faithful types. + +### Runbook insight: peerDep range narrowed instead of widened + +Our intention is to **drop React 17 support** as part of this PR, to `18.3.1 - 19.2.5`. React 17 is unsupported upstream, no consumer is still on it, and dropping it shrinks the test/CI matrix surface. + +## Recipe summary — adapted for this repo + +1. [x] Pin `codemod` + `types-react-codemod` as devDeps with `--save-exact` (no `npx`/`pnpm dlx` — Skyscanner Security stance). +2. [x] ~~`react/19/migration-recipe`~~ — **skipped** (destructive on this codebase; see runbook insight above). +3. [x] ~~Individual mechanical codemods~~ — **skipped** (dry-runs as 0-changes for all four). +4. [x] `types-react-codemod preset-19` — applied with `refobject-defaults` and `useRef-required-initial` excluded (those two only typecheck against `@types/react@19`; deferred to the future `@types/react` bump PR). +5. [x] `types-react-codemod react-element-default-any-props` — confirmed no-op (already covered by preset-19). +6. [x] Custom jscodeshift transform applied (`scripts/react-19/transforms/strip-proptypes.js`, see scripts/react-19/README.md). Touched 27 files: removed prop-types from `.tsx`, migrated function-component `.defaultProps` to ES6 destructure defaults across `.js` and `.tsx`. License headers preserved. +7. [x] Manual cleanup of edge cases the transform left (1 eslint-disable + 2 dead-import removals). +8. [x] `forwardRef` ref-callback implicit-return scan — 0 issues, all 14 sites use block-body callbacks. +9. [x] Update `peerDependencies` in `packages/package.json` to `18.3.1 - 19.2.5` (drops React 17, adds 19; range syntax matching the existing peerDep style). +10. [x] Add CI matrix entry running tests against React 19.2.5 (`continue-on-error: true` initially). +11. [x] Uninstall codemod packages. +12. [ ] Coordinate version bump with the Backpack team. + +## Deferred to follow-up PRs + +The current PR establishes the scaffolding (peerDep range, CI matrix, codemod tooling, `.tsx` prop-types/defaultProps cleanup). The following items land in separate PRs: + +- **35 class components with `static defaultProps`** (27 `.tsx` + 8 `.js`) — React 19 makes these no-ops, so defaults silently stop applying. Each needs either conversion to a functional component (preferred) or destructure-with-defaults inside `render()` (quick fix). See the React19 CI matrix output for the failure surface; group by package to keep PRs reviewable. +- **13 `.js` story/HOC files with leftover `Component.defaultProps = ...`** — Phase B of the transform couldn't merge defaults because the function bodies are implicit-return arrows or otherwise ineligible. React 19 silently ignores these. +- **`.js` (Flow) prop-types removal** — intentionally skipped by the transform because the project's `react/prop-types` lint rule treats removed prop-types as missing prop validation on `.js` files. Full removal happens during the parallel TS migration; React 19 ignores `propTypes` silently in the meantime. +- **`@types/react@19` bump** — running `types-react-codemod preset-19`'s `refobject-defaults` and `useRef-required-initial` sub-transforms together with bumping `@types/react` to 19. These were skipped here because they emit code that only typechecks against `@types/react@19`. +- **Track and fix the React19 CI matrix failures** — typecheck has 8 known errors (the deferred sub-transforms above plus a missing `@types/prop-types`), and jest has 326 suite failures (mostly transitive deps still using removed React 18 internals like `ReactCurrentDispatcher`). Once green, flip the matrix from `continue-on-error: true` to required. +- **Move the custom transform to `web-migration-scripts/migrations/2026-05-react-19/transforms/`** when that migration directory is set up. + diff --git a/package-lock.json b/package-lock.json index 80e0f02a49..6f9494fe08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -501,9 +501,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -880,11 +880,13 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.22.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1391,12 +1393,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.22.5", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-flow": "^7.22.5" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2169,13 +2173,15 @@ } }, "node_modules/@babel/preset-flow": { - "version": "7.22.15", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.27.1.tgz", + "integrity": "sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-flow-strip-types": "^7.22.5" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-flow-strip-types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -23917,10 +23923,11 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } diff --git a/package.json b/package.json index e8b0695d8a..95af867f3e 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,9 @@ "setupFilesAfterEnv": [ "/scripts/jest/setup.js" ], + "snapshotSerializers": [ + "/scripts/jest/normalizeUseIdSerializer.js" + ], "testEnvironment": "jsdom", "testRegex": "(?:packages|token-sync)/.*-test\\.[jt]sx?$", "transformIgnorePatterns": [ diff --git a/packages/bpk-component-accordion/src/BpkAccordionItem.tsx b/packages/bpk-component-accordion/src/BpkAccordionItem.tsx index 166d9ae657..74b959ffed 100644 --- a/packages/bpk-component-accordion/src/BpkAccordionItem.tsx +++ b/packages/bpk-component-accordion/src/BpkAccordionItem.tsx @@ -40,7 +40,7 @@ export type BpkAccordionItemProps = { className?: string; expanded?: boolean; initiallyExpanded?: boolean; - icon?: ReactElement; + icon?: ReactElement; onClick?: () => void; tagName?: 'span' | 'p' | 'text' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; textStyle?: (typeof TEXT_STYLES)[keyof typeof TEXT_STYLES]; diff --git a/packages/bpk-component-accordion/src/withSingleItemAccordionState.tsx b/packages/bpk-component-accordion/src/withSingleItemAccordionState.tsx index c89ffa3808..66189e05e4 100644 --- a/packages/bpk-component-accordion/src/withSingleItemAccordionState.tsx +++ b/packages/bpk-component-accordion/src/withSingleItemAccordionState.tsx @@ -24,10 +24,10 @@ import { wrapDisplayName } from '../../bpk-react-utils'; import type { BpkAccordionProps } from './BpkAccordion'; const getInitiallyExpanded = (children: ReactNode) => { - const accordionItems = Children.toArray(children) as ReactElement[]; + const accordionItems = Children.toArray(children) as Array>; const result = accordionItems.reduceRight( (prev, item) => (item.props.initiallyExpanded ? item : prev), - {} as ReactElement, + {} as ReactElement, ); return (result || {}).key || null; }; @@ -61,7 +61,7 @@ const withSingleItemAccordionState =

( this.setState({ expanded: key }); }; - renderAccordionItem = (accordionItem: ReactElement) => { + renderAccordionItem = (accordionItem: ReactElement) => { const expanded = this.state.expanded === accordionItem.key; const onClick = () => this.openAccordionItem(accordionItem?.key); @@ -74,7 +74,7 @@ const withSingleItemAccordionState =

( return ( {Children.toArray(children).map((el) => - this.renderAccordionItem(el as ReactElement), + this.renderAccordionItem(el as ReactElement), )} ); diff --git a/packages/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx b/packages/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx index f2b50100c7..6923e2e200 100644 --- a/packages/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx +++ b/packages/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx @@ -25,7 +25,7 @@ import { BpkProvider } from '../../bpk-component-layout'; import BpkAiBlurb from './BpkAiBlurb'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkAiBlurb.Root', () => { diff --git a/packages/bpk-component-ai-blurb/src/accessibility-test.tsx b/packages/bpk-component-ai-blurb/src/accessibility-test.tsx index 4f3c6373ef..9245677b02 100644 --- a/packages/bpk-component-ai-blurb/src/accessibility-test.tsx +++ b/packages/bpk-component-ai-blurb/src/accessibility-test.tsx @@ -26,7 +26,7 @@ import { BpkProvider } from '../../bpk-component-layout'; import BpkAiBlurb from './BpkAiBlurb'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkAiBlurb accessibility tests', () => { diff --git a/packages/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx b/packages/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx index 3ce2cd1f02..0c422fc071 100644 --- a/packages/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx +++ b/packages/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx @@ -28,8 +28,8 @@ import STYLES from './BpkAriaLive.stories.module.scss'; const getClassName = cssModules(STYLES); type AriaLiveDemoProps = { - preamble?: ReactElement | null; - children: ReactElement; + preamble?: ReactElement | null; + children: ReactElement; className?: string | null; style?: {}; visible?: Boolean; diff --git a/packages/bpk-component-aria-live/src/BpkAriaLive.tsx b/packages/bpk-component-aria-live/src/BpkAriaLive.tsx index fd8d57f8c0..3d38217bdb 100644 --- a/packages/bpk-component-aria-live/src/BpkAriaLive.tsx +++ b/packages/bpk-component-aria-live/src/BpkAriaLive.tsx @@ -34,7 +34,7 @@ export type PolitenessSetting = (typeof POLITENESS_SETTINGS)[keyof typeof POLITENESS_SETTINGS]; export type Props = { - children: ReactElement | string; + children: ReactElement | string; politenessSetting?: PolitenessSetting; visible?: boolean; className?: string | null; diff --git a/packages/bpk-component-autosuggest/src/BpkAutosuggestSuggestion.js b/packages/bpk-component-autosuggest/src/BpkAutosuggestSuggestion.js index 6eccc8f130..d84ea169f2 100644 --- a/packages/bpk-component-autosuggest/src/BpkAutosuggestSuggestion.js +++ b/packages/bpk-component-autosuggest/src/BpkAutosuggestSuggestion.js @@ -38,7 +38,7 @@ type Props = { const BpkAutosuggestSuggestion = (props: Props) => { const classNames = [getClassName('bpk-autosuggest__suggestion')]; - const { className, icon, indent, subHeading, tertiaryLabel, value, ...rest } = + const { className = null, icon = null, indent = false, subHeading = null, tertiaryLabel = null, value, ...rest } = props; const Icon = icon; @@ -98,12 +98,4 @@ BpkAutosuggestSuggestion.propTypes = { className: PropTypes.string, }; -BpkAutosuggestSuggestion.defaultProps = { - subHeading: null, - tertiaryLabel: null, - icon: null, - indent: false, - className: null, -}; - export default BpkAutosuggestSuggestion; diff --git a/packages/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx b/packages/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx index 312fcb6152..06a69f0aab 100644 --- a/packages/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx +++ b/packages/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx @@ -17,7 +17,7 @@ */ import { Component } from 'react'; -import type { ReactElement, InputHTMLAttributes, LegacyRef } from 'react'; +import type { ReactElement, InputHTMLAttributes, Ref } from 'react'; import { userEvent, within } from 'storybook/test'; @@ -179,10 +179,10 @@ type Props = { multiSection: boolean; renderInputComponent?: ( inputProps: InputHTMLAttributes & { - ref?: LegacyRef; + ref?: Ref; }, - ) => ReactElement; - renderSectionTitle: (section: Section) => ReactElement | null; + ) => ReactElement; + renderSectionTitle: (section: Section) => ReactElement | null; getSectionSuggestions: (section: Section) => Suggestion[]; }; @@ -322,7 +322,7 @@ export const HighlightFistSuggestion: Story = { // --- Multi-section example --- -const renderSectionTitle = (section: { title: string }): ReactElement => ( +const renderSectionTitle = (section: { title: string }): ReactElement => (

{section.title}
); @@ -375,7 +375,7 @@ export const SmallInput: Story = { const renderCustomInput = ( inputProps: InputHTMLAttributes & { - ref?: LegacyRef; + ref?: Ref; }, ) => (
= { }) => void; onSuggestionsFetchRequested: (value: string) => void; onSuggestionsClearRequested: () => void; - renderSuggestion: (suggestion: T) => ReactElement; + renderSuggestion: (suggestion: T) => ReactElement; id: string; enterKeyHint?: EnterKeyHintType; getA11yResultsMessage: (resultCount: number) => string; @@ -113,17 +113,17 @@ export type BpkAutoSuggestProps = { isDesktop?: boolean; onLoad?: (inputValue: string) => void; onClick?: () => void; - renderBesideInput?: () => ReactElement; + renderBesideInput?: () => ReactElement; showClear?: boolean; theme?: Partial; highlightFirstSuggestion?: boolean; shouldRenderSuggestions?: (value?: string) => boolean; multiSection?: boolean; getSectionSuggestions?: (section: T) => T[]; - renderSectionTitle?: (section: T) => ReactElement | null; + renderSectionTitle?: (section: T) => ReactElement | null; alwaysRenderSuggestions?: boolean; onInputValueChange?: (input: { method: string; newValue: string }) => void; - renderInputComponent?: (inputProps: BpkInputRenderProps) => ReactElement; + renderInputComponent?: (inputProps: BpkInputRenderProps) => ReactElement; onSuggestionHighlighted?: (data: { suggestion: T | null }) => void; focusInputOnSuggestionClick?: boolean; }; diff --git a/packages/bpk-component-barchart/src/BpkBarchartBar.js b/packages/bpk-component-barchart/src/BpkBarchartBar.js index 8116dcd8aa..306fcdd02a 100644 --- a/packages/bpk-component-barchart/src/BpkBarchartBar.js +++ b/packages/bpk-component-barchart/src/BpkBarchartBar.js @@ -61,15 +61,15 @@ type Props = { const BpkBarchartBar = (props: Props) => { const { - className, + className = null, height, label, - onClick, - onFocus, - onHover, - outlier, - padding, - selected, + onClick = null, + onFocus = null, + onHover = null, + outlier = false, + padding = 0, + selected = false, width, x, y, @@ -142,14 +142,4 @@ BpkBarchartBar.propTypes = { selected: PropTypes.bool, }; -BpkBarchartBar.defaultProps = { - className: null, - onClick: null, - onHover: null, - onFocus: null, - outlier: false, - padding: 0, - selected: false, -}; - export default BpkBarchartBar; diff --git a/packages/bpk-component-barchart/src/BpkBarchartBars.js b/packages/bpk-component-barchart/src/BpkBarchartBars.js index f3bc70372f..6c64f59113 100644 --- a/packages/bpk-component-barchart/src/BpkBarchartBars.js +++ b/packages/bpk-component-barchart/src/BpkBarchartBars.js @@ -76,15 +76,15 @@ const BpkBarchartBars = (props: Props) => { BarComponent, data, getBarLabel, - getBarSelection, + getBarSelection = () => false, height, - innerPadding, + innerPadding = 0.35, margin, maxYValue, - onBarClick, - onBarFocus, - onBarHover, - outerPadding, + onBarClick = null, + onBarFocus = null, + onBarHover = null, + outerPadding = 0.35, xScale, xScaleDataKey, yScale, @@ -154,13 +154,4 @@ BpkBarchartBars.propTypes = { onBarFocus: PropTypes.func, }; -BpkBarchartBars.defaultProps = { - outerPadding: 0.35, - innerPadding: 0.35, - onBarClick: null, - onBarHover: null, - onBarFocus: null, - getBarSelection: () => false, -}; - export default BpkBarchartBars; diff --git a/packages/bpk-component-barchart/src/BpkChartAxis.js b/packages/bpk-component-barchart/src/BpkChartAxis.js index 904dccd3fe..d39a836ad0 100644 --- a/packages/bpk-component-barchart/src/BpkChartAxis.js +++ b/packages/bpk-component-barchart/src/BpkChartAxis.js @@ -104,14 +104,14 @@ type Props = { const BpkChartAxis = (props: Props) => { const { height, - label, + label = null, margin, - numTicks, + numTicks = null, orientation, scale, - tickEvery, - tickOffset, - tickValue, + tickEvery = 1, + tickOffset = 0, + tickValue = identity, width, ...rest } = props; @@ -176,12 +176,4 @@ BpkChartAxis.propTypes = { tickEvery: PropTypes.number, }; -BpkChartAxis.defaultProps = { - tickOffset: 0, - tickEvery: 1, - tickValue: identity, - numTicks: null, - label: null, -}; - export default BpkChartAxis; diff --git a/packages/bpk-component-barchart/src/BpkChartDataTable.js b/packages/bpk-component-barchart/src/BpkChartDataTable.js index 4cc9a7bdab..0322397720 100644 --- a/packages/bpk-component-barchart/src/BpkChartDataTable.js +++ b/packages/bpk-component-barchart/src/BpkChartDataTable.js @@ -37,7 +37,7 @@ type Props = { }; const BpkChartDataTable = (props: Props) => { - const { data, xAxisLabel, xScaleDataKey, yAxisLabel, yScaleDataKey } = props; + const { data = null, xAxisLabel, xScaleDataKey, yAxisLabel, yScaleDataKey } = props; const rows = data.map((point, i) => { const key = `chart-data-table-row-${i}`; @@ -70,8 +70,4 @@ BpkChartDataTable.propTypes = { yAxisLabel: PropTypes.string.isRequired, }; -BpkChartDataTable.defaultProps = { - data: null, -}; - export default BpkChartDataTable; diff --git a/packages/bpk-component-barchart/src/BpkChartGridLines.js b/packages/bpk-component-barchart/src/BpkChartGridLines.js index 056a859b18..3092367536 100644 --- a/packages/bpk-component-barchart/src/BpkChartGridLines.js +++ b/packages/bpk-component-barchart/src/BpkChartGridLines.js @@ -49,11 +49,11 @@ const BpkChartGridLines = (props: Props) => { const { height, margin, - numTicks, + numTicks = null, orientation, scale, - tickEvery, - tickOffset, + tickEvery = 1, + tickOffset = 0, width, ...rest } = props; @@ -80,12 +80,12 @@ const BpkChartGridLines = (props: Props) => { const toLine = (tick, i) => ( // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'. - + />) ); return ( @@ -110,10 +110,4 @@ BpkChartGridLines.propTypes = { tickEvery: PropTypes.number, }; -BpkChartGridLines.defaultProps = { - numTicks: null, - tickOffset: 0, - tickEvery: 1, -}; - export default BpkChartGridLines; diff --git a/packages/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx b/packages/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx index ef5fff6ae4..44395f4da9 100644 --- a/packages/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx +++ b/packages/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx @@ -169,7 +169,7 @@ const BpkBottomSheet = ({ // For custom title (ReactNode), wrap it with an element that has the correct id // so BpkNavigationBar's aria-labelledby reference is valid const titleWithId = hasTitle && typeof title !== 'string' && isValidElement(title) - ? cloneElement(title as ReactElement, { id: showHiddenTitle ? hiddenTitleId : headingId }) + ? cloneElement(title as ReactElement, { id: showHiddenTitle ? hiddenTitleId : headingId }) : title; return ( diff --git a/packages/bpk-component-breakpoint/src/BpkBreakpoint.tsx b/packages/bpk-component-breakpoint/src/BpkBreakpoint.tsx index 01bc0a52fe..af359b38c6 100644 --- a/packages/bpk-component-breakpoint/src/BpkBreakpoint.tsx +++ b/packages/bpk-component-breakpoint/src/BpkBreakpoint.tsx @@ -77,15 +77,15 @@ const BpkBreakpoint = ({ useLegacyWarning(query, legacy, isClient); if (isClient) { if (typeof children === 'function') { - return children(matches) as ReactElement; + return children(matches) as ReactElement; } - return matches ? (children as ReactElement) : null; + return matches ? (children as ReactElement) : null; } if (typeof children === 'function') { - return children(!!matchSSR) as ReactElement; + return children(!!matchSSR) as ReactElement; } - return matchSSR ? (children as ReactElement) : null; + return matchSSR ? (children as ReactElement) : null; }; export { BREAKPOINTS }; export default BpkBreakpoint; diff --git a/packages/bpk-component-calendar/src/BpkCalendarGrid-test.tsx b/packages/bpk-component-calendar/src/BpkCalendarGrid-test.tsx index f79649316d..e55b8ec885 100644 --- a/packages/bpk-component-calendar/src/BpkCalendarGrid-test.tsx +++ b/packages/bpk-component-calendar/src/BpkCalendarGrid-test.tsx @@ -16,8 +16,6 @@ * limitations under the License. */ -import PropTypes from 'prop-types'; - import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { addMonths, isWeekend, format } from 'date-fns'; @@ -83,9 +81,6 @@ describe('BpkCalendarGrid', () => { } return
; }; - MyCustomDate.propTypes = { - date: PropTypes.instanceOf(Date).isRequired, - }; const { asFragment } = render( { } type DateContainerProps = { - children: ReactElement; + children: ReactElement; className?: string | null; isEmptyCell: boolean; selectionType: string; diff --git a/packages/bpk-component-calendar/src/custom-proptypes.ts b/packages/bpk-component-calendar/src/custom-proptypes.ts index 01ae85d652..ab5ab79c39 100644 --- a/packages/bpk-component-calendar/src/custom-proptypes.ts +++ b/packages/bpk-component-calendar/src/custom-proptypes.ts @@ -49,4 +49,4 @@ export type WeekDay = { export type WeekDayKey = string; export type DaysOfWeek = WeekDay[]; export type DateModifiers = { [key: string]: Function }; -export type ReactComponent = string | ((props: any) => ReactElement); +export type ReactComponent = string | ((props: any) => ReactElement); diff --git a/packages/bpk-component-card-list/src/common-types.ts b/packages/bpk-component-card-list/src/common-types.ts index 09aad37575..1a21825d75 100644 --- a/packages/bpk-component-card-list/src/common-types.ts +++ b/packages/bpk-component-card-list/src/common-types.ts @@ -40,7 +40,7 @@ const ACCESSORY_MOBILE_TYPES = { } as const; type ExpandProps = { - children: string | ReactElement; + children: string | ReactElement; collapsed: boolean; onExpandToggle: () => void; }; @@ -54,7 +54,7 @@ type AccessibilityLabels = { }; type CardListBaseProps = { - cardList: ReactElement[]; + cardList: Array>; layoutMobile: LayoutMobile; layoutDesktop: LayoutDesktop; accessoryDesktop?: (typeof ACCESSORY_DESKTOP_TYPES)[keyof typeof ACCESSORY_DESKTOP_TYPES]; @@ -62,7 +62,7 @@ type CardListBaseProps = { initiallyShownCardsDesktop?: number; initiallyShownCardsMobile?: number; initiallyInViewCardIndex?: number; - chipGroup?: ReactElement; + chipGroup?: ReactElement; buttonContent?: React.ReactNode; onButtonClick?: () => void; onExpandClick?: () => void; @@ -80,7 +80,7 @@ type TitleProps = { } type CardListGridStackProps = { - children: ReactElement[]; + children: Array>; initiallyShownCards: number; layout: typeof LAYOUTS.grid | typeof LAYOUTS.stack; accessory?: diff --git a/packages/bpk-component-card-list/testMocks.tsx b/packages/bpk-component-card-list/testMocks.tsx index c336b05e78..84381c327e 100644 --- a/packages/bpk-component-card-list/testMocks.tsx +++ b/packages/bpk-component-card-list/testMocks.tsx @@ -16,6 +16,8 @@ * limitations under the License. */ +import type { JSX } from 'react'; + import BpkCard from '../bpk-component-card'; const mockCards = (numberOfCards: number): JSX.Element[] => diff --git a/packages/bpk-component-card/src/BpkCardV2/BpkCardV2-test.tsx b/packages/bpk-component-card/src/BpkCardV2/BpkCardV2-test.tsx index c33f26a582..b154fb5a5d 100644 --- a/packages/bpk-component-card/src/BpkCardV2/BpkCardV2-test.tsx +++ b/packages/bpk-component-card/src/BpkCardV2/BpkCardV2-test.tsx @@ -28,7 +28,7 @@ import { CARD_V2_SURFACE_COLORS, CARD_V2_VARIANTS } from './common-types'; const toKebab = (s: string) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkCardV2', () => { diff --git a/packages/bpk-component-card/src/BpkCardV2/accessibility-test.tsx b/packages/bpk-component-card/src/BpkCardV2/accessibility-test.tsx index 9c7d71fa48..c66ed72165 100644 --- a/packages/bpk-component-card/src/BpkCardV2/accessibility-test.tsx +++ b/packages/bpk-component-card/src/BpkCardV2/accessibility-test.tsx @@ -28,7 +28,7 @@ import { CARD_V2_SURFACE_COLORS, CARD_V2_VARIANTS } from './common-types'; expect.extend(toHaveNoViolations); -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkCardV2 Accessibility', () => { diff --git a/packages/bpk-component-card/src/BpkCardV2/integration-test.tsx b/packages/bpk-component-card/src/BpkCardV2/integration-test.tsx index 8dab0e17c2..637dd6cdbc 100644 --- a/packages/bpk-component-card/src/BpkCardV2/integration-test.tsx +++ b/packages/bpk-component-card/src/BpkCardV2/integration-test.tsx @@ -28,7 +28,7 @@ import { CARD_V2_SURFACE_COLORS } from './common-types'; const toKebab = (s: string) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkCardV2 Integration Tests', () => { diff --git a/packages/bpk-component-card/src/BpkCardV2/snapshot-test.tsx b/packages/bpk-component-card/src/BpkCardV2/snapshot-test.tsx index 69baec86a9..2b08f94b2d 100644 --- a/packages/bpk-component-card/src/BpkCardV2/snapshot-test.tsx +++ b/packages/bpk-component-card/src/BpkCardV2/snapshot-test.tsx @@ -27,7 +27,7 @@ import BpkText, { TEXT_STYLES } from '../../../bpk-component-text'; import BpkCardV2 from './BpkCardV2'; import { CARD_V2_SURFACE_COLORS, CARD_V2_VARIANTS } from './common-types'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkCardV2 Snapshots', () => { diff --git a/packages/bpk-component-chatbot-input/src/BpkChatbotInput-test.tsx b/packages/bpk-component-chatbot-input/src/BpkChatbotInput-test.tsx index 089663a76a..50875dff66 100644 --- a/packages/bpk-component-chatbot-input/src/BpkChatbotInput-test.tsx +++ b/packages/bpk-component-chatbot-input/src/BpkChatbotInput-test.tsx @@ -28,7 +28,7 @@ import BpkChatbotInput from './BpkChatbotInput'; import { CHATBOT_INPUT_TYPES } from './common-types'; import { MAX_CHARACTERS } from './constants'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); const defaultProps = { diff --git a/packages/bpk-component-chatbot-input/src/ChatDemo.stories.tsx b/packages/bpk-component-chatbot-input/src/ChatDemo.stories.tsx index 5ca56823e1..4cff7d10f1 100644 --- a/packages/bpk-component-chatbot-input/src/ChatDemo.stories.tsx +++ b/packages/bpk-component-chatbot-input/src/ChatDemo.stories.tsx @@ -298,7 +298,7 @@ const ChatShell = ({ }: { inputSection: ReactNode; messages: Message[]; - messagesEndRef: React.RefObject; + messagesEndRef: React.Ref; renderMessage: (message: Message) => ReactNode; }) => ( +const renderWithProvider = (ui: ReactElement) => render({ui}); const defaultProps = { diff --git a/packages/bpk-component-chatbot-input/src/hooks/useChatbotInput.ts b/packages/bpk-component-chatbot-input/src/hooks/useChatbotInput.ts index 9da0a12a18..24e43b89ee 100644 --- a/packages/bpk-component-chatbot-input/src/hooks/useChatbotInput.ts +++ b/packages/bpk-component-chatbot-input/src/hooks/useChatbotInput.ts @@ -43,7 +43,7 @@ interface UseChatbotInputOptions { } interface UseChatbotInputReturn { - inputRef: RefObject; + inputRef: RefObject; isFocused: boolean; isDisabled: boolean; isOverLimit: boolean; diff --git a/packages/bpk-component-chatbot-input/src/hooks/useTextAreaAutoResize.ts b/packages/bpk-component-chatbot-input/src/hooks/useTextAreaAutoResize.ts index 7679481cbd..3f86388d5e 100644 --- a/packages/bpk-component-chatbot-input/src/hooks/useTextAreaAutoResize.ts +++ b/packages/bpk-component-chatbot-input/src/hooks/useTextAreaAutoResize.ts @@ -20,7 +20,7 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import type { RefObject } from 'react'; interface UseTextAreaAutoResizeProps { - ref: RefObject; + ref: RefObject; value: string; enabled?: boolean; maxLines?: number; diff --git a/packages/bpk-component-chip/src/commonTypes.ts b/packages/bpk-component-chip/src/commonTypes.ts index 8868fea079..0730d01c6f 100644 --- a/packages/bpk-component-chip/src/commonTypes.ts +++ b/packages/bpk-component-chip/src/commonTypes.ts @@ -16,7 +16,6 @@ * limitations under the License. */ -import PropTypes from 'prop-types'; import type { ComponentProps, ReactNode, SyntheticEvent } from 'react'; export const CHIP_TYPES = { diff --git a/packages/bpk-component-comparison-table/src/BpkComparisonTable/BpkComparisonTable-test.tsx b/packages/bpk-component-comparison-table/src/BpkComparisonTable/BpkComparisonTable-test.tsx index 294a367a0b..d699b034f7 100644 --- a/packages/bpk-component-comparison-table/src/BpkComparisonTable/BpkComparisonTable-test.tsx +++ b/packages/bpk-component-comparison-table/src/BpkComparisonTable/BpkComparisonTable-test.tsx @@ -27,7 +27,7 @@ import BpkComparisonTable from './BpkComparisonTable'; import type { BpkCompareColumn } from './common-types'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); beforeAll(() => { diff --git a/packages/bpk-component-comparison-table/src/BpkComparisonTable/accessibility-test.tsx b/packages/bpk-component-comparison-table/src/BpkComparisonTable/accessibility-test.tsx index 7b8c855eee..3206deceb7 100644 --- a/packages/bpk-component-comparison-table/src/BpkComparisonTable/accessibility-test.tsx +++ b/packages/bpk-component-comparison-table/src/BpkComparisonTable/accessibility-test.tsx @@ -27,7 +27,7 @@ import BpkComparisonTable from './BpkComparisonTable'; import type { BpkCompareColumn } from './common-types'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); expect.extend(toHaveNoViolations); diff --git a/packages/bpk-component-comparison-table/src/BpkComparisonTray/BpkComparisonTray-test.tsx b/packages/bpk-component-comparison-table/src/BpkComparisonTray/BpkComparisonTray-test.tsx index 78fb23e908..6f616389f4 100644 --- a/packages/bpk-component-comparison-table/src/BpkComparisonTray/BpkComparisonTray-test.tsx +++ b/packages/bpk-component-comparison-table/src/BpkComparisonTray/BpkComparisonTray-test.tsx @@ -26,7 +26,7 @@ import BpkComparisonTray from './BpkComparisonTray'; import type { BpkComparisonItem } from './common-types'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); const ITEM_1: BpkComparisonItem = { id: '1', label: 'VIP Cars', image: 'car1.png' }; diff --git a/packages/bpk-component-comparison-table/src/BpkComparisonTray/accessibility-test.tsx b/packages/bpk-component-comparison-table/src/BpkComparisonTray/accessibility-test.tsx index ef714dbb7a..42a76119fe 100644 --- a/packages/bpk-component-comparison-table/src/BpkComparisonTray/accessibility-test.tsx +++ b/packages/bpk-component-comparison-table/src/BpkComparisonTray/accessibility-test.tsx @@ -25,7 +25,7 @@ import { BpkProvider } from '../../../bpk-component-layout'; import BpkComparisonTray from './BpkComparisonTray'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); expect.extend(toHaveNoViolations); diff --git a/packages/bpk-component-datepicker/src/BpkDatepicker.tsx b/packages/bpk-component-datepicker/src/BpkDatepicker.tsx index d561abfa9a..77888c9c82 100644 --- a/packages/bpk-component-datepicker/src/BpkDatepicker.tsx +++ b/packages/bpk-component-datepicker/src/BpkDatepicker.tsx @@ -17,7 +17,7 @@ */ import { createRef, Component } from 'react'; -import type { ReactElement, RefObject } from 'react'; +import type { ReactElement, Ref } from 'react'; import BpkBreakpoint, { BREAKPOINTS } from '../../bpk-component-breakpoint'; import { @@ -78,7 +78,7 @@ type Props = { /** * By default BpkInput. If passed, it should be a DOM node with a ref attached to it. */ - inputComponent: ReactElement | null; + inputComponent: ReactElement | null; dateModifiers?: {}; fixedWidth?: boolean; inputProps?: {}; @@ -113,7 +113,7 @@ class BpkDatepicker extends Component { elementRef?: HTMLInputElement; - focusRef?: RefObject; + focusRef?: Ref; static defaultProps = { calendarComponent: DefaultCalendar, diff --git a/packages/bpk-component-drawer/src/BpkDrawerContent-test.tsx b/packages/bpk-component-drawer/src/BpkDrawerContent-test.tsx index 731bff1461..1add107600 100644 --- a/packages/bpk-component-drawer/src/BpkDrawerContent-test.tsx +++ b/packages/bpk-component-drawer/src/BpkDrawerContent-test.tsx @@ -25,7 +25,7 @@ import BpkDrawerContent from './BpkDrawerContent'; jest.mock( 'react-transition-group/Transition', () => - ({ children }: { children: (state: string) => ReactElement }) => + ({ children }: { children: (state: string) => ReactElement }) => children('entered'), ); diff --git a/packages/bpk-component-drawer/src/BpkDrawerContent.tsx b/packages/bpk-component-drawer/src/BpkDrawerContent.tsx index 0561606c82..651685f95c 100644 --- a/packages/bpk-component-drawer/src/BpkDrawerContent.tsx +++ b/packages/bpk-component-drawer/src/BpkDrawerContent.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import type { CSSProperties, ReactNode, RefObject } from 'react'; +import type { CSSProperties, ReactNode, Ref } from 'react'; import { Transition } from 'react-transition-group'; @@ -33,7 +33,7 @@ const getClassName = cssModules(STYLES); type Props = { children: ReactNode, - dialogRef: () => RefObject, + dialogRef: Ref, onCloseAnimationComplete: () => void, onClose: () => void id: string, diff --git a/packages/bpk-component-fieldset/src/BpkFieldset.stories.tsx b/packages/bpk-component-fieldset/src/BpkFieldset.stories.tsx index 9999ffb760..6b3e73b257 100644 --- a/packages/bpk-component-fieldset/src/BpkFieldset.stories.tsx +++ b/packages/bpk-component-fieldset/src/BpkFieldset.stories.tsx @@ -95,7 +95,7 @@ type FieldsetContainerProps = { description?: string; required?: boolean; className?: string | null; - children: ReactElement; + children: ReactElement; }; type FieldsetContainerState = { diff --git a/packages/bpk-component-fieldset/src/BpkFieldset.tsx b/packages/bpk-component-fieldset/src/BpkFieldset.tsx index 6b2f2d2a6c..44bd45a810 100644 --- a/packages/bpk-component-fieldset/src/BpkFieldset.tsx +++ b/packages/bpk-component-fieldset/src/BpkFieldset.tsx @@ -29,7 +29,7 @@ import STYLES from './BpkFieldset.module.scss'; const getClassName = cssModules(STYLES); type BaseProps = { - children: ReactElement; + children: ReactElement; disabled?: boolean; valid?: boolean | null; required?: boolean; diff --git a/packages/bpk-component-input/src/BpkInput.stories.tsx b/packages/bpk-component-input/src/BpkInput.stories.tsx index b9e72d62da..04a15ded66 100644 --- a/packages/bpk-component-input/src/BpkInput.stories.tsx +++ b/packages/bpk-component-input/src/BpkInput.stories.tsx @@ -16,7 +16,6 @@ * limitations under the License. */ -import PropTypes from 'prop-types'; import { Component } from 'react'; import { ArgTypes, Title, Markdown } from '@storybook/addon-docs/blocks'; @@ -32,7 +31,6 @@ import { cssModules } from '../../bpk-react-utils'; import BpkInput from './BpkInput'; import { - propTypes as inputPropTypes, defaultProps as inputDefaultProps, INPUT_TYPES, CLEAR_BUTTON_MODES, @@ -45,16 +43,9 @@ import STYLES from './BpkInput.stories.module.scss'; const getClassName = cssModules(STYLES); -const { value: valueProp, ...propTypes } = inputPropTypes; - const WithOpenEventsMock = (props: WithOpenEventsProps) =>
; class ClearableInput extends Component { - static propTypes = { - ...propTypes, - initialValue: PropTypes.string.isRequired, - }; - static defaultProps = { ...inputDefaultProps, }; diff --git a/packages/bpk-component-input/src/withOpenEvents.tsx b/packages/bpk-component-input/src/withOpenEvents.tsx index 98b596b23b..ed0569d066 100644 --- a/packages/bpk-component-input/src/withOpenEvents.tsx +++ b/packages/bpk-component-input/src/withOpenEvents.tsx @@ -147,7 +147,7 @@ const withOpenEvents =

(WithOpenEventsInputComponent: Componen } }; - render(): ReactElement { + render(): ReactElement { const { className, hasTouchSupport, diff --git a/packages/bpk-component-layout/src/BpkProvider.tsx b/packages/bpk-component-layout/src/BpkProvider.tsx index 751fded8d4..19af275fcb 100644 --- a/packages/bpk-component-layout/src/BpkProvider.tsx +++ b/packages/bpk-component-layout/src/BpkProvider.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import type { ReactNode } from 'react'; +import type { ReactNode, JSX } from 'react'; import { useEffect, useState } from 'react'; import { LocaleProvider } from '@ark-ui/react'; diff --git a/packages/bpk-component-map/src/BpkBasicMapMarker.tsx b/packages/bpk-component-map/src/BpkBasicMapMarker.tsx index 88c1594d9d..ba93932c83 100644 --- a/packages/bpk-component-map/src/BpkBasicMapMarker.tsx +++ b/packages/bpk-component-map/src/BpkBasicMapMarker.tsx @@ -16,7 +16,6 @@ * limitations under the License. */ -import PropTypes from 'prop-types'; import type { ReactNode } from 'react'; import { getDataComponentAttribute } from '../../bpk-react-utils'; @@ -24,7 +23,7 @@ import { getDataComponentAttribute } from '../../bpk-react-utils'; // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`. import BpkOverlayView from './BpkOverlayView'; // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`. -import { LatLongPropType, type LatLong } from './common-types'; +import { type LatLong } from './common-types'; type Props = { children: ReactNode, @@ -51,9 +50,4 @@ const BpkBasicMapMarker = (props: Props) => { ); }; -BpkBasicMapMarker.propTypes = { - children: PropTypes.node.isRequired, - position: LatLongPropType.isRequired, -}; - export default BpkBasicMapMarker; diff --git a/packages/bpk-component-map/src/BpkIconMarker.js b/packages/bpk-component-map/src/BpkIconMarker.js index 16ea0f610e..cfc1c55525 100644 --- a/packages/bpk-component-map/src/BpkIconMarker.js +++ b/packages/bpk-component-map/src/BpkIconMarker.js @@ -41,7 +41,7 @@ export type Props = { }; const BpkIconMarker = (props: Props) => { - const { buttonProps, className, icon, onClick, position, selected, ...rest } = + const { buttonProps = null, className = null, icon, onClick = null, position, selected = false, ...rest } = props; const wrapperClassNames = getClassName( @@ -85,11 +85,4 @@ BpkIconMarker.propTypes = { buttonProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; -BpkIconMarker.defaultProps = { - className: null, - onClick: null, - selected: false, - buttonProps: null, -}; - export default BpkIconMarker; diff --git a/packages/bpk-component-map/src/BpkIconMarkerBackground.js b/packages/bpk-component-map/src/BpkIconMarkerBackground.js index d94b89b3ca..4cb53b1c85 100644 --- a/packages/bpk-component-map/src/BpkIconMarkerBackground.js +++ b/packages/bpk-component-map/src/BpkIconMarkerBackground.js @@ -31,7 +31,7 @@ type Props = { }; const BpkIconMarkerBackground = (props: Props) => { - const { disabled, interactive, selected, ...rest } = props; + const { disabled = false, interactive = false, selected = false, ...rest } = props; const classNames = getClassName( 'bpk-icon-marker-background', @@ -72,10 +72,4 @@ const BpkIconMarkerBackground = (props: Props) => { ); }; -BpkIconMarkerBackground.defaultProps = { - disabled: false, - interactive: false, - selected: false, -}; - export default BpkIconMarkerBackground; diff --git a/packages/bpk-component-map/src/BpkMap.js b/packages/bpk-component-map/src/BpkMap.js index 44000ecf2f..5511206bf2 100644 --- a/packages/bpk-component-map/src/BpkMap.js +++ b/packages/bpk-component-map/src/BpkMap.js @@ -78,20 +78,20 @@ type Props = { const BpkMap = (props: Props) => { const { - bounds, - center, - children, - className, - greedyGestureHandling, - mapId, - mapOptionStyles, - mapRef, - onRegionChange, - onTilesLoaded, - onZoom, - panEnabled, - showControls, - zoom, + bounds = null, + center = undefined, + children = null, + className = null, + greedyGestureHandling = false, + mapId = null, + mapOptionStyles = null, + mapRef = null, + onRegionChange = null, + onTilesLoaded = null, + onZoom = null, + panEnabled = true, + showControls = true, + zoom = 15, } = props; if (!bounds && !center) { @@ -210,21 +210,4 @@ BpkMap.propTypes = { mapId: PropTypes.string, }; -BpkMap.defaultProps = { - bounds: null, - center: undefined, - children: null, - greedyGestureHandling: false, - mapRef: null, - onRegionChange: null, - onZoom: null, - onTilesLoaded: null, - panEnabled: true, - showControls: true, - zoom: 15, - className: null, - mapOptionStyles: null, - mapId: null, -}; - export default BpkMap; diff --git a/packages/bpk-component-map/src/BpkMap.stories.js b/packages/bpk-component-map/src/BpkMap.stories.js index 80a1d4d1c1..bfafeb6325 100644 --- a/packages/bpk-component-map/src/BpkMap.stories.js +++ b/packages/bpk-component-map/src/BpkMap.stories.js @@ -97,7 +97,8 @@ const AlignedFoodIconSm = withRtlSupport(FoodIconSm); const AlignedHeartIconSm = withRtlSupport(HeartIconSm); const StoryMap = (props) => { - const { children, language, ...rest } = props; + // eslint-disable-next-line no-unused-vars + const { children = null, language = '', ...rest } = props; return (

{/* $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'. */} @@ -113,11 +114,6 @@ StoryMap.propTypes = { language: PropTypes.string, }; -StoryMap.defaultProps = { - children: null, - language: '', -}; - const venues = [ { id: '1', diff --git a/packages/bpk-component-map/src/withGoogleMapsScript.js b/packages/bpk-component-map/src/withGoogleMapsScript.js index c6a1af0b89..d27c37b7b8 100644 --- a/packages/bpk-component-map/src/withGoogleMapsScript.js +++ b/packages/bpk-component-map/src/withGoogleMapsScript.js @@ -37,9 +37,9 @@ export const LibraryShapeType = PropTypes.arrayOf( function withGoogleMapsScript(Component: ComponentType) { const WithGoogleMapsScript = ({ googleMapsApiKey, - libraries, - loadingElement, - preventGoogleFontsLoading, + libraries = ['geometry', 'drawing', 'places'], + loadingElement = , + preventGoogleFontsLoading = false, ...rest }: { [string]: any, @@ -69,12 +69,6 @@ function withGoogleMapsScript(Component: ComponentType) { preventGoogleFontsLoading: PropTypes.bool, }; - WithGoogleMapsScript.defaultProps = { - loadingElement: , - preventGoogleFontsLoading: false, - libraries: ['geometry', 'drawing', 'places'], - }; - return WithGoogleMapsScript; } diff --git a/packages/bpk-component-navigation-bar/src/BpkNavigationBar.tsx b/packages/bpk-component-navigation-bar/src/BpkNavigationBar.tsx index 98ac83d10a..90f4689466 100644 --- a/packages/bpk-component-navigation-bar/src/BpkNavigationBar.tsx +++ b/packages/bpk-component-navigation-bar/src/BpkNavigationBar.tsx @@ -42,8 +42,8 @@ export type Props = { * Note: this prop only applies when `title` is a string; ReactNode titles are not truncated and wrap naturally. */ wrapTitle?: boolean; className?: string; - leadingButton?: ReactElement | null; - trailingButton?: ReactElement | null; + leadingButton?: ReactElement | null; + trailingButton?: ReactElement | null; sticky?: boolean; barStyle?: BarStyle; [rest: string]: any; // Inexact rest. See decisions/inexact-rest.md diff --git a/packages/bpk-component-navigation-tab-group/src/BpkNavigationTabGroup.tsx b/packages/bpk-component-navigation-tab-group/src/BpkNavigationTabGroup.tsx index 3c531b9559..e00a7265c5 100644 --- a/packages/bpk-component-navigation-tab-group/src/BpkNavigationTabGroup.tsx +++ b/packages/bpk-component-navigation-tab-group/src/BpkNavigationTabGroup.tsx @@ -61,7 +61,7 @@ type TabWrapProps = { tab: TabWrapItem; type: NavigationTabGroupTypes; selected: boolean; - children: ReactElement; + children: ReactElement; onClick: (e: MouseEvent) => void; }; diff --git a/packages/bpk-component-pagination/src/BpkPagination.js b/packages/bpk-component-pagination/src/BpkPagination.js index 776e8c4164..5dfccaac11 100644 --- a/packages/bpk-component-pagination/src/BpkPagination.js +++ b/packages/bpk-component-pagination/src/BpkPagination.js @@ -36,15 +36,15 @@ const handlePageChange = (onPageChange, pageCount) => (nextPageIndex) => { const BpkPagination = (props) => { const classNames = [getClassName('bpk-pagination')]; const { - className, + className = null, nextLabel, - onPageChange, + onPageChange = null, pageCount, pageLabel, paginationLabel, previousLabel, selectedPageIndex, - visibleRange, + visibleRange = 3, ...rest } = props; @@ -93,10 +93,4 @@ BpkPagination.propTypes = { className: PropTypes.string, }; -BpkPagination.defaultProps = { - onPageChange: null, - visibleRange: 3, - className: null, -}; - export default BpkPagination; diff --git a/packages/bpk-component-pagination/src/BpkPaginationBreak.js b/packages/bpk-component-pagination/src/BpkPaginationBreak.js index 2c803e4e7f..f48f4263fa 100644 --- a/packages/bpk-component-pagination/src/BpkPaginationBreak.js +++ b/packages/bpk-component-pagination/src/BpkPaginationBreak.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; const BpkPaginationBreak = (props) => { - const { breakLabel } = props; + const { breakLabel = '...' } = props; return
{breakLabel}
; }; @@ -27,8 +27,4 @@ BpkPaginationBreak.propTypes = { breakLabel: PropTypes.string, }; -BpkPaginationBreak.defaultProps = { - breakLabel: '...', -}; - export default BpkPaginationBreak; diff --git a/packages/bpk-component-pagination/src/BpkPaginationNudger.js b/packages/bpk-component-pagination/src/BpkPaginationNudger.js index f79749ff14..32c60e32b4 100644 --- a/packages/bpk-component-pagination/src/BpkPaginationNudger.js +++ b/packages/bpk-component-pagination/src/BpkPaginationNudger.js @@ -34,7 +34,7 @@ const nudgerIcon = (forward) => forward ? () : (); const BpkPaginationNudger = (props) => { - const { disabled, forward, label, onNudge } = props; + const { disabled = false, forward = false, label, onNudge } = props; return (
@@ -60,9 +60,4 @@ BpkPaginationNudger.propTypes = { disabled: PropTypes.bool, }; -BpkPaginationNudger.defaultProps = { - forward: false, - disabled: false, -}; - export default BpkPaginationNudger; diff --git a/packages/bpk-component-pagination/src/BpkPaginationPage.js b/packages/bpk-component-pagination/src/BpkPaginationPage.js index c4f87d04f0..e6b2960462 100644 --- a/packages/bpk-component-pagination/src/BpkPaginationPage.js +++ b/packages/bpk-component-pagination/src/BpkPaginationPage.js @@ -26,7 +26,7 @@ const getClassName = cssModules(STYLES); const BpkPaginationPage = (props) => { const classNames = [getClassName('bpk-pagination-page')]; - const { isSelected, onSelect, page, pageLabel } = props; + const { isSelected = false, onSelect, page, pageLabel } = props; if (!isSelected) { // reverse class type so we can always load `buttons.bpk-button` as a base style for overridding. @@ -53,8 +53,4 @@ BpkPaginationPage.propTypes = { isSelected: PropTypes.bool, }; -BpkPaginationPage.defaultProps = { - isSelected: false, -}; - export default BpkPaginationPage; diff --git a/packages/bpk-component-phone-input/src/BpkPhoneInput.js b/packages/bpk-component-phone-input/src/BpkPhoneInput.js index b7f77ee393..c13f84fa28 100644 --- a/packages/bpk-component-phone-input/src/BpkPhoneInput.js +++ b/packages/bpk-component-phone-input/src/BpkPhoneInput.js @@ -75,21 +75,21 @@ type CommonProps = { const BpkPhoneInput = (props: Props) => { const { - className, + className = null, dialingCode, - dialingCodeMask, + dialingCodeMask = false, dialingCodeProps, dialingCodes, - disabled, + disabled = false, id, label, - large, + large = false, name, onChange, onDialingCodeChange, - valid, + valid = null, value, - wrapperProps, + wrapperProps = {}, ...rest } = props; @@ -170,9 +170,9 @@ const BpkPhoneInput = (props: Props) => { > {dialingCodes.map(({ code, description, ...extraDialingProps }) => ( // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'. - + ) ))}
@@ -229,13 +229,4 @@ BpkPhoneInput.propTypes = { wrapperProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; -BpkPhoneInput.defaultProps = { - className: null, - disabled: false, - dialingCodeMask: false, - large: false, - valid: null, - wrapperProps: {}, -}; - export default BpkPhoneInput; diff --git a/packages/bpk-component-scrollable-calendar/src/BpkScrollableCalendarGrid-test.tsx b/packages/bpk-component-scrollable-calendar/src/BpkScrollableCalendarGrid-test.tsx index 1842949eef..bb0b269dd6 100644 --- a/packages/bpk-component-scrollable-calendar/src/BpkScrollableCalendarGrid-test.tsx +++ b/packages/bpk-component-scrollable-calendar/src/BpkScrollableCalendarGrid-test.tsx @@ -16,8 +16,6 @@ * limitations under the License. */ -import PropTypes from 'prop-types'; - import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { addMonths, isWeekend } from 'date-fns'; @@ -93,9 +91,6 @@ describe('BpkCalendarScrollGrid', () => { } return
; }; - MyCustomDate.propTypes = { - date: PropTypes.instanceOf(Date).isRequired, - }; const { asFragment } = render( { } return
; }; - MyCustomDate.propTypes = { - date: PropTypes.instanceOf(Date).isRequired, - }; const { asFragment } = render( { - const { blank, children, className, href, onClick, ...rest } = props; + const { blank = false, children, className = null, href = null, onClick = null, ...rest } = props; const classNames = [ getClassName( 'bpk-section-list-item', @@ -103,11 +103,4 @@ BpkSectionListItem.propTypes = { onClick: PropTypes.func, }; -BpkSectionListItem.defaultProps = { - blank: false, - className: null, - href: null, - onClick: null, -}; - export default BpkSectionListItem; diff --git a/packages/bpk-component-section-list/src/BpkSectionListSection.js b/packages/bpk-component-section-list/src/BpkSectionListSection.js index a912075199..600737980f 100644 --- a/packages/bpk-component-section-list/src/BpkSectionListSection.js +++ b/packages/bpk-component-section-list/src/BpkSectionListSection.js @@ -35,7 +35,7 @@ type Props = { }; const BpkSectionListSection = (props: Props) => { - const { children, headerText, ...rest } = props; + const { children, headerText = null, ...rest } = props; return (
@@ -58,8 +58,4 @@ BpkSectionListSection.propTypes = { headerText: PropTypes.string, }; -BpkSectionListSection.defaultProps = { - headerText: null, -}; - export default BpkSectionListSection; diff --git a/packages/bpk-component-select/src/BpkSelect.tsx b/packages/bpk-component-select/src/BpkSelect.tsx index 1e47cb4ce1..d5105b21be 100644 --- a/packages/bpk-component-select/src/BpkSelect.tsx +++ b/packages/bpk-component-select/src/BpkSelect.tsx @@ -38,7 +38,7 @@ export type Props = Omit< dockedFirst?: boolean; dockedLast?: boolean; dockedMiddle?: boolean; - image?: ReactElement | null; + image?: ReactElement | null; large?: boolean; valid?: boolean | null; wrapperClassName?: string | null; diff --git a/packages/bpk-component-slider/src/BpkSlider.d.ts b/packages/bpk-component-slider/src/BpkSlider.d.ts index ddd1caf117..e482ec5946 100644 --- a/packages/bpk-component-slider/src/BpkSlider.d.ts +++ b/packages/bpk-component-slider/src/BpkSlider.d.ts @@ -15,6 +15,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { JSX } from 'react'; + import type { Props as BpkSliderProps } from './BpkSlider' export type Props = BpkSliderProps declare const BpkSlider: ({ ariaLabel, ariaValuetext, max, min, minDistance, onAfterChange, onChange, step, value }: Props) => JSX.Element diff --git a/packages/bpk-component-slider/src/BpkSlider.tsx b/packages/bpk-component-slider/src/BpkSlider.tsx index 79c1cd724a..99430d60b9 100644 --- a/packages/bpk-component-slider/src/BpkSlider.tsx +++ b/packages/bpk-component-slider/src/BpkSlider.tsx @@ -102,7 +102,7 @@ const BpkSlider = ({ } }, []); - const thumbRefs = [useRef(null), useRef(null)]; + const thumbRefs = [useRef(null), useRef(null)]; const handleOnChange = useCallback( (sliderValues: number[]) => { @@ -201,12 +201,12 @@ const BpkSlider = ({ const BubbleInput = forwardRef( ( props: ComponentPropsWithRef<'input'> & { - thumbRef: RefObject; + thumbRef: RefObject; }, forwardedRef, ) => { const { thumbRef, value, ...inputProps } = props; - const ref = useRef(); + const ref = useRef(null); const composedRefs = useComposedRefs(forwardedRef, ref); // This Hook Provides the native behaviour that the input range type would have around the "change" event. diff --git a/packages/bpk-component-spinner/src/SpinnerLayout.story-helpers.tsx b/packages/bpk-component-spinner/src/SpinnerLayout.story-helpers.tsx index 937afb043a..52022fc6a6 100644 --- a/packages/bpk-component-spinner/src/SpinnerLayout.story-helpers.tsx +++ b/packages/bpk-component-spinner/src/SpinnerLayout.story-helpers.tsx @@ -16,7 +16,6 @@ * limitations under the License. */ -import PropTypes from 'prop-types'; import type { ReactElement } from 'react'; import { Children } from 'react'; @@ -49,8 +48,4 @@ const SpinnerLayout = (props: Props) => { ); }; -SpinnerLayout.propTypes = { - children: PropTypes.node.isRequired, -}; - export default SpinnerLayout; diff --git a/packages/bpk-component-ticket/src/BpkTicket.js b/packages/bpk-component-ticket/src/BpkTicket.js index 55a0f5f79d..b0bb72c1d6 100644 --- a/packages/bpk-component-ticket/src/BpkTicket.js +++ b/packages/bpk-component-ticket/src/BpkTicket.js @@ -46,13 +46,13 @@ type Props = { const BpkTicket = (props: Props) => { const { children, - className, - href, - padded, + className = null, + href = null, + padded = true, stub, - stubClassName, - stubProps, - vertical, + stubClassName = null, + stubProps = {}, + vertical = false, ...rest } = props; @@ -141,13 +141,4 @@ BpkTicket.propTypes = { stubProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; -BpkTicket.defaultProps = { - className: null, - href: null, - padded: true, - vertical: false, - stubClassName: null, - stubProps: {}, -}; - export default BpkTicket; diff --git a/packages/package-lock.json b/packages/package-lock.json index cc9c56e3a1..8988d84e99 100644 --- a/packages/package-lock.json +++ b/packages/package-lock.json @@ -35,8 +35,8 @@ }, "peerDependencies": { "date-fns": "3.3.1 - 4", - "react": "17.0.2 - 18.3.1", - "react-dom": "17.0.2 - 18.3.1", + "react": "18.3.1 - 19.2.5", + "react-dom": "18.3.1 - 19.2.5", "react-transition-group": "^4.4.5", "sass": "^1", "sass-embedded": "^1" diff --git a/packages/package.json b/packages/package.json index 172053580a..0f91efba17 100644 --- a/packages/package.json +++ b/packages/package.json @@ -48,8 +48,8 @@ }, "peerDependencies": { "date-fns": "3.3.1 - 4", - "react": "17.0.2 - 18.3.1", - "react-dom": "17.0.2 - 18.3.1", + "react": "18.3.1 - 19.2.5", + "react-dom": "18.3.1 - 19.2.5", "react-transition-group": "^4.4.5", "sass": "^1", "sass-embedded": "^1" diff --git a/packages/react-version-test.js b/packages/react-version-test.js index 9d8d5f47d0..58d14d17e3 100644 --- a/packages/react-version-test.js +++ b/packages/react-version-test.js @@ -18,6 +18,6 @@ import { version } from 'react'; -it('packages/* should be ^18.0.0', () => { - expect(version).toMatch(/^18/); +it('packages/* should run on React 18 or 19', () => { + expect(version).toMatch(/^(18|19)\./); }); diff --git a/scripts/jest/normalizeUseIdSerializer.js b/scripts/jest/normalizeUseIdSerializer.js new file mode 100644 index 0000000000..fd1dc3ecba --- /dev/null +++ b/scripts/jest/normalizeUseIdSerializer.js @@ -0,0 +1,43 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2016 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// React 18's useId() emits IDs of the form `:r{n}:` while React 19 emits +// `_r_{n}_`. Tests that snapshot DOM with these IDs would otherwise need two +// sets of snapshots — one per React major. This serializer normalises the R19 +// form back to the R18 form so a single snapshot serves both. +const { plugins } = require('pretty-format'); + +const { DOMCollection, DOMElement } = plugins; +const r19Pattern = /_r_(\w+)_/g; + +module.exports = { + test: (val) => + val != null && (DOMElement.test(val) || DOMCollection.test(val)), + serialize: (val, config, indentation, depth, refs, printer) => { + const inner = DOMElement.test(val) ? DOMElement : DOMCollection; + const serialized = inner.serialize( + val, + config, + indentation, + depth, + refs, + printer, + ); + return serialized.replace(r19Pattern, ':r$1:'); + }, +}; diff --git a/scripts/react-19/README.md b/scripts/react-19/README.md new file mode 100644 index 0000000000..f951a69212 --- /dev/null +++ b/scripts/react-19/README.md @@ -0,0 +1,74 @@ +# React 19 migration scripts + +One-shot tooling used during the LOOM-2442 migration. Kept in the repo for +reproducibility; not part of the normal build/test pipeline. + +## `transforms/strip-proptypes.js` + +Custom jscodeshift transform that: + +- **Phase A** (`.ts`/`.tsx` only): removes `Identifier.propTypes = { ... }` + expression statements, `static propTypes` class fields, and the + `import PropTypes from 'prop-types'` import (only when no other references + to `PropTypes` remain in the file). +- **Phase B** (all files): converts function-component + `Identifier.defaultProps = ` into ES6 destructure + defaults at the function's parameter or top-of-body destructure point. +- **Phase C** (all files): reports class components with `static defaultProps` + to stderr — manual migration required (React 19 makes them no-ops). + +`.js`/`.jsx` files keep their `prop-types` in place because the project's +`react/prop-types` lint rule requires either prop-types or types for prop +validation, and most `.js` Flow components rely on prop-types. React 19 +ignores `propTypes` silently, so leaving them is safe; full removal will +happen during the parallel TS migration. + +## Running the transform + +The codemod toolchain isn't installed by default — install it on demand: + +```bash +npm install --save-dev --save-exact codemod@1.9.1 types-react-codemod@3.5.3 +``` + +(types-react-codemod transitively pulls in jscodeshift, which is what we +actually need.) + +Then run two passes: + +```bash +# .ts / .tsx files +node ./node_modules/types-react-codemod/node_modules/jscodeshift/bin/jscodeshift.js \ + --transform scripts/react-19/transforms/strip-proptypes.js \ + --parser=tsx \ + --no-babel \ + --extensions=ts,tsx \ + --ignore-pattern '**/{node_modules,dist,build,storybook-static}/**' \ + packages .storybook + +# .js / .jsx files (Flow) +node ./node_modules/types-react-codemod/node_modules/jscodeshift/bin/jscodeshift.js \ + --transform scripts/react-19/transforms/strip-proptypes.js \ + --parser=babylon \ + --no-babel \ + --extensions=js,jsx \ + --ignore-pattern '**/{node_modules,dist,build,storybook-static}/**' \ + packages .storybook +``` + +Skip messages (Phase B couldn't safely merge defaults; Phase C class +components; PropTypes-as-const-export files) go to stderr — capture with +`2>/tmp/skips.txt` for review. + +When done, uninstall the codemod packages: + +```bash +npm uninstall codemod types-react-codemod +``` + +## Future home + +This transform should ultimately move to +`web-migration-scripts/migrations/2026-05-react-19/transforms/` +once that migration directory is set up. Kept locally for now to give +LOOM-2442 reviewers a clear lineage of how the diff was generated. diff --git a/scripts/react-19/transforms/strip-proptypes.js b/scripts/react-19/transforms/strip-proptypes.js new file mode 100644 index 0000000000..afbcdfad28 --- /dev/null +++ b/scripts/react-19/transforms/strip-proptypes.js @@ -0,0 +1,267 @@ +/* + * Strip prop-types and migrate function-component defaultProps for React 19. + * + * Phase A (TS/TSX files only): + * - Remove top-level `.propTypes = { ... }` ExpressionStatements. + * - Remove `static propTypes = { ... }` ClassProperty fields. + * - Remove `import PropTypes from 'prop-types'` IF no remaining references. + * + * Skipped for .js/.jsx because the project's eslint config requires either + * prop-types or types for prop validation, and most .js components rely on + * prop-types. React 19 ignores propTypes silently on all components, so + * leaving them in place doesn't break anything; the .js cleanup happens + * later as part of the TS migration. + * + * Phase B (all files): + * - Find `.defaultProps = ` ExpressionStatements + * where resolves to a function declaration / arrow function / + * function expression at the top level, AND the function's first parameter + * destructures `props` either inline or via `const { ... } = props` at the + * top of the body. + * - Merge each defaultProps key into the destructure as `key = value`. If the + * key isn't in the destructure already, append it. + * - Remove the .defaultProps assignment. + * - If the function uses `(props: Props)` and accesses props as `props.X` + * without a top-level destructure, SKIP — leave for manual review. + * + * Phase C (all files): + * - Class components with `static defaultProps`: SKIP and report. Manual + * migration required (convert to functional component or add destructure- + * with-defaults inside render()). + */ + +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + const root = j(file.source); + let changed = false; + const skips = []; + + const runPhaseA = /\.(ts|tsx)$/.test(file.path); + + if (runPhaseA) { + // Phase A.2: remove `.propTypes = { ... }` ExpressionStatements. + root + .find(j.ExpressionStatement, { + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + property: { name: 'propTypes' }, + }, + }, + }) + .forEach((path) => { + const {left} = path.node.expression; + if (left.object.type !== 'Identifier') return; + j(path).remove(); + changed = true; + }); + + // Phase A.3: remove `static propTypes = { ... }` class fields. + root + .find(j.ClassProperty, { + static: true, + key: { name: 'propTypes' }, + }) + .forEach((path) => { + j(path).remove(); + changed = true; + }); + } + + // Phase B: function-component defaultProps → destructure defaults. + root + .find(j.ExpressionStatement, { + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + property: { name: 'defaultProps' }, + }, + right: { type: 'ObjectExpression' }, + }, + }) + .forEach((path) => { + const expr = path.node.expression; + const {left} = expr; + if (left.object.type !== 'Identifier') return; + const componentName = left.object.name; + const defaultsObj = expr.right; + + const decls = root + .find(j.VariableDeclarator, { id: { name: componentName } }) + .filter((p) => { + const {init} = p.node; + return ( + init && + (init.type === 'ArrowFunctionExpression' || + init.type === 'FunctionExpression') + ); + }); + + const fnDecls = root.find(j.FunctionDeclaration, { + id: { name: componentName }, + }); + + let fnPath = null; + if (decls.size() === 1) fnPath = decls.get(); + else if (fnDecls.size() === 1) fnPath = fnDecls.get(); + + if (!fnPath) { + skips.push(`${file.path}: defaultProps for "${componentName}" — declaration not found or ambiguous`); + return; + } + + const fnNode = fnPath.node.init || fnPath.node; + const {params} = fnNode; + if (!params || params.length === 0) { + skips.push(`${file.path}: defaultProps for "${componentName}" — fn has no params`); + return; + } + + const firstParam = params[0]; + let destructurePattern = null; + + if (firstParam.type === 'ObjectPattern') { + destructurePattern = firstParam; + } else if ( + firstParam.type === 'AssignmentPattern' && + firstParam.left.type === 'ObjectPattern' + ) { + destructurePattern = firstParam.left; + } else if (firstParam.type === 'Identifier') { + const paramName = firstParam.name; + const {body} = fnNode; + if (!body || body.type !== 'BlockStatement') { + skips.push(`${file.path}: defaultProps for "${componentName}" — body is not a block`); + return; + } + const firstStmt = body.body.find( + (s) => + s.type === 'VariableDeclaration' && + s.declarations.length === 1 && + s.declarations[0].id.type === 'ObjectPattern' && + s.declarations[0].init && + s.declarations[0].init.type === 'Identifier' && + s.declarations[0].init.name === paramName, + ); + if (!firstStmt) { + skips.push(`${file.path}: defaultProps for "${componentName}" — no top-level destructure of ${paramName}`); + return; + } + destructurePattern = firstStmt.declarations[0].id; + } else { + skips.push(`${file.path}: defaultProps for "${componentName}" — unhandled first param type ${firstParam.type}`); + return; + } + + for (const prop of defaultsObj.properties) { + if (prop.type !== 'Property' && prop.type !== 'ObjectProperty') { + skips.push(`${file.path}: defaultProps for "${componentName}" — non-Property entry, skipped`); + return; + } + if (prop.computed) { + skips.push(`${file.path}: defaultProps for "${componentName}" — computed key, skipped`); + return; + } + if (prop.key.type !== 'Identifier' && prop.key.type !== 'Literal' && prop.key.type !== 'StringLiteral') { + skips.push(`${file.path}: defaultProps for "${componentName}" — non-identifier key`); + return; + } + const keyName = + prop.key.type === 'Identifier' ? prop.key.name : prop.key.value; + + const existing = destructurePattern.properties.find((p) => { + if (p.type !== 'Property' && p.type !== 'ObjectProperty') return false; + if (p.computed) return false; + const k = p.key.type === 'Identifier' ? p.key.name : p.key.value; + return k === keyName; + }); + + if (existing) { + if (existing.value.type === 'AssignmentPattern') { + // Keep existing default; don't override. + } else { + existing.value = j.assignmentPattern(existing.value, prop.value); + changed = true; + } + } else { + const newProp = j.property.from({ + kind: 'init', + key: j.identifier(keyName), + value: j.assignmentPattern(j.identifier(keyName), prop.value), + shorthand: true, + }); + destructurePattern.properties.push(newProp); + changed = true; + } + } + + j(path).remove(); + changed = true; + }); + + if (runPhaseA) { + // Phase A.1: strip `import PropTypes from 'prop-types'` if no remaining + // references after A.2/A.3. Re-attach leading comments (license header) + // to the next sibling so they aren't lost with the import. + root + .find(j.ImportDeclaration, { source: { value: 'prop-types' } }) + .forEach((path) => { + const localName = path.node.specifiers + .map((s) => s.local && s.local.name) + .filter(Boolean)[0]; + if (!localName) { + j(path).remove(); + changed = true; + return; + } + const remainingRefs = root + .find(j.Identifier, { name: localName }) + .filter((p) => { + if ( + p.parent.node.type === 'ImportDefaultSpecifier' || + p.parent.node.type === 'ImportSpecifier' || + p.parent.node.type === 'ImportNamespaceSpecifier' + ) { + return false; + } + return true; + }); + if (remainingRefs.size() > 0) { + skips.push(`${file.path}: PropTypes still referenced — leaving import in place`); + return; + } + const parent = path.parent.node; + const siblings = parent.body || parent.program?.body; + if (siblings) { + const idx = siblings.indexOf(path.node); + const next = siblings[idx + 1]; + if (next && path.node.comments && path.node.comments.length > 0) { + const leading = path.node.comments.filter((c) => c.leading); + next.comments = (next.comments || []).concat(leading); + } + } + j(path).remove(); + changed = true; + }); + } + + // Phase C: report class components with `static defaultProps`. + root + .find(j.ClassProperty, { static: true, key: { name: 'defaultProps' } }) + .forEach(() => { + skips.push(`${file.path}: class component static defaultProps — manual migration`); + }); + + if (skips.length > 0) { + process.stderr.write(`${skips.map((s) => `[skip] ${s}`).join('\n') }\n`); + } + + return changed ? root.toSource({ quote: 'single' }) : null; +}; + +// Parser is selected via the `--parser` CLI flag. Run twice: once with +// --parser=tsx for .ts/.tsx, once with --parser=babylon for .js/.jsx.