From 0236698ee50fab889af5e06cea6e086188a0a593 Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Mon, 15 Jun 2026 21:46:26 -0700 Subject: [PATCH 1/9] refactor: drop findDOMNode and dead defaultProps Replace ReactDOM.findDOMNode(this) in the deprecated ColorPicker dropdown Box with a typed root ref (removed in React 19), and delete the redundant Button.defaultProps block (ignored for forwardRef in React 19; already shadowed by the destructuring defaults). Both fixes are version-safe under React 18. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../deprecated-button/deprecated-button.tsx | 6 ------ .../color-picker/deprecated-dropdown/dropdown.tsx | 13 +++++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/components/color-picker/deprecated-button/deprecated-button.tsx b/src/components/color-picker/deprecated-button/deprecated-button.tsx index 0b81fbf1f..a7095f621 100644 --- a/src/components/color-picker/deprecated-button/deprecated-button.tsx +++ b/src/components/color-picker/deprecated-button/deprecated-button.tsx @@ -83,10 +83,4 @@ const Button = React.forwardRef(function Button( Button.displayName = 'Button' -Button.defaultProps = { - size: 'default', - loading: false, - disabled: false, -} - export { Button } diff --git a/src/components/color-picker/deprecated-dropdown/dropdown.tsx b/src/components/color-picker/deprecated-dropdown/dropdown.tsx index 403252dd5..768744de8 100644 --- a/src/components/color-picker/deprecated-dropdown/dropdown.tsx +++ b/src/components/color-picker/deprecated-dropdown/dropdown.tsx @@ -1,7 +1,6 @@ import './dropdown.less' import * as React from 'react' -import ReactDOM from 'react-dom' import classNames from 'classnames' @@ -29,6 +28,8 @@ type BoxState = { class Box extends React.Component { public static displayName: string + private rootRef = React.createRef() + constructor(props: BoxProps, context: React.Context) { super(props, context) this.state = { @@ -48,7 +49,7 @@ class Box extends React.Component { _timeout?: ReturnType _handleClickOutside = (event: MouseEvent) => { - const dropdownDOMNode = ReactDOM.findDOMNode(this) + const dropdownDOMNode = this.rootRef.current if (dropdownDOMNode && !dropdownDOMNode.contains(event.target as Node)) this._toggleShowBody() @@ -93,13 +94,12 @@ class Box extends React.Component { ) if (scrollingParent) { - const dropdown = ReactDOM.findDOMNode(this) + const dropdown = this.rootRef.current if (!dropdown) { return } - const dropdownVerticalPosition = (ReactDOM.findDOMNode(this) as HTMLElement) - .offsetTop - const dropdownTrigger = (dropdown as Element).querySelector('.trigger') + const dropdownVerticalPosition = dropdown.offsetTop + const dropdownTrigger = dropdown.querySelector('.trigger') if (!dropdownTrigger) { return } @@ -160,6 +160,7 @@ class Box extends React.Component { return (
Date: Mon, 15 Jun 2026 22:13:29 -0700 Subject: [PATCH 2/9] feat: support React 19 Bump dev react/react-dom/@types/react/@types/react-dom/react-is to 19.x and widen the peer ranges to >=18 <20 (additive for existing React 18 consumers). Run types-react-codemod preset-19 for the React.JSX scoping and useRef initial argument, then hand-fix the React 19 type and runtime breakages: - tooltip: read the child ref from props.ref under React 19 (falls back to element.ref on 18) instead of the removed element.ref. - polymorphism: cast the forwardRef render fn to React 19's PropsWithoutRef render signature. - stack: widen react-keyed-flatten-children's React 19-removed ReactChild[] return to ReactNode[]. - revert the codemod's ReactElement additions (the repo bans explicit any; bare ReactElement now defaults to unknown). - add @types/prop-types, previously pulled in transitively by @types/react 18. Also drive the hover-opened submenu test selection via keyboard: jsdom has no layout, so ariakit closes the submenu when the pointer moves onto an item, and React 19's synchronous flushing unmounts it before the click lands. Real-browser hover + click works (verified in Storybook); keyboard selection is layout- independent and version-stable. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 72 +++++++++---------- package.json | 15 ++-- src/checkbox-field/checkbox-field.tsx | 2 + .../deprecated-dropdown/dropdown.tsx | 2 + src/icons/alert-icon.tsx | 1 + src/icons/banner-icon.tsx | 1 + src/icons/close-icon.tsx | 2 + src/icons/password-hidden-icon.tsx | 2 + src/icons/password-visible-icon.tsx | 2 + src/menu/menu.test.tsx | 4 +- src/modal/modal-examples.stories.tsx | 2 + src/modal/modal-stories-components.tsx | 1 + src/select-field/select-field.tsx | 1 + src/stack/stack.tsx | 21 +++--- src/text-area/text-area.tsx | 4 +- src/toast/toast.stories.tsx | 1 + src/toast/use-toasts.tsx | 2 +- src/tooltip/tooltip.tsx | 11 +-- src/utils/polymorphism.ts | 24 +++++-- src/utils/storybook-helper.tsx | 1 + 20 files changed, 101 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index bab4a7013..47974a483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,8 +50,9 @@ "@types/jest": "28.1.8", "@types/jest-axe": "^3.5.3", "@types/marked": "^4.0.8", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", + "@types/prop-types": "^15.7.15", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@vitejs/plugin-react": "5.2.0", @@ -80,11 +81,11 @@ "mockdate": "^3.0.2", "plop": "^3.1.1", "prettier": "3.6.2", - "react": "18.3.1", + "react": "19.2.7", "react-compiler-runtime": "1.0.0", "react-docgen-typescript": "2.4.0", - "react-dom": "18.3.1", - "react-is": "18.3.1", + "react-dom": "19.2.7", + "react-is": "19.2.7", "rimraf": "^3.0.2", "rollup": "2.79.2", "rollup-plugin-styles": "4.0.0", @@ -99,9 +100,9 @@ "@ariakit/react": "~0.4.19", "classnames": "^2.2.5", "prop-types": "^15.6.2", - "react": ">=18.0.0 <19.0.0", + "react": ">=18.0.0 <20.0.0", "react-compiler-runtime": "^1.0.0", - "react-dom": ">=18.0.0 <19.0.0" + "react-dom": ">=18.0.0 <20.0.0" } }, "node_modules/@actions/core": { @@ -7350,26 +7351,26 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^19.2.0" } }, "node_modules/@types/resolve": { @@ -22621,13 +22622,10 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } @@ -22700,17 +22698,16 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.7" } }, "node_modules/react-focus-lock": { @@ -22737,9 +22734,9 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", "dev": true, "license": "MIT" }, @@ -23703,14 +23700,11 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "license": "MIT" }, "node_modules/semantic-release": { "version": "25.0.3", diff --git a/package.json b/package.json index 4d2715e0e..7ce7e09a5 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,9 @@ "@ariakit/react": "~0.4.19", "classnames": "^2.2.5", "prop-types": "^15.6.2", - "react": ">=18.0.0 <19.0.0", + "react": ">=18.0.0 <20.0.0", "react-compiler-runtime": "^1.0.0", - "react-dom": ">=18.0.0 <19.0.0" + "react-dom": ">=18.0.0 <20.0.0" }, "devDependencies": { "@ariakit/react": "0.4.19", @@ -95,8 +95,9 @@ "@types/jest": "28.1.8", "@types/jest-axe": "^3.5.3", "@types/marked": "^4.0.8", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", + "@types/prop-types": "^15.7.15", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@vitejs/plugin-react": "5.2.0", @@ -125,11 +126,11 @@ "mockdate": "^3.0.2", "plop": "^3.1.1", "prettier": "3.6.2", - "react": "18.3.1", + "react": "19.2.7", "react-compiler-runtime": "1.0.0", - "react-dom": "18.3.1", "react-docgen-typescript": "2.4.0", - "react-is": "18.3.1", + "react-dom": "19.2.7", + "react-is": "19.2.7", "rimraf": "^3.0.2", "rollup": "2.79.2", "rollup-plugin-styles": "4.0.0", diff --git a/src/checkbox-field/checkbox-field.tsx b/src/checkbox-field/checkbox-field.tsx index 4b9dcb0cb..b4ced5b9e 100644 --- a/src/checkbox-field/checkbox-field.tsx +++ b/src/checkbox-field/checkbox-field.tsx @@ -8,6 +8,8 @@ import { useForkRef } from './use-fork-ref' import styles from './checkbox-field.module.css' +import type { JSX } from 'react' + interface CheckboxFieldProps extends Omit< JSX.IntrinsicElements['input'], diff --git a/src/components/color-picker/deprecated-dropdown/dropdown.tsx b/src/components/color-picker/deprecated-dropdown/dropdown.tsx index 768744de8..758ca95b8 100644 --- a/src/components/color-picker/deprecated-dropdown/dropdown.tsx +++ b/src/components/color-picker/deprecated-dropdown/dropdown.tsx @@ -6,6 +6,8 @@ import classNames from 'classnames' import Button from '../deprecated-button' +import type { JSX } from 'react' + type BoxProps = { onShowBody?: () => void onHideBody?: () => void diff --git a/src/icons/alert-icon.tsx b/src/icons/alert-icon.tsx index 0fef79350..1abcbd1b9 100644 --- a/src/icons/alert-icon.tsx +++ b/src/icons/alert-icon.tsx @@ -1,5 +1,6 @@ import * as React from 'react' +import type { JSX } from 'react' import type { AlertTone } from '../utils/common-types' const alertIconForTone: Record = { diff --git a/src/icons/banner-icon.tsx b/src/icons/banner-icon.tsx index f915c1d3a..e2cbb5c8b 100644 --- a/src/icons/banner-icon.tsx +++ b/src/icons/banner-icon.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import styles from './banner-icon.module.css' +import type { JSX } from 'react' import type { SystemBannerType } from '../banner/banner' const bannerIconForType: Record = { diff --git a/src/icons/close-icon.tsx b/src/icons/close-icon.tsx index bd416e116..300fd94eb 100644 --- a/src/icons/close-icon.tsx +++ b/src/icons/close-icon.tsx @@ -1,5 +1,7 @@ import * as React from 'react' +import type { JSX } from 'react' + function CloseIcon(props: JSX.IntrinsicElements['svg']) { return ( diff --git a/src/icons/password-hidden-icon.tsx b/src/icons/password-hidden-icon.tsx index 7651f3397..31482e92c 100644 --- a/src/icons/password-hidden-icon.tsx +++ b/src/icons/password-hidden-icon.tsx @@ -1,5 +1,7 @@ import * as React from 'react' +import type { JSX } from 'react' + function PasswordHiddenIcon(props: JSX.IntrinsicElements['svg']) { return ( diff --git a/src/icons/password-visible-icon.tsx b/src/icons/password-visible-icon.tsx index 93dce8294..5aac70ab4 100644 --- a/src/icons/password-visible-icon.tsx +++ b/src/icons/password-visible-icon.tsx @@ -1,5 +1,7 @@ import * as React from 'react' +import type { JSX } from 'react' + function PasswordVisibleIcon(props: JSX.IntrinsicElements['svg']) { return ( diff --git a/src/menu/menu.test.tsx b/src/menu/menu.test.tsx index 2eb8e8505..56eea40b6 100644 --- a/src/menu/menu.test.tsx +++ b/src/menu/menu.test.tsx @@ -286,8 +286,10 @@ describe('Menu', () => { expect(screen.getByRole('menuitem', { name: 'Save' })).toBeVisible() }) + // Select via keyboard: in React 19 the hover-opened submenu closes synchronously when the pointer moves onto an item, unmounting it before a click can land. + await user.keyboard('{ArrowRight}') await act(async () => { - await user.click(screen.getByRole('menuitem', { name: 'Save' })) + await user.keyboard('{Enter}') await flushMicrotasks() }) diff --git a/src/modal/modal-examples.stories.tsx b/src/modal/modal-examples.stories.tsx index 0136d8474..f90ab2bb7 100644 --- a/src/modal/modal-examples.stories.tsx +++ b/src/modal/modal-examples.stories.tsx @@ -29,6 +29,8 @@ import { ScrollableContent, } from './modal-stories-components' +import type { JSX } from 'react' + export default { title: '🪟 Overlays/Modal/Examples', component: ModalComponents.Modal, diff --git a/src/modal/modal-stories-components.tsx b/src/modal/modal-stories-components.tsx index 1745edf58..9c4b3678f 100644 --- a/src/modal/modal-stories-components.tsx +++ b/src/modal/modal-stories-components.tsx @@ -8,6 +8,7 @@ import { Placeholder, times } from '../utils/storybook-helper' import * as ModalComponents from './modal' +import type { JSX } from 'react' import type { ModalFooterProps, ModalHeaderProps, ModalProps } from './modal' function Link({ children, ...props }: JSX.IntrinsicElements['a']) { diff --git a/src/select-field/select-field.tsx b/src/select-field/select-field.tsx index 23335784b..3e1228ee1 100644 --- a/src/select-field/select-field.tsx +++ b/src/select-field/select-field.tsx @@ -5,6 +5,7 @@ import { Box } from '../box' import styles from './select-field.module.css' +import type { JSX } from 'react' import type { BaseFieldVariantProps, FieldComponentProps } from '../base-field' interface SelectFieldProps diff --git a/src/stack/stack.tsx b/src/stack/stack.tsx index dd9caded6..60f956e81 100644 --- a/src/stack/stack.tsx +++ b/src/stack/stack.tsx @@ -54,15 +54,18 @@ const Stack = polymorphicComponent<'div', StackProps>(function Stack( ref={ref} > {dividers !== 'none' - ? React.Children.map(flattenChildren(children), (child, index) => - index > 0 ? ( - <> - - {child} - - ) : ( - child - ), + ? React.Children.map( + // react-keyed-flatten-children returns ReactChild[], a type removed in React 19. + flattenChildren(children) as React.ReactNode[], + (child, index) => + index > 0 ? ( + <> + + {child} + + ) : ( + child + ), ) : children} diff --git a/src/text-area/text-area.tsx b/src/text-area/text-area.tsx index e9574f380..8ca50aab7 100644 --- a/src/text-area/text-area.tsx +++ b/src/text-area/text-area.tsx @@ -137,8 +137,8 @@ function useAutoExpand({ }: { value: string | undefined autoExpand: boolean - containerRef: React.RefObject - internalRef: React.RefObject + containerRef: React.RefObject + internalRef: React.RefObject }) { const isControlled = value !== undefined diff --git a/src/toast/toast.stories.tsx b/src/toast/toast.stories.tsx index 14f4b5862..7116c91ce 100644 --- a/src/toast/toast.stories.tsx +++ b/src/toast/toast.stories.tsx @@ -18,6 +18,7 @@ import { Text } from '../text' import { StaticToast } from './static-toast' import { Toast, ToastsProvider, useToasts } from './use-toasts' +import type { JSX } from 'react' import type { ButtonVariant } from '../button' import type { StaticToastProps } from './static-toast' diff --git a/src/toast/use-toasts.tsx b/src/toast/use-toasts.tsx index 6e412df27..b4a41ce4c 100644 --- a/src/toast/use-toasts.tsx +++ b/src/toast/use-toasts.tsx @@ -70,7 +70,7 @@ const InternalToast = React.forwardRef(funct }, ref, ) { - const timeoutRef = React.useRef() + const timeoutRef = React.useRef(undefined) const removeToast = React.useCallback( function removeToast() { diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx index 8a8058e22..dfd18a531 100644 --- a/src/tooltip/tooltip.tsx +++ b/src/tooltip/tooltip.tsx @@ -12,6 +12,7 @@ import { Box } from '../box' import styles from './tooltip.module.css' import type { TooltipStore, TooltipStoreState } from '@ariakit/react' +import type { JSX } from 'react' import type { ObfuscatedClassName } from '../utils/common-types' const defaultShowTimeout = 500 @@ -144,13 +145,15 @@ const Tooltip = React.forwardRef( } /* eslint-disable react-hooks/refs */ - if (typeof child.ref === 'string') { - throw new Error('Tooltip: String refs cannot be used as they cannot be forwarded') - } + const childRef = + 'ref' in child.props + ? (child.props.ref as React.Ref) + : // child.ref access kept for React 18 compatibility + (child as { ref?: React.Ref }).ref return ( <> - + {isOpen && content ? ( = Omit & P2 @@ -172,12 +172,24 @@ interface PolymorphicComponent< defaultProps?: Partial< PolymorphicComponentProps > - propTypes?: React.WeakValidationMap< + propTypes?: PropTypes.WeakValidationMap< PolymorphicComponentProps > displayName?: string } +/** The render function shape React 19's `forwardRef` expects (props wrapped in `PropsWithoutRef`). */ +type PolymorphicRenderFunction< + ComponentType extends React.ElementType, + OwnProps, + ShouldObfuscateClassName extends ObfuscateClassNameMode, +> = React.ForwardRefRenderFunction< + ElementByTagOrAny, + React.PropsWithoutRef< + PolymorphicComponentProps + > +> + /** * A wrapper to use React.forwardRef with polymorphic components in a type-safe manner. This is a * convenience over merely using React.forwardRef directly, and then manually forcing the resulting @@ -190,11 +202,9 @@ function polymorphicComponent< OwnProps = EmptyObject, ShouldObfuscateClassName extends ObfuscateClassNameMode = 'obfuscateClassName', >(render: ForwardRefFunction) { - return React.forwardRef(render) as PolymorphicComponent< - ComponentType, - OwnProps, - ShouldObfuscateClassName - > + return React.forwardRef( + render as PolymorphicRenderFunction, + ) as PolymorphicComponent } export type { PolymorphicComponent, PolymorphicComponentProps } diff --git a/src/utils/storybook-helper.tsx b/src/utils/storybook-helper.tsx index 3ab097b7a..4d4050d2d 100644 --- a/src/utils/storybook-helper.tsx +++ b/src/utils/storybook-helper.tsx @@ -6,6 +6,7 @@ import { Box } from '../box' import { Heading } from '../heading' import { Stack } from '../stack' +import type { JSX } from 'react' import type { BoxProps } from '../box' import type { Space } from './common-types' From 0a43ff67728da9bb57de3539d4c106cc8ca6bd7b Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Mon, 15 Jun 2026 22:19:05 -0700 Subject: [PATCH 3/9] test: add React 18 jest run Add aliased react-18 / react-dom-18 dev dependencies and a second Jest config that remaps react/react-dom to them, so the suite runs against React 18 from a single install. Wire test:react18 into validate (and therefore the pre-push hook), sharing the same committed snapshots. react-dom-18 peers on react ^18 while the top-level react is 19; an overrides entry points its react peer at the top-level version so npm resolves the tree (Jest remaps everything to the 18 aliases at run time regardless). Co-Authored-By: Claude Opus 4.8 (1M context) --- .eslintignore | 1 + jest.config.react18.js | 13 +++++++++++++ package-lock.json | 41 +++++++++++++++++++++++++++++++++++++++++ package.json | 11 +++++++++-- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 jest.config.react18.js diff --git a/.eslintignore b/.eslintignore index 889fc9e51..57bc5417d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ lint-staged.config.js jest.config.js +jest.config.react18.js babel.config.js plopfile.mjs release.config.js diff --git a/jest.config.react18.js b/jest.config.react18.js new file mode 100644 index 000000000..46c5f7701 --- /dev/null +++ b/jest.config.react18.js @@ -0,0 +1,13 @@ +const baseConfig = require('./jest.config') + +// Runs the Jest suite against React 18 by remapping react/react-dom to the aliased 18 packages. +module.exports = { + ...baseConfig, + moduleNameMapper: { + ...baseConfig.moduleNameMapper, + '^react$': 'react-18', + '^react/(.*)$': 'react-18/$1', + '^react-dom$': 'react-dom-18', + '^react-dom/(.*)$': 'react-dom-18/$1', + }, +} diff --git a/package-lock.json b/package-lock.json index 47974a483..21defb74b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,9 +82,11 @@ "plop": "^3.1.1", "prettier": "3.6.2", "react": "19.2.7", + "react-18": "npm:react@18.3.1", "react-compiler-runtime": "1.0.0", "react-docgen-typescript": "2.4.0", "react-dom": "19.2.7", + "react-dom-18": "npm:react-dom@18.3.1", "react-is": "19.2.7", "rimraf": "^3.0.2", "rollup": "2.79.2", @@ -22630,6 +22632,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-18": { + "name": "react", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-clientside-effect": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz", @@ -22710,6 +22726,31 @@ "react": "^19.2.7" } }, + "node_modules/react-dom-18": { + "name": "react-dom", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom-18/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/react-focus-lock": { "version": "2.13.7", "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.7.tgz", diff --git a/package.json b/package.json index 7ce7e09a5..fff16647d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "scripts": { "postinstall": "patch-package", "setup": "npm install && npm run validate", - "validate": "npm run lint && npm run type-check && npm run test", + "validate": "npm run lint && npm run type-check && npm run test && npm run test:react18", "start": "ON_SUCCESS=\"./scripts/organize-styles.sh\" rollup -c --watch --no-watch.clearScreen", "prestart:yalc": "npm run clean && yalc publish", "start:yalc": "ON_SUCCESS=\"npm run start:yalc:success\" rollup -c --watch --no-watch.clearScreen", @@ -49,6 +49,8 @@ "clean": "rimraf es lib styles dist", "test": "jest --passWithNoTests", "test:watch": "npm run test -- --watch", + "test:react18": "jest --config jest.config.react18.js", + "test:react18:watch": "npm run test:react18 -- --watch", "test:coverage": "npm run test -- --coverage", "type-check": "tsc --noEmit -p ./tsconfig.json", "lint": "eslint --format codeframe --cache --ext js,jsx,ts,tsx ./", @@ -127,9 +129,11 @@ "plop": "^3.1.1", "prettier": "3.6.2", "react": "19.2.7", + "react-18": "npm:react@18.3.1", "react-compiler-runtime": "1.0.0", "react-docgen-typescript": "2.4.0", "react-dom": "19.2.7", + "react-dom-18": "npm:react-dom@18.3.1", "react-is": "19.2.7", "rimraf": "^3.0.2", "rollup": "2.79.2", @@ -155,6 +159,9 @@ "pretty-format": { "react-is": "$react-is" }, - "react-element-to-jsx-string": "15.0.0" + "react-element-to-jsx-string": "15.0.0", + "react-dom-18": { + "react": "$react" + } } } From e01c8cf594992a0accf422632792b808578cb2bc Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Mon, 15 Jun 2026 22:44:56 -0700 Subject: [PATCH 4/9] ci: run tests against React 18 and 19 Add a second unit-testing step running npm run test:react18 off the same install, so CI exercises the suite under both React versions. Two sequential steps on the single runner; a strategy matrix is unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/check-ci-validation.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-ci-validation.yml b/.github/workflows/check-ci-validation.yml index ccd6713b2..112dfe298 100644 --- a/.github/workflows/check-ci-validation.yml +++ b/.github/workflows/check-ci-validation.yml @@ -129,10 +129,14 @@ jobs: run: | npm ci - - name: Test codebase correctness + - name: Test codebase correctness (React 19) run: | npm run test + - name: Test codebase correctness (React 18) + run: | + npm run test:react18 + build-package: name: Build Package runs-on: ubuntu-latest From e51708e8b9ce0fa17e33925e5052631d6b151e8d Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Mon, 15 Jun 2026 22:47:03 -0700 Subject: [PATCH 5/9] chore: remove prop-types usage React 19 ignores propTypes, so drop the only two uses: the import + propTypes block in ThreeDotsIcon and the propTypes member in the deprecated polymorphism types. Remove prop-types from peerDependencies and the @types/prop-types dev dependency (added in the React 19 bump only to type the now-deleted usage). Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 9 --------- package.json | 2 -- src/components/icons/ThreeDotsIcon.svg.tsx | 6 ------ src/utils/polymorphism.ts | 4 ---- 4 files changed, 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21defb74b..84e90365a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "@types/jest": "28.1.8", "@types/jest-axe": "^3.5.3", "@types/marked": "^4.0.8", - "@types/prop-types": "^15.7.15", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.46.2", @@ -101,7 +100,6 @@ "peerDependencies": { "@ariakit/react": "~0.4.19", "classnames": "^2.2.5", - "prop-types": "^15.6.2", "react": ">=18.0.0 <20.0.0", "react-compiler-runtime": "^1.0.0", "react-dom": ">=18.0.0 <20.0.0" @@ -7349,13 +7347,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", diff --git a/package.json b/package.json index fff16647d..34e2c1a8e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "peerDependencies": { "@ariakit/react": "~0.4.19", "classnames": "^2.2.5", - "prop-types": "^15.6.2", "react": ">=18.0.0 <20.0.0", "react-compiler-runtime": "^1.0.0", "react-dom": ">=18.0.0 <20.0.0" @@ -97,7 +96,6 @@ "@types/jest": "28.1.8", "@types/jest-axe": "^3.5.3", "@types/marked": "^4.0.8", - "@types/prop-types": "^15.7.15", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.46.2", diff --git a/src/components/icons/ThreeDotsIcon.svg.tsx b/src/components/icons/ThreeDotsIcon.svg.tsx index bb0eea95e..0290b1aee 100644 --- a/src/components/icons/ThreeDotsIcon.svg.tsx +++ b/src/components/icons/ThreeDotsIcon.svg.tsx @@ -1,7 +1,5 @@ import * as React from 'react' -import PropTypes from 'prop-types' - const ThreeDotsIcon = ({ color = '#A5A5A5' }) => ( @@ -11,9 +9,5 @@ const ThreeDotsIcon = ({ color = '#A5A5A5' }) => ( ) -ThreeDotsIcon.propTypes = { - /** Color of the icon. */ - color: PropTypes.string, -} export default ThreeDotsIcon diff --git a/src/utils/polymorphism.ts b/src/utils/polymorphism.ts index ec171acc8..c233b3d89 100644 --- a/src/utils/polymorphism.ts +++ b/src/utils/polymorphism.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as React from 'react' -import type * as PropTypes from 'prop-types' import type { ObfuscatedClassName } from './common-types' type Merge = Omit & P2 @@ -172,9 +171,6 @@ interface PolymorphicComponent< defaultProps?: Partial< PolymorphicComponentProps > - propTypes?: PropTypes.WeakValidationMap< - PolymorphicComponentProps - > displayName?: string } From 06f5c402fd2f21c14284268f15df7a2ba52b0d7c Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Tue, 16 Jun 2026 13:17:57 -0700 Subject: [PATCH 6/9] refactor: drop the ref-masking cast in polymorphicComponent PolymorphicComponentProps is built on React.ComponentProps, which includes ref under React 19. A forwardRef render never receives ref in props, so move the strip into the ForwardRefFunction contract via PropsWithoutRef and ForwardedRef, dropping the cast and the now-unused PolymorphicRenderFunction. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/utils/polymorphism.ts | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/utils/polymorphism.ts b/src/utils/polymorphism.ts index c233b3d89..0422bf11d 100644 --- a/src/utils/polymorphism.ts +++ b/src/utils/polymorphism.ts @@ -103,11 +103,10 @@ interface ForwardRefFunction< ShouldObfuscateClassName extends ObfuscateClassNameMode, > { ( - props: PolymorphicComponentProps, - ref: - | ((instance: ElementByTagOrAny | null) => void) - | React.MutableRefObject | null> - | null, + props: React.PropsWithoutRef< + PolymorphicComponentProps + >, + ref: React.ForwardedRef>, ): React.ReactElement | null displayName?: string } @@ -174,18 +173,6 @@ interface PolymorphicComponent< displayName?: string } -/** The render function shape React 19's `forwardRef` expects (props wrapped in `PropsWithoutRef`). */ -type PolymorphicRenderFunction< - ComponentType extends React.ElementType, - OwnProps, - ShouldObfuscateClassName extends ObfuscateClassNameMode, -> = React.ForwardRefRenderFunction< - ElementByTagOrAny, - React.PropsWithoutRef< - PolymorphicComponentProps - > -> - /** * A wrapper to use React.forwardRef with polymorphic components in a type-safe manner. This is a * convenience over merely using React.forwardRef directly, and then manually forcing the resulting @@ -198,9 +185,11 @@ function polymorphicComponent< OwnProps = EmptyObject, ShouldObfuscateClassName extends ObfuscateClassNameMode = 'obfuscateClassName', >(render: ForwardRefFunction) { - return React.forwardRef( - render as PolymorphicRenderFunction, - ) as PolymorphicComponent + return React.forwardRef(render) as PolymorphicComponent< + ComponentType, + OwnProps, + ShouldObfuscateClassName + > } export type { PolymorphicComponent, PolymorphicComponentProps } From a79132d149a398b4bc254c26523a2e3249eb2516 Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Tue, 16 Jun 2026 13:18:15 -0700 Subject: [PATCH 7/9] refactor: type ThreeDotsIcon's color prop Extract a named ThreeDotsIconProps type and switch to a function declaration so the color prop is explicitly typed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/icons/ThreeDotsIcon.svg.tsx | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/icons/ThreeDotsIcon.svg.tsx b/src/components/icons/ThreeDotsIcon.svg.tsx index 0290b1aee..4246a3e88 100644 --- a/src/components/icons/ThreeDotsIcon.svg.tsx +++ b/src/components/icons/ThreeDotsIcon.svg.tsx @@ -1,13 +1,19 @@ import * as React from 'react' -const ThreeDotsIcon = ({ color = '#A5A5A5' }) => ( - - - - - - - -) +type ThreeDotsIconProps = { + color?: string +} + +function ThreeDotsIcon({ color = '#A5A5A5' }: ThreeDotsIconProps) { + return ( + + + + + + + + ) +} export default ThreeDotsIcon From e4a2f56e15fe6ecd6c1a5de5493d170b5c63bb50 Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Tue, 16 Jun 2026 13:18:33 -0700 Subject: [PATCH 8/9] test: document the keyboard-driven submenu selection Explain why the submenu test drives selection via keyboard: jsdom can't give Ariakit the DOMRects it needs to keep the hovercard open, and React 19 flushes the close render before a click can land. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/menu/menu.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/menu/menu.test.tsx b/src/menu/menu.test.tsx index 56eea40b6..3ff2b166b 100644 --- a/src/menu/menu.test.tsx +++ b/src/menu/menu.test.tsx @@ -286,7 +286,13 @@ describe('Menu', () => { expect(screen.getByRole('menuitem', { name: 'Save' })).toBeVisible() }) - // Select via keyboard: in React 19 the hover-opened submenu closes synchronously when the pointer moves onto an item, unmounting it before a click can land. + // Without providing the correct DOMRects, Ariakit can't compute the geometry needed to + // determine whether the hovercard should stay open ([ref](https://github.com/ariakit/ariakit/blob/200aa56676b8373fe10ec6c5fac61003ae82c1de/packages/ariakit-react-core/src/hovercard/hovercard.tsx#L201-L222)), + // so it schedules for it to be closed. In React 18, we used to be able to assert that + // clicks happen on the menu before the closed menu renders, but in React 19, the render + // happens before that ([ref](https://github.com/facebook/react/pull/26512)). + // Since we don't want to test Ariakit's internals, we fall back to testing keyboard + // navigation to bypass this. await user.keyboard('{ArrowRight}') await act(async () => { await user.keyboard('{Enter}') From 17c8ba2c03c29c7c2af7447b1440ff83f5fbeb78 Mon Sep 17 00:00:00 2001 From: Frankie Yan Date: Tue, 16 Jun 2026 13:21:04 -0700 Subject: [PATCH 9/9] chore: type-check against React 18 typings Add a tsconfig.react18.json that remaps react/react-dom to aliased @types/react@18 and @types/react-dom@18, wired into a type-check:react18 script, the validate chain, and CI. This catches typings regressions against the older React the library still supports. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/check-ci-validation.yml | 4 +++ package-lock.json | 32 +++++++++++++++++++++++ package.json | 8 +++++- tsconfig.react18.json | 17 ++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tsconfig.react18.json diff --git a/.github/workflows/check-ci-validation.yml b/.github/workflows/check-ci-validation.yml index 112dfe298..a9a04d637 100644 --- a/.github/workflows/check-ci-validation.yml +++ b/.github/workflows/check-ci-validation.yml @@ -88,6 +88,10 @@ jobs: run: | npm run type-check + - name: Perform type checking with TypeScript (React 18 typings) + run: | + npm run type-check:react18 + - name: Check React Compiler compatibility if: ${{ github.event_name == 'pull_request' }} run: | diff --git a/package-lock.json b/package-lock.json index 84e90365a..7625eb089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,9 @@ "@types/jest-axe": "^3.5.3", "@types/marked": "^4.0.8", "@types/react": "19.2.17", + "@types/react-18": "npm:@types/react@^18.3", "@types/react-dom": "19.2.3", + "@types/react-dom-18": "npm:@types/react-dom@^18.3", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@vitejs/plugin-react": "5.2.0", @@ -7347,6 +7349,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -7356,6 +7365,18 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/react-18": { + "name": "@types/react", + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, "node_modules/@types/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", @@ -7366,6 +7387,17 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-dom-18": { + "name": "@types/react-dom", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", diff --git a/package.json b/package.json index 34e2c1a8e..6712eecf9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "scripts": { "postinstall": "patch-package", "setup": "npm install && npm run validate", - "validate": "npm run lint && npm run type-check && npm run test && npm run test:react18", + "validate": "npm run lint && npm run type-check && npm run type-check:react18 && npm run test && npm run test:react18", "start": "ON_SUCCESS=\"./scripts/organize-styles.sh\" rollup -c --watch --no-watch.clearScreen", "prestart:yalc": "npm run clean && yalc publish", "start:yalc": "ON_SUCCESS=\"npm run start:yalc:success\" rollup -c --watch --no-watch.clearScreen", @@ -53,6 +53,7 @@ "test:react18:watch": "npm run test:react18 -- --watch", "test:coverage": "npm run test -- --coverage", "type-check": "tsc --noEmit -p ./tsconfig.json", + "type-check:react18": "tsc --noEmit -p ./tsconfig.react18.json", "lint": "eslint --format codeframe --cache --ext js,jsx,ts,tsx ./", "storybook": "storybook dev -p 6006", "prettify": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,css,scss,less,md,mdx}\"", @@ -97,7 +98,9 @@ "@types/jest-axe": "^3.5.3", "@types/marked": "^4.0.8", "@types/react": "19.2.17", + "@types/react-18": "npm:@types/react@^18.3", "@types/react-dom": "19.2.3", + "@types/react-dom-18": "npm:@types/react-dom@^18.3", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@vitejs/plugin-react": "5.2.0", @@ -160,6 +163,9 @@ "react-element-to-jsx-string": "15.0.0", "react-dom-18": { "react": "$react" + }, + "@types/react-dom-18": { + "@types/react": "$@types/react" } } } diff --git a/tsconfig.react18.json b/tsconfig.react18.json new file mode 100644 index 000000000..9594bbc45 --- /dev/null +++ b/tsconfig.react18.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@": ["./"], + "@storybook/react": ["node_modules/@storybook/react/dist/index.d.ts"], + "@storybook/react-vite": ["node_modules/@storybook/react-vite/dist/index.d.ts"], + "storybook/actions": ["node_modules/storybook/dist/actions/index.d.ts"], + "storybook/test": ["node_modules/storybook/dist/test/index.d.ts"], + "react": ["node_modules/@types/react-18"], + "react/*": ["node_modules/@types/react-18/*"], + "react-dom": ["node_modules/@types/react-dom-18"], + "react-dom/*": ["node_modules/@types/react-dom-18/*"], + "*": ["src/*", "node_modules/*"] + } + } +}