From 14a795737bde129128cee17eb2a1514d84921070 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Fri, 18 Aug 2023 19:52:43 -0400 Subject: [PATCH] Replace react-dragula with dnd-kit (#1451) * Replace react-dragula package with dnd-kit * Refactor to function component * Remove @types/dragula as well * Move * Initial implementation * Ditch DragOverlay * Fix function name * Fix handling drag * Fix scrolling issue * Create gorgeous-meals-glow.md * Fix app test * Fix extension test * Fix styling --- .changeset/gorgeous-meals-glow.md | 5 + extension/test/app/containers/App.spec.js | 4 +- packages/redux-devtools-app/test/app.spec.tsx | 4 +- .../package.json | 7 +- .../src/ActionList.tsx | 325 ++++++++++-------- .../src/utils/createStylingFromTheme.ts | 21 -- pnpm-lock.yaml | 124 ++++--- 7 files changed, 260 insertions(+), 230 deletions(-) create mode 100644 .changeset/gorgeous-meals-glow.md diff --git a/.changeset/gorgeous-meals-glow.md b/.changeset/gorgeous-meals-glow.md new file mode 100644 index 0000000000..7919983ccc --- /dev/null +++ b/.changeset/gorgeous-meals-glow.md @@ -0,0 +1,5 @@ +--- +'@redux-devtools/inspector-monitor': patch +--- + +Replace react-dragula with dnd-kit diff --git a/extension/test/app/containers/App.spec.js b/extension/test/app/containers/App.spec.js index 7817da6492..051f5b30ec 100644 --- a/extension/test/app/containers/App.spec.js +++ b/extension/test/app/containers/App.spec.js @@ -37,8 +37,6 @@ describe('App container', () => { , ); const actionList = screen.getByTestId('actionList'); - expect( - within(actionList).getByTestId('actionListRows'), - ).toBeEmptyDOMElement(); + expect(within(actionList).queryByRole('button')).not.toBeInTheDocument(); }); }); diff --git a/packages/redux-devtools-app/test/app.spec.tsx b/packages/redux-devtools-app/test/app.spec.tsx index bbfa918129..30389c81a5 100644 --- a/packages/redux-devtools-app/test/app.spec.tsx +++ b/packages/redux-devtools-app/test/app.spec.tsx @@ -45,9 +45,7 @@ describe('App container', () => { , ); const actionList = screen.getByTestId('actionList'); - expect( - within(actionList).getByTestId('actionListRows'), - ).toBeEmptyDOMElement(); + expect(within(actionList).queryByRole('button')).not.toBeInTheDocument(); }); }); diff --git a/packages/redux-devtools-inspector-monitor/package.json b/packages/redux-devtools-inspector-monitor/package.json index cd57ff0d69..dc2679e240 100644 --- a/packages/redux-devtools-inspector-monitor/package.json +++ b/packages/redux-devtools-inspector-monitor/package.json @@ -36,7 +36,10 @@ }, "dependencies": { "@babel/runtime": "^7.22.6", - "@types/dragula": "^3.7.1", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@types/lodash": "^4.14.196", "@types/prop-types": "^15.7.5", "@types/redux-devtools-themes": "^1.0.0", @@ -50,7 +53,6 @@ "lodash.debounce": "^4.0.8", "prop-types": "^15.8.1", "react-base16-styling": "^0.9.1", - "react-dragula": "^1.1.17", "react-json-tree": "^0.18.0", "redux-devtools-themes": "^1.0.0" }, @@ -68,7 +70,6 @@ "@types/history": "^4.7.11", "@types/lodash.debounce": "^4.0.7", "@types/react": "^18.2.18", - "@types/react-dragula": "^1.1.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.46.0", diff --git a/packages/redux-devtools-inspector-monitor/src/ActionList.tsx b/packages/redux-devtools-inspector-monitor/src/ActionList.tsx index 4b312e8e81..f0c29986ef 100644 --- a/packages/redux-devtools-inspector-monitor/src/ActionList.tsx +++ b/packages/redux-devtools-inspector-monitor/src/ActionList.tsx @@ -1,9 +1,24 @@ -import React, { PureComponent, RefCallback } from 'react'; -import { Drake } from 'dragula'; -import dragula from 'react-dragula'; +import React, { ReactNode, useCallback, useLayoutEffect, useRef } from 'react'; import { Action } from 'redux'; import { PerformAction } from '@redux-devtools/core'; import { StylingFunction } from 'react-base16-styling'; +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { restrictToFirstScrollableAncestor } from '@dnd-kit/modifiers'; +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import ActionListRow from './ActionListRow'; import ActionListHeader from './ActionListHeader'; @@ -21,6 +36,10 @@ function getTimestamps>( }; } +function scrollToBottom(node: HTMLDivElement) { + node.scrollTop = node.scrollHeight; +} + interface Props> { actions: { [actionId: number]: PerformAction }; actionIds: number[]; @@ -44,152 +63,172 @@ interface Props> { lastActionId: number; } -export default class ActionList< - A extends Action, -> extends PureComponent> { - node?: HTMLDivElement | null; - scrollDown?: boolean; - drake?: Drake; - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - const node = this.node; - if (!node) { - this.scrollDown = true; - } else if (this.props.lastActionId !== nextProps.lastActionId) { - const { scrollTop, offsetHeight, scrollHeight } = node; - this.scrollDown = - Math.abs(scrollHeight - (scrollTop + offsetHeight)) < 50; - } else { - this.scrollDown = false; - } - } - - componentDidMount() { - this.scrollDown = true; - this.scrollToBottom(); - - if (!this.props.draggableActions) return; - const container = this.node!; - this.drake = dragula([container], { - copy: false, - copySortSource: false, - mirrorContainer: container, - accepts: (el, target, source, sibling) => - !sibling || !!parseInt(sibling.getAttribute('data-id')!), - moves: (el, source, handle) => - !!parseInt(el!.getAttribute('data-id')!) && - handle!.className.indexOf('selectorButton') !== 0, - }).on('drop', (el, target, source, sibling) => { - let beforeActionId = this.props.actionIds.length; - if (sibling && sibling.className.indexOf('gu-mirror') === -1) { - beforeActionId = parseInt(sibling.getAttribute('data-id')!); +export default function ActionList>({ + styling, + actions, + actionIds, + isWideLayout, + onToggleAction, + skippedActionIds, + selectedActionId, + startActionId, + onSelect, + onSearch, + searchValue, + currentActionId, + hideMainButtons, + hideActionButtons, + onCommit, + onSweep, + onJumpToState, + lastActionId, + onReorderAction, +}: Props) { + const nodeRef = useRef(null); + const prevLastActionId = useRef(); + + useLayoutEffect(() => { + if (nodeRef.current && prevLastActionId.current !== lastActionId) { + const { scrollTop, offsetHeight, scrollHeight } = nodeRef.current; + if (Math.abs(scrollHeight - (scrollTop + offsetHeight)) < 50) { + scrollToBottom(nodeRef.current); } - const actionId = parseInt(el.getAttribute('data-id')!); - this.props.onReorderAction(actionId, beforeActionId); - }); - } - - componentWillUnmount() { - if (this.drake) this.drake.destroy(); - } - - componentDidUpdate() { - this.scrollToBottom(); - } - - scrollToBottom() { - if (this.scrollDown && this.node) { - this.node.scrollTop = this.node.scrollHeight; } - } - getRef: RefCallback = (node) => { - this.node = node; - }; + prevLastActionId.current = lastActionId; + }, [lastActionId]); + + const setNodeRef = useCallback((node: HTMLDivElement | null) => { + if (node && !nodeRef.current) { + scrollToBottom(node); + } + + nodeRef.current = node; + }, []); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = useCallback( + ({ active, over }: DragEndEvent) => { + if (over && active.id !== over.id) { + const activeIndex = actionIds.indexOf(active.id as number); + const overIndex = actionIds.indexOf(over.id as number); + + const beforeActionId = + overIndex < activeIndex + ? (over.id as number) + : overIndex < actionIds.length - 1 + ? actionIds[overIndex + 1] + : actionIds.length; - render() { - const { - styling, - actions, - actionIds, - isWideLayout, - onToggleAction, - skippedActionIds, - selectedActionId, - startActionId, - onSelect, - onSearch, - searchValue, - currentActionId, - hideMainButtons, - hideActionButtons, - onCommit, - onSweep, - onJumpToState, - } = this.props; - const lowerSearchValue = searchValue && searchValue.toLowerCase(); - const filteredActionIds = searchValue - ? actionIds.filter( - (id) => - (actions[id].action.type as string) - .toLowerCase() - .indexOf(lowerSearchValue as string) !== -1, - ) - : actionIds; - - return ( + onReorderAction(active.id as number, beforeActionId); + } + }, + [actionIds, onReorderAction], + ); + + const lowerSearchValue = searchValue && searchValue.toLowerCase(); + const filteredActionIds = searchValue + ? actionIds.filter( + (id) => + (actions[id].action.type as string) + .toLowerCase() + .indexOf(lowerSearchValue as string) !== -1, + ) + : actionIds; + + return ( +
+ 0} + hasStagedActions={actionIds.length > 1} + searchValue={searchValue} + />
- 0} - hasStagedActions={actionIds.length > 1} - searchValue={searchValue} - /> -
- {filteredActionIds.map((actionId) => ( - = startActionId && - actionId <= (selectedActionId as number)) || - actionId === selectedActionId - } - isInFuture={ - actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId) - } - onSelect={(e: React.MouseEvent) => - onSelect(e, actionId) - } - timestamps={getTimestamps(actions, actionIds, actionId)} - action={actions[actionId].action} - onToggleClick={() => onToggleAction(actionId)} - onJumpClick={() => onJumpToState(actionId)} - onCommitClick={() => onCommit()} - hideActionButtons={hideActionButtons} - isSkipped={skippedActionIds.indexOf(actionId) !== -1} - /> - ))} -
+ + {filteredActionIds.map((actionId) => ( + + = startActionId && + actionId <= (selectedActionId as number)) || + actionId === selectedActionId + } + isInFuture={ + actionIds.indexOf(actionId) > + actionIds.indexOf(currentActionId) + } + onSelect={(e: React.MouseEvent) => + onSelect(e, actionId) + } + timestamps={getTimestamps(actions, actionIds, actionId)} + action={actions[actionId].action} + onToggleClick={() => onToggleAction(actionId)} + onJumpClick={() => onJumpToState(actionId)} + onCommitClick={() => onCommit()} + hideActionButtons={hideActionButtons} + isSkipped={skippedActionIds.indexOf(actionId) !== -1} + /> + + ))} + +
- ); - } +
+ ); +} + +interface SortableItemProps { + readonly children: ReactNode; + readonly actionId: number; +} + +function SortableItem({ children, actionId }: SortableItemProps) { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: actionId }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ {children} +
+ ); } diff --git a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts index 00f998e414..9bd45ea32a 100644 --- a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts +++ b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts @@ -86,23 +86,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({ actionListRows: { overflow: 'auto', - - '& div.gu-transit': { - opacity: '0.3', - }, - - '& div.gu-mirror': { - position: 'fixed', - opacity: '0.8', - height: 'auto !important', - 'border-width': '1px', - 'border-style': 'solid', - 'border-color': map.LIST_BORDER_COLOR, - }, - - '& div.gu-hide': { - display: 'none', - }, }, actionListHeaderSelector: { @@ -126,10 +109,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({ cursor: 'pointer', 'user-select': 'none', - '&:last-child': { - 'border-bottom-width': 0, - }, - 'border-bottom-color': map.BORDER_COLOR, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8b935b694..0bac9bc8b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1623,9 +1623,18 @@ importers: '@babel/runtime': specifier: ^7.22.6 version: 7.22.6 - '@types/dragula': - specifier: ^3.7.1 - version: 3.7.1 + '@dnd-kit/core': + specifier: ^6.0.8 + version: 6.0.8(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/modifiers': + specifier: ^6.0.1 + version: 6.0.1(@dnd-kit/core@6.0.8)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^7.0.2 + version: 7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0) + '@dnd-kit/utilities': + specifier: ^3.2.1 + version: 3.2.1(react@18.2.0) '@types/lodash': specifier: ^4.14.196 version: 4.14.196 @@ -1665,9 +1674,6 @@ importers: react-base16-styling: specifier: ^0.9.1 version: link:../react-base16-styling - react-dragula: - specifier: ^1.1.17 - version: 1.1.17 react-json-tree: specifier: ^0.18.0 version: link:../react-json-tree @@ -1714,9 +1720,6 @@ importers: '@types/react': specifier: ^18.2.18 version: 18.2.18 - '@types/react-dragula': - specifier: ^1.1.0 - version: 1.1.0 '@typescript-eslint/eslint-plugin': specifier: ^5.62.0 version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.1.6) @@ -6474,6 +6477,61 @@ packages: engines: {node: '>=10.0.0'} dev: true + /@dnd-kit/accessibility@3.0.1(react@18.2.0): + resolution: {integrity: sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.1 + dev: false + + /@dnd-kit/core@6.0.8(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.0.1(react@18.2.0) + '@dnd-kit/utilities': 3.2.1(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.1 + dev: false + + /@dnd-kit/modifiers@6.0.1(@dnd-kit/core@6.0.8)(react@18.2.0): + resolution: {integrity: sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==} + peerDependencies: + '@dnd-kit/core': ^6.0.6 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.0.8(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.1(react@18.2.0) + react: 18.2.0 + tslib: 2.6.1 + dev: false + + /@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0): + resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} + peerDependencies: + '@dnd-kit/core': ^6.0.7 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.0.8(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.1(react@18.2.0) + react: 18.2.0 + tslib: 2.6.1 + dev: false + + /@dnd-kit/utilities@3.2.1(react@18.2.0): + resolution: {integrity: sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.1 + dev: false + /@electron/get@2.0.2: resolution: {integrity: sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==} engines: {node: '>=12'} @@ -9932,9 +9990,6 @@ packages: resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} dev: true - /@types/dragula@3.7.1: - resolution: {integrity: sha512-hbMEG5+wZEwV6NK4cbexldLWEvYNox8PywM9ICIeCTM99g8nJxccE3C8vvl66TCfnN+R8ioNdOrHWJT+5X0pvw==} - /@types/ejs@3.1.2: resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==} dev: true @@ -10247,12 +10302,6 @@ packages: dependencies: '@types/react': 18.2.18 - /@types/react-dragula@1.1.0: - resolution: {integrity: sha512-wgRIVV2jo/Gria1PK3K26II7gfRD3VTcMfPYhL0CuIApSeon7xjBTj8Xs8Ln+Vbb/FuRKWfUaJXmF4R3KUGntA==} - dependencies: - '@types/dragula': 3.7.1 - dev: true - /@types/react-test-renderer@18.0.0: resolution: {integrity: sha512-C7/5FBJ3g3sqUahguGi03O79b8afNeSD6T8/GU50oQrJCU0bVCCGQHaGKUbg2Ce8VQEEqTw8/HiS6lXHHdgkdQ==} dependencies: @@ -11337,10 +11386,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /atoa@1.0.0: - resolution: {integrity: sha512-VVE1H6cc4ai+ZXo/CRWoJiHXrA1qfA31DPnx6D20+kSI547hQN5Greh51LQ1baMRMfxO5K5M4ImMtZbZt2DODQ==} - dev: false - /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -12369,13 +12414,6 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - /contra@1.9.4: - resolution: {integrity: sha512-N9ArHAqwR/lhPq4OdIAwH4e1btn6EIZMAz4TazjnzCiVECcWUPTma+dRAM38ERImEJBh8NiCCpjoQruSZ+agYg==} - dependencies: - atoa: 1.0.0 - ticky: 1.0.1 - dev: false - /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -12526,12 +12564,6 @@ packages: shebang-command: 2.0.0 which: 2.0.2 - /crossvent@1.5.4: - resolution: {integrity: sha512-b6gEmNAh3kemyfNJ0LQzA/29A+YeGwevlSkNp2x0TzLOMYc0b85qRAD06OUuLWLQpR7HdJHNZQTlD1cfwoTrzg==} - dependencies: - custom-event: 1.0.0 - dev: false - /crypt@0.0.2: resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} dev: false @@ -12660,10 +12692,6 @@ packages: stream-transform: 2.1.3 dev: true - /custom-event@1.0.0: - resolution: {integrity: sha512-6nOXX3UitrmdvSJWoVR2dlzhbX5bEUqmqsMUyx1ypCLZkHHkcuYtdpW3p94RGvcFkTV7DkLo+Ilbwnlwi8L+jw==} - dev: false - /d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -13317,13 +13345,6 @@ packages: engines: {node: '>=12'} dev: true - /dragula@3.7.2: - resolution: {integrity: sha512-iDPdNTPZY7P/l0CQ800QiX+PNA2XF9iC3ePLWfGxeb/j8iPPedRuQdfSOfZrazgSpmaShYvYQ/jx7keWb4YNzA==} - dependencies: - contra: 1.9.4 - crossvent: 1.5.4 - dev: false - /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true @@ -18836,13 +18857,6 @@ packages: react: 18.2.0 scheduler: 0.23.0 - /react-dragula@1.1.17: - resolution: {integrity: sha512-gJdY190sPWAyV8jz79vyK9SGk97bVOHjUguVNIYIEVosvt27HLxnbJo4qiuEkb/nAuGY13Im2CHup92fUyO3fw==} - dependencies: - atoa: 1.0.0 - dragula: 3.7.2 - dev: false - /react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} peerDependencies: @@ -20760,10 +20774,6 @@ packages: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} dev: true - /ticky@1.0.1: - resolution: {integrity: sha512-RX35iq/D+lrsqhcPWIazM9ELkjOe30MSeoBHQHSsRwd1YuhJO5ui1K1/R0r7N3mFvbLBs33idw+eR6j+w6i/DA==} - dev: false - /tildify@2.0.0: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'}