diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f8e2d0f6..160ee25e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # @baseapp-frontend/components +## 1.3.0 + +### Minor Changes + +- Implements native version for `useRoomListSubscription` hook to subscribe and unsubscribe based on the application's state — e.g., when the app goes inactive and then returns to the foreground + ## 1.2.9 ### Patch Changes diff --git a/packages/components/modules/messages/common/graphql/subscriptions/useRoomListSubscription.tsx b/packages/components/modules/messages/common/graphql/subscriptions/useRoomListSubscription.tsx index ab80f54b..890c338e 100644 --- a/packages/components/modules/messages/common/graphql/subscriptions/useRoomListSubscription.tsx +++ b/packages/components/modules/messages/common/graphql/subscriptions/useRoomListSubscription.tsx @@ -38,6 +38,58 @@ export const RoomListSubscriptionQuery = graphql` } ` +export const wasRemovedFromChatRoom = ( + data: useRoomListSubscription$data | null | undefined, + profileId: string, +) => + data?.chatRoomOnRoomUpdate?.removedParticipants?.some((node) => node?.profile?.id === profileId) + +export const getRoomListSubscriptionConfig = ( + profileId: string, + connections: string[], + selectedRoom: string | undefined, + resetChatRoom: VoidFunction, + onRemoval?: VoidFunction | undefined, +) => ({ + subscription: RoomListSubscriptionQuery, + onError: console.error, + variables: { profileId, connections }, + updater: ( + store: RecordSourceSelectorProxy, + data: useRoomListSubscription$data | null | undefined, + ) => { + const roomId = data?.chatRoomOnRoomUpdate?.room?.node?.id + if (!roomId) return + if (wasRemovedFromChatRoom(data, profileId)) { + getChatRoomConnections(store, profileId).forEach((connectionRecord) => + ConnectionHandler.deleteNode(connectionRecord, roomId), + ) + } else { + const isArchived = data?.chatRoomOnRoomUpdate?.room?.node?.isArchived + getChatRoomConnections( + store, + profileId, + ({ q, archived }) => q === '' && archived === isArchived, + ).forEach((connectionRecord) => { + ConnectionHandler.deleteNode(connectionRecord, roomId) + const serverEdge = store.getRootField('chatRoomOnRoomUpdate')?.getLinkedRecord('room') + const edge = ConnectionHandler.buildConnectionEdge(store, connectionRecord, serverEdge) + if (edge) { + ConnectionHandler.insertEdgeBefore(connectionRecord, edge) + } + }) + } + }, + onNext: (data: useRoomListSubscription$data | null | undefined) => { + if (wasRemovedFromChatRoom(data, profileId)) { + if (selectedRoom && data?.chatRoomOnRoomUpdate?.room?.node?.id === selectedRoom) { + resetChatRoom() + } + onRemoval?.() + } + }, +}) + export const useRoomListSubscription = ({ connections, profileId, @@ -50,52 +102,11 @@ export const useRoomListSubscription = ({ }) => { const { id: selectedRoom, resetChatRoom } = useChatRoom() - const config = useMemo(() => { - const wasRemovedFromChatRoom = (data: useRoomListSubscription$data | null | undefined) => - data?.chatRoomOnRoomUpdate?.removedParticipants?.some( - (node) => node?.profile?.id === profileId, - ) - - return { - subscription: RoomListSubscriptionQuery, - onError: console.error, - variables: { profileId, connections }, - updater: ( - store: RecordSourceSelectorProxy, - data: useRoomListSubscription$data | null | undefined, - ) => { - const roomId = data?.chatRoomOnRoomUpdate?.room?.node?.id - if (!roomId) return - if (wasRemovedFromChatRoom(data)) { - getChatRoomConnections(store, profileId).forEach((connectionRecord) => - ConnectionHandler.deleteNode(connectionRecord, roomId), - ) - } else { - const isArchived = data?.chatRoomOnRoomUpdate?.room?.node?.isArchived - getChatRoomConnections( - store, - profileId, - ({ q, archived }) => q === '' && archived === isArchived, - ).forEach((connectionRecord) => { - ConnectionHandler.deleteNode(connectionRecord, roomId) - const serverEdge = store.getRootField('chatRoomOnRoomUpdate')?.getLinkedRecord('room') - const edge = ConnectionHandler.buildConnectionEdge(store, connectionRecord, serverEdge) - if (edge) { - ConnectionHandler.insertEdgeBefore(connectionRecord, edge) - } - }) - } - }, - onNext: (data: useRoomListSubscription$data | null | undefined) => { - if (wasRemovedFromChatRoom(data)) { - if (selectedRoom && data?.chatRoomOnRoomUpdate?.room?.node?.id === selectedRoom) { - resetChatRoom() - } - onRemoval?.() - } - }, - } - }, [profileId, connections, onRemoval, selectedRoom, resetChatRoom]) + const config = useMemo( + () => + getRoomListSubscriptionConfig(profileId, connections, selectedRoom, resetChatRoom, onRemoval), + [profileId, connections, onRemoval, selectedRoom, resetChatRoom], + ) return useSubscription(config) } diff --git a/packages/components/modules/messages/native/graphql/subscriptions/useRoomListSubscription.tsx b/packages/components/modules/messages/native/graphql/subscriptions/useRoomListSubscription.tsx new file mode 100644 index 00000000..cbc4e32b --- /dev/null +++ b/packages/components/modules/messages/native/graphql/subscriptions/useRoomListSubscription.tsx @@ -0,0 +1,59 @@ +'use-client' + +import { useCallback, useMemo, useRef } from 'react' + +import { useAppStateSubscription } from '@baseapp-frontend/utils/hooks/useAppStateSubscription' + +import { useFocusEffect } from 'expo-router' +import { Disposable, Environment, requestSubscription } from 'react-relay' +import { GraphQLSubscriptionConfig } from 'relay-runtime' + +import { useRoomListSubscription as useRoomListSubscriptionType } from '../../../../../__generated__/useRoomListSubscription.graphql' +import { getRoomListSubscriptionConfig } from '../../../common' +import useChatRoom from '../../../common/context/useChatRoom' + +export const useRoomListSubscription = ({ + connections, + profileId, + onRemoval, + environment, +}: { + connections: string[] + profileId: string + environment: Environment + onRemoval?: VoidFunction + isRemoval?: boolean +}) => { + const { id: selectedRoom, resetChatRoom } = useChatRoom() + const disposableRef = useRef(null) + + const config = useMemo>( + () => + getRoomListSubscriptionConfig(profileId, connections, selectedRoom, resetChatRoom, onRemoval), + [profileId, connections, onRemoval, selectedRoom, resetChatRoom], + ) + + const subscribe = useCallback(() => { + if (!profileId) return + disposableRef.current?.dispose?.() + disposableRef.current = requestSubscription(environment, config) + }, [profileId, environment, config]) + + const unsubscribe = useCallback(() => { + disposableRef.current?.dispose?.() + disposableRef.current = null + }, []) + + useFocusEffect( + useCallback(() => { + subscribe() + return () => { + unsubscribe() + } + }, [subscribe, unsubscribe]), + ) + + useAppStateSubscription(() => { + subscribe() + }) +} diff --git a/packages/components/modules/messages/native/index.ts b/packages/components/modules/messages/native/index.ts index 6c865c48..ba6fd5f8 100644 --- a/packages/components/modules/messages/native/index.ts +++ b/packages/components/modules/messages/native/index.ts @@ -1,2 +1,3 @@ // export native messages components export * from './graphql/subscriptions/useMessagesListSubscription' +export * from './graphql/subscriptions/useRoomListSubscription' diff --git a/packages/components/package.json b/packages/components/package.json index 59f2107b..5c1aeca5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@baseapp-frontend/components", "description": "BaseApp components modules such as comments, notifications, messages, and more.", - "version": "1.2.9", + "version": "1.3.0", "sideEffects": false, "scripts": { "babel:transpile": "babel modules -d tmp-babel --extensions .ts,.tsx --ignore '**/__tests__/**','**/__storybook__/**'", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13857e85..710c0894 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -889,10 +889,10 @@ importers: version: 8.57.1 eslint-config-airbnb: specifier: catalog:lint - version: 19.0.4(eslint-plugin-import@2.32.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1) + version: 19.0.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1) eslint-config-airbnb-typescript: specifier: catalog:lint - version: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-plugin-import@2.32.0)(eslint@8.57.1) + version: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) eslint-config-next: specifier: catalog:lint version: 13.5.11(eslint@8.57.1)(typescript@5.8.3) @@ -901,7 +901,7 @@ importers: version: 8.10.0(eslint@8.57.1) eslint-plugin-import: specifier: catalog:lint - version: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + version: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: specifier: catalog:lint version: 6.10.2(eslint@8.57.1) @@ -3295,7 +3295,7 @@ packages: engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 - '@types/react': ^19.1.4 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': @@ -3308,7 +3308,7 @@ packages: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 '@mui/material': '>=5.15.0' - '@types/react': ^19.1.4 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -3383,7 +3383,7 @@ packages: peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@types/react': ^19.1.4 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@emotion/react': @@ -3949,7 +3949,7 @@ packages: '@radix-ui/react-slot@1.2.0': resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} peerDependencies: - '@types/react': '*' + '@types/react': ^19.1.4 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -10000,6 +10000,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -17424,28 +17425,28 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.1 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) object.assign: 4.1.7 object.entries: 1.1.9 semver: 6.3.1 - eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.32.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1): + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -17459,8 +17460,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -17483,7 +17484,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1(supports-color@8.1.1) @@ -17494,22 +17495,22 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -17520,7 +17521,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3