From f90283866fc64c324b7cc844aed381fa10223c11 Mon Sep 17 00:00:00 2001 From: Andy Edwards Date: Tue, 12 Feb 2019 22:47:10 -0600 Subject: [PATCH] feat: add React Hooks API --- README.md | 315 +++++++++++++++++++++++++++++++++++++++------ package.json | 7 +- src/hooks.js | 187 +++++++++++++++++++++++++++ src/index.js | 10 +- test/hooks.test.js | 190 +++++++++++++++++++++++++++ yarn.lock | 28 ++-- 6 files changed, 681 insertions(+), 56 deletions(-) create mode 100644 src/hooks.js create mode 100644 test/hooks.test.js diff --git a/README.md b/README.md index a0e20cb..10c2bc7 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,38 @@ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![npm version](https://badge.fury.io/js/material-ui-popup-state.svg)](https://badge.fury.io/js/material-ui-popup-state) -PopupState takes care of the boilerplate for common Menu, Popover and Popper use cases. +Takes care of the boilerplate for common Menu, Popover and Popper use cases. -It is a [render props component](https://reactjs.org/docs/render-props.html) that +Provides a [Custom React Hook](https://reactjs.org/docs/hooks-custom.html) that keeps track of the local state for a single popup, and functions to connect trigger, toggle, and +popover/menu/popper components to the state. + +Also provides a [Render Props Component](https://reactjs.org/docs/render-props.html) that keeps track of the local state for a single popup, and passes the state and mutation functions to a child render function. # Table of Contents + + + - [Installation](#installation) - [Examples](#examples) - * [Menu Example](#menu-example) - * [Popover Example](#popover-example) - * [Mouse Over Interaction](#mouse-over-interaction) - * [Popper](#popper) + - [Menu](#menu) + - [Popover](#popover) + - [Mouse Over Interaction](#mouse-over-interaction) + - [Popper](#popper) +- [Examples with React Hooks](#examples-with-react-hooks) + - [Menu](#menu-1) + - [Popover](#popover-1) + - [Popper](#popper-1) - [API](#api) - * [Bind Functions](#bind-functions) - * [`PopupState` Props](#popupstate-props) - + [`variant` (`'popover'` or `'popper'`, **required**)](#variant-popover-or-popper-required) - + [`popupId` (`string`, **optional** but strongly encouraged)](#popupid-string-optional-but-strongly-encouraged) - + [`children` (`(popupState: InjectedProps) => ?React.Node`, **required**)](#children-popupstate-injectedprops--reactnode-required) + - [Bind Functions](#bind-functions) + - [`PopupState` Props](#popupstate-props) +- [React Hooks API](#react-hooks-api) + - [Bind Functions](#bind-functions-1) + - [`usePopupState` Props](#usepopupstate-props) + - [`usePopupState` return value](#usepopupstate-return-value) + + # Installation @@ -34,7 +47,7 @@ npm install --save material-ui-popup-state # Examples -## Menu Example +## Menu ```js import * as React from 'react' @@ -55,14 +68,15 @@ const MenuPopupState = () => ( Death - )}} + )} + } ) export default MenuPopupState ``` -## Popover Example +## Popover ```js import React from 'react' @@ -97,7 +111,9 @@ const PopoverPopupState = ({ classes }) => ( horizontal: 'center', }} > - The content of the Popover. + + The content of the Popover. + )} @@ -134,7 +150,9 @@ const HoverPopoverPopupState = ({ classes }) => ( {popupState => (
- Hover with a Popover. + + Hover with a Popover. + ( {({ TransitionProps }) => ( - The content of the Popper. + + The content of the Popper. + )} @@ -212,19 +232,157 @@ PopperPopupState.propTypes = { export default withStyles(styles)(PopperPopupState) ``` +# Examples with React Hooks + +## Menu + +```js +import * as React from 'react' +import Button from '@material-ui/core/Button' +import Menu from '@material-ui/core/Menu' +import MenuItem from '@material-ui/core/MenuItem' +import { usePopupState, bindTrigger, bindMenu } from 'material-ui-popup-state/hooks' + +const MenuPopupState = () => { + const popupState = usePopupState({variant: 'popover', popupId: 'demoMenu'}) + return ( +
+ + + Cake + Death + +
+ ) +) + +export default MenuPopupState +``` + +## Popover + +```js +import React from 'react' +import PropTypes from 'prop-types' +import { withStyles } from '@material-ui/core/styles' +import Typography from '@material-ui/core/Typography' +import Button from '@material-ui/core/Button' +import Popover from '@material-ui/core/Popover' +import { + usePopupState, + bindTrigger, + bindPopover, +} from 'material-ui-popup-state/hooks' + +const styles = theme => ({ + typography: { + margin: theme.spacing.unit * 2, + }, +}) + +const PopoverPopupState = ({ classes }) => { + const popupState = usePopupState({ + variant: 'popover', + popupId: 'demoPopover', + }) + return ( +
+ + + + The content of the Popover. + + +
+ ) +} + +PopoverPopupState.propTypes = { + classes: PropTypes.object.isRequired, +} + +export default withStyles(styles)(PopoverPopupState) +``` + +## Popper + +```js +import React from 'react' +import PropTypes from 'prop-types' +import { withStyles } from '@material-ui/core/styles' +import Typography from '@material-ui/core/Typography' +import Button from '@material-ui/core/Button' +import Popper from '@material-ui/core/Popper' +import { + usePopupState, + bindToggle, + bindPopper, +} from 'material-ui-popup-state/hooks' +import Fade from '@material-ui/core/Fade' +import Paper from '@material-ui/core/Paper' + +const styles = theme => ({ + typography: { + padding: theme.spacing.unit * 2, + }, +}) + +const PopperPopupState = ({ classes }) => { + const popupState = usePopupState({ variant: 'popper', popupId: 'demoPopper' }) + return ( +
+ + + {({ TransitionProps }) => ( + + + + The content of the Popper. + + + + )} + +
+ ) +} + +PopperPopupState.propTypes = { + classes: PropTypes.object.isRequired, +} + +export default withStyles(styles)(PopperPopupState) +``` + # API ## Bind Functions -`@material-ui/core/PopupState` exports several helper functions you can use to +`material-ui-popup-state` exports several helper functions you can use to connect components easily: -* `bindMenu`: creates props to control a `Menu` component. -* `bindPopover`: creates props to control a `Popover` component. -* `bindPopper`: creates props to control a `Popper` component. -* `bindTrigger`: creates props for a component that opens the popup when clicked. -* `bindToggle`: creates props for a component that toggles the popup when clicked. -* `bindHover`: creates props for a component that opens the popup while hovered. +- `bindMenu`: creates props to control a `Menu` component. +- `bindPopover`: creates props to control a `Popover` component. +- `bindPopper`: creates props to control a `Popper` component. +- `bindTrigger`: creates props for a component that opens the popup when clicked. +- `bindToggle`: creates props for a component that toggles the popup when clicked. +- `bindHover`: creates props for a component that opens the popup while hovered. To use one of these functions, you should call it with the props `PopupState` passed to your child function, and spread the return value into the desired @@ -235,10 +393,10 @@ import * as React from 'react' import Button from '@material-ui/core/Button' import Menu from '@material-ui/core/Menu' import MenuItem from '@material-ui/core/MenuItem' -import PopupState, { bindTrigger, bindMenu } from '@material-ui/core/PopupState' +import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state' const MenuPopupState = () => ( - + {popupState => ( + + Cake + Death + +
+ ) +} + +export default MenuPopupState +``` + +## `usePopupState` + +This is a [Custom Hook](https://reactjs.org/docs/hooks-custom.html) that uses `useState` internally, therefore the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) apply to `usePopupState`. + +## `usePopupState` Props + +### `variant` (`'popover'` or `'popper'`, **required**) + +Use `'popover'` if your popup is a `Popover` or `Menu`; use `'popper'` if your +popup is a `Popper`. + +Right now this only affects whether `bindTrigger`/`bindToggle`/`bindHover` return +an `aria-owns` prop or an `aria-describedby` prop. + +### `popupId` (`string`, **optional** but strongly encouraged) + +The `id` for the popup component. It will be passed to the child props so that +the trigger component may declare the same id in an ARIA prop. + +## `usePopupState` return value + +An object with the following properties: + +- `open(eventOrAnchorEl)`: opens the popup +- `close()`: closes the popup +- `toggle(eventOrAnchorEl)`: opens the popup if it is closed, or +- closes the popup if it is open. +- `setOpen(open, [eventOrAnchorEl])`: sets whether the popup is open. +- `eventOrAnchorEl` is required if `open` is truthy. +- `isOpen`: `true`/`false` if the popup is open/closed +- `anchorEl`: the current anchor element (`null` when the popup is closed) +- `popupId`: the `popupId` prop you passed to `PopupState` +- `variant`: the `variant` prop you passed to `PopupState` diff --git a/package.json b/package.json index 8e26940..aafa853 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "menu", "popover", "boilerplate", - "render-props" + "render-props", + "react-hooks" ], "author": "Andy Edwards", "license": "MIT", @@ -119,8 +120,8 @@ "nyc": "^13.1.0", "prettier": "^1.15.2", "prettier-eslint": "^8.8.2", - "react": "^16.6.3", - "react-dom": "^16.6.3", + "react": "^16.8.1", + "react-dom": "^16.8.1", "rimraf": "^2.6.0", "semantic-release": "^15.1.4", "sinon": "^6.1.4", diff --git a/src/hooks.js b/src/hooks.js new file mode 100644 index 0000000..4d59697 --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,187 @@ +// @flow + +import * as React from 'react' + +if (!React.useState) { + throw new Error( + `React.useState (added in 16.8.0) must be defined to use the hooks API` + ) +} + +export type Variant = 'popover' | 'popper' + +export type PopupState = { + open: (eventOrAnchorEl: SyntheticEvent | HTMLElement) => void, + close: () => void, + toggle: (eventOrAnchorEl: SyntheticEvent | HTMLElement) => void, + setOpen: ( + open: boolean, + eventOrAnchorEl?: SyntheticEvent | HTMLElement + ) => void, + isOpen: boolean, + anchorEl: ?HTMLElement, + popupId: ?string, + variant: Variant, +} + +let eventOrAnchorElWarned: boolean = false + +export function usePopupState({ + popupId, + variant, +}: { + popupId: ?string, + variant: Variant, +}): PopupState { + const [anchorEl, setAnchorEl] = React.useState(null) + + const toggle = (eventOrAnchorEl: SyntheticEvent | HTMLElement) => { + if (anchorEl) close() + else open(eventOrAnchorEl) + } + + const open = (eventOrAnchorEl: SyntheticEvent | HTMLElement) => { + if (!eventOrAnchorElWarned && !eventOrAnchorEl) { + eventOrAnchorElWarned = true + console.error('eventOrAnchorEl should be defined') // eslint-disable-line no-console + } + setAnchorEl( + eventOrAnchorEl && eventOrAnchorEl.currentTarget + ? (eventOrAnchorEl.currentTarget: any) + : (eventOrAnchorEl: any) + ) + } + + const close = () => setAnchorEl(null) + + const setOpen = ( + nextOpen: boolean, + eventOrAnchorEl?: SyntheticEvent | HTMLElement + ) => { + if (nextOpen) { + if (!eventOrAnchorEl) { + throw new Error('eventOrAnchorEl must be defined when opening') + } + open(eventOrAnchorEl) + } else close() + } + + return { + anchorEl, + setAnchorEl, + popupId, + variant, + isOpen: anchorEl != null, + open, + close, + toggle, + setOpen, + } +} + +/** + * Creates props for a component that opens the popup when clicked. + * + * @param {object} popupState the argument passed to the child function of + * `PopupState` + */ +export function bindTrigger({ + isOpen, + open, + popupId, + variant, +}: PopupState): { + 'aria-owns'?: ?string, + 'aria-describedby'?: ?string, + 'aria-haspopup': true, + onClick: (event: SyntheticEvent) => void, +} { + return { + [variant === 'popover' ? 'aria-owns' : 'aria-describedby']: isOpen + ? popupId + : null, + 'aria-haspopup': true, + onClick: open, + } +} + +/** + * Creates props for a component that toggles the popup when clicked. + * + * @param {object} popupState the argument passed to the child function of + * `PopupState` + */ +export function bindToggle({ + isOpen, + toggle, + popupId, + variant, +}: PopupState): { + 'aria-owns'?: ?string, + 'aria-describedby'?: ?string, + 'aria-haspopup': true, + onClick: (event: SyntheticEvent) => void, +} { + return { + [variant === 'popover' ? 'aria-owns' : 'aria-describedby']: isOpen + ? popupId + : null, + 'aria-haspopup': true, + onClick: toggle, + } +} + +/** + * Creates props for a `Popover` component. + * + * @param {object} popupState the argument passed to the child function of + * `PopupState` + */ +export function bindPopover({ + isOpen, + anchorEl, + close, + popupId, +}: PopupState): { + id: ?string, + anchorEl: ?HTMLElement, + open: boolean, + onClose: () => void, +} { + return { + id: popupId, + anchorEl, + open: isOpen, + onClose: close, + } +} + +/** + * Creates props for a `Menu` component. + * + * @param {object} popupState the argument passed to the child function of + * `PopupState` + */ +export const bindMenu = bindPopover + +/** + * Creates props for a `Popper` component. + * + * @param {object} popupState the argument passed to the child function of + * `PopupState` + */ +export function bindPopper({ + isOpen, + anchorEl, + popupId, +}: PopupState): { + id: ?string, + anchorEl: ?HTMLElement, + open: boolean, +} { + return { + id: popupId, + anchorEl, + open: isOpen, + } +} diff --git a/src/index.js b/src/index.js index 642e8ae..47fa72b 100644 --- a/src/index.js +++ b/src/index.js @@ -243,7 +243,15 @@ export default class PopupState extends React.Component { popupId && typeof document !== 'undefined' ? document.getElementById(popupId) // eslint-disable-line no-undef : null - const { relatedTarget } = (event: any) + let relatedTarget: any = (event: any).relatedTarget + if ( + relatedTarget && + typeof document !== 'undefined' && + relatedTarget.parentElement === document.body // eslint-disable-line no-undef + ) { + const { childNodes } = relatedTarget + if (childNodes.length) relatedTarget = childNodes[childNodes.length - 1] + } if ( hovered && !isAncestor(popup, relatedTarget) && diff --git a/test/hooks.test.js b/test/hooks.test.js new file mode 100644 index 0000000..6803cfd --- /dev/null +++ b/test/hooks.test.js @@ -0,0 +1,190 @@ +// @flow + +import * as React from 'react' +import { assert } from 'chai' +import createMount from './utils/createMount' +import Button from '@material-ui/core/Button' +import Popper from '@material-ui/core/Popper' +import Menu from '@material-ui/core/Menu' +import MenuItem from '@material-ui/core/MenuItem' +import { + usePopupState, + bindMenu, + bindPopper, + bindTrigger, + bindToggle, +} from '../src/hooks' + +/* eslint-disable react/jsx-handler-names */ + +describe('usePopupState', () => { + let mount + + before(() => { + mount = createMount() + }) + + after(() => { + mount.cleanUp() + }) + + describe('bindMenu/bindTrigger', () => { + let buttonRef + let button + let menu + + const popupStates = [] + + beforeEach(() => (popupStates.length = 0)) + + const MenuTest = () => { + const popupState = usePopupState({ popupId: 'menu', variant: 'popover' }) + popupStates.push(popupState) + return ( + + + + Test + + + ) + } + + it('passes correct props to bindTrigger/bindMenu', () => { + const wrapper = mount() + button = wrapper.find(Button) + menu = wrapper.find(Menu) + assert.strictEqual(popupStates[0].isOpen, false) + assert.strictEqual(button.prop('aria-owns'), null) + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onClick'), popupStates[0].open) + assert.strictEqual(menu.prop('id'), 'menu') + assert.strictEqual(menu.prop('anchorEl'), null) + assert.strictEqual(menu.prop('open'), false) + assert.strictEqual(menu.prop('onClose'), popupStates[0].close) + + button.simulate('click') + wrapper.update() + button = wrapper.find(Button) + menu = wrapper.find(Menu) + assert.strictEqual(popupStates[1].isOpen, true) + assert.strictEqual(button.prop('aria-owns'), 'menu') + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onClick'), popupStates[1].open) + assert.strictEqual(menu.prop('id'), 'menu') + assert.strictEqual(menu.prop('anchorEl'), buttonRef) + assert.strictEqual(menu.prop('open'), true) + assert.strictEqual(menu.prop('onClose'), popupStates[1].close) + + wrapper.find(MenuItem).simulate('click') + wrapper.update() + button = wrapper.find(Button) + menu = wrapper.find(Menu) + assert.strictEqual(popupStates[2].isOpen, false) + assert.strictEqual(button.prop('aria-owns'), null) + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onClick'), popupStates[2].open) + assert.strictEqual(menu.prop('id'), 'menu') + assert.strictEqual(menu.prop('anchorEl'), null) + assert.strictEqual(menu.prop('open'), false) + assert.strictEqual(menu.prop('onClose'), popupStates[2].close) + }) + it('open/close works', () => { + const wrapper = mount() + + popupStates[0].open(buttonRef) + wrapper.update() + assert.strictEqual(popupStates[1].isOpen, true) + + popupStates[1].close() + wrapper.update() + assert.strictEqual(popupStates[2].isOpen, false) + }) + it('toggle works', () => { + const wrapper = mount() + + popupStates[0].toggle(buttonRef) + wrapper.update() + assert.strictEqual(popupStates[1].isOpen, true) + + popupStates[1].toggle(buttonRef) + wrapper.update() + assert.strictEqual(popupStates[2].isOpen, false) + }) + it('setOpen works', () => { + const wrapper = mount() + + popupStates[0].setOpen(true, buttonRef) + wrapper.update() + assert.strictEqual(popupStates[1].isOpen, true) + + popupStates[1].setOpen(false) + wrapper.update() + assert.strictEqual(popupStates[2].isOpen, false) + }) + }) + describe('bindToggle/bindPopper', () => { + let buttonRef + let button + let popper + + const popupStates = [] + + beforeEach(() => (popupStates.length = 0)) + + const PopperTest = () => { + const popupState = usePopupState({ popupId: 'popper', variant: 'popper' }) + popupStates.push(popupState) + return ( + + + The popper content + + ) + } + + it('passes correct props to bindToggle/bindPopper', () => { + const wrapper = mount() + button = wrapper.find(Button) + popper = wrapper.find(Popper) + assert.strictEqual(popupStates[0].isOpen, false) + assert.strictEqual(button.prop('aria-describedby'), null) + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onClick'), popupStates[0].toggle) + assert.strictEqual(popper.prop('id'), 'popper') + assert.strictEqual(popper.prop('anchorEl'), null) + assert.strictEqual(popper.prop('open'), false) + assert.strictEqual(popper.prop('onClose'), undefined) + + button.simulate('click') + wrapper.update() + button = wrapper.find(Button) + popper = wrapper.find(Popper) + assert.strictEqual(popupStates[1].isOpen, true) + assert.strictEqual(button.prop('aria-describedby'), 'popper') + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onClick'), popupStates[1].toggle) + assert.strictEqual(popper.prop('id'), 'popper') + assert.strictEqual(popper.prop('anchorEl'), buttonRef) + assert.strictEqual(popper.prop('open'), true) + assert.strictEqual(popper.prop('onClose'), undefined) + + button.simulate('click') + wrapper.update() + button = wrapper.find(Button) + popper = wrapper.find(Popper) + assert.strictEqual(popupStates[2].isOpen, false) + assert.strictEqual(button.prop('aria-describedby'), null) + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onClick'), popupStates[2].toggle) + assert.strictEqual(popper.prop('id'), 'popper') + assert.strictEqual(popper.prop('anchorEl'), null) + assert.strictEqual(popper.prop('open'), false) + assert.strictEqual(popper.prop('onClose'), undefined) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index a825aa1..666c332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7309,15 +7309,15 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.6.3: - version "16.6.3" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0" - integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ== +react-dom@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.1.tgz#ec860f98853d09d39bafd3a6f1e12389d283dbb4" + integrity sha512-N74IZUrPt6UiDjXaO7UbDDFXeUXnVhZzeRLy/6iqqN1ipfjrhR60Bp5NuBK+rv3GMdqdIuwIl22u1SYwf330bg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.11.2" + scheduler "^0.13.1" react-event-listener@^0.6.2: version "0.6.2" @@ -7359,15 +7359,15 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react@^16.6.3: - version "16.6.3" - resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c" - integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw== +react@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a" + integrity sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.11.2" + scheduler "^0.13.1" read-cmd-shim@^1.0.1, read-cmd-shim@~1.0.1: version "1.0.1" @@ -7875,10 +7875,10 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.11.2: - version "0.11.3" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b" - integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ== +scheduler@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.1.tgz#1a217df1bfaabaf4f1b92a9127d5d732d85a9591" + integrity sha512-VJKOkiKIN2/6NOoexuypwSrybx13MY7NSy9RNt8wPvZDMRT1CW6qlpF5jXRToXNHz3uWzbm2elNpZfXfGPqP9A== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1"