From a2084f319b731d98b59485799fb80357094f8c38 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 5 Jul 2024 12:33:07 -0400 Subject: [PATCH] feat: Add interleaved HTJ2K and volume progressive loading (#4276) --- extensions/cornerstone/src/index.tsx | 25 ++ extensions/cornerstone/src/init.tsx | 10 +- .../default/src/Panels/PanelStudyBrowser.tsx | 27 ++- extensions/default/src/getPanelModule.tsx | 2 +- .../PanelStudyBrowserTracking.tsx | 20 +- modes/basic-test-mode/src/index.ts | 4 + .../core/src/extensions/ExtensionManager.ts | 5 +- .../docs/migration-guide/from-3p7-to-3p8.md | 2 +- .../migration-guide/from-3p8-to-3p9-beta.md | 215 ++++++++++++++++++ platform/docs/docs/migration-guide/from-v2.md | 2 +- yarn.lock | 41 +++- 11 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 4ea40d202..cc46e9afb 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -42,6 +42,7 @@ import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchroniz const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader; const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {}; +const { imageRetrieveMetadataProvider } = cornerstone.utilities; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'); @@ -55,6 +56,15 @@ const OHIFCornerstoneViewport = props => { ); }; +const stackRetrieveOptions = { + retrieveOptions: { + single: { + streaming: true, + decodeLevel: 1, + }, + }, +}; + /** * */ @@ -80,6 +90,21 @@ const cornerstoneExtension: Types.Extensions.Extension = { toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [ cornerstoneTools.Enums.Events.TOOL_ACTIVATED, ]); + + // Configure the interleaved/HTJ2K loader + imageRetrieveMetadataProvider.clear(); + // The default volume interleaved options are to interleave the + // image retrieve, but don't perform progressive loading per image + // This interleaves images and replicates them for low-resolution depth volume + // reconstruction, which progressively improves + imageRetrieveMetadataProvider.add( + 'volume', + cornerstone.ProgressiveRetrieveImages.interleavedRetrieveStages + ); + // The default stack loading option is to progressive load HTJ2K images + // There are other possible options, but these need more thought about + // how to define them. + imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions); }, onModeExit: ({ servicesManager }: withAppTypes): void => { diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index cba90a855..f7919ce9f 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -99,7 +99,7 @@ export default async function init({ hangingProtocolService, viewportGridService, stateSyncService, - studyPrefetcherService + studyPrefetcherService, } = servicesManager.services; window.services = servicesManager.services; @@ -180,10 +180,12 @@ export default async function init({ ); // this provider is required for Calibration tool metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999); + // These are set reasonably low to allow for interleaved retrieves and slower + // connections. imageLoadPoolManager.maxNumRequests = { - interaction: appConfig?.maxNumRequests?.interaction || 100, - thumbnail: appConfig?.maxNumRequests?.thumbnail || 75, - prefetch: appConfig?.maxNumRequests?.prefetch || 10, + interaction: appConfig?.maxNumRequests?.interaction || 10, + thumbnail: appConfig?.maxNumRequests?.thumbnail || 5, + prefetch: appConfig?.maxNumRequests?.prefetch || 5, }; initWADOImageLoader(userAuthenticationService, appConfig, extensionManager); diff --git a/extensions/default/src/Panels/PanelStudyBrowser.tsx b/extensions/default/src/Panels/PanelStudyBrowser.tsx index 650fd6c90..bbb7c4b77 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.tsx @@ -31,6 +31,7 @@ function PanelStudyBrowser({ const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ ...StudyInstanceUIDs, ]); + const [hasLoadedViewports, setHasLoadedViewports] = useState(false); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); @@ -108,6 +109,18 @@ function PanelStudyBrowser({ // // ~~ Initial Thumbnails useEffect(() => { + if (!hasLoadedViewports) { + if (activeViewportId) { + // Once there is an active viewport id, it means the layout is ready + // so wait a bit of time to allow the viewports preferential loading + // which improves user experience of responsiveness significantly on slower + // systems. + window.setTimeout(() => setHasLoadedViewports(true), 250); + } + + return; + } + const currentDisplaySets = displaySetService.activeDisplaySets; currentDisplaySets.forEach(async dSet => { const newImageSrcEntry = {}; @@ -126,7 +139,14 @@ function PanelStudyBrowser({ return { ...prevState, ...newImageSrcEntry }; }); }); - }, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]); + }, [ + StudyInstanceUIDs, + dataSource, + displaySetService, + getImageSrc, + hasLoadedViewports, + activeViewportId, + ]); // ~~ displaySets useEffect(() => { @@ -144,6 +164,9 @@ function PanelStudyBrowser({ const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { + if (!hasLoadedViewports) { + return; + } const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { const newImageSrcEntry = {}; @@ -213,7 +236,7 @@ function PanelStudyBrowser({ const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] + [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); diff --git a/extensions/default/src/getPanelModule.tsx b/extensions/default/src/getPanelModule.tsx index e294805df..138a8d193 100644 --- a/extensions/default/src/getPanelModule.tsx +++ b/extensions/default/src/getPanelModule.tsx @@ -31,7 +31,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }) }), }, { - name: 'measure', + name: 'measurements', iconName: 'tab-linear', iconLabel: 'Measure', label: i18n.t('SidePanel:Measurements'), diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 076d492a6..eaa10f40e 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -43,6 +43,7 @@ function PanelStudyBrowserTracking({ ...StudyInstanceUIDs, ]); const [studyDisplayList, setStudyDisplayList] = useState([]); + const [hasLoadedViewports, setHasLoadedViewports] = useState(false); const [displaySets, setDisplaySets] = useState([]); const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({}); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); @@ -128,6 +129,18 @@ function PanelStudyBrowserTracking({ // ~~ Initial Thumbnails useEffect(() => { + if (!hasLoadedViewports) { + if (activeViewportId) { + // Once there is an active viewport id, it means the layout is ready + // so wait a bit of time to allow the viewports preferential loading + // which improves user experience of responsiveness significantly on slower + // systems. + window.setTimeout(() => setHasLoadedViewports(true), 250); + } + + return; + } + const currentDisplaySets = displaySetService.activeDisplaySets; if (!currentDisplaySets.length) { @@ -152,7 +165,7 @@ function PanelStudyBrowserTracking({ return { ...prevState, ...newImageSrcEntry }; }); }); - }, [displaySetService, dataSource, getImageSrc]); + }, [displaySetService, dataSource, getImageSrc, activeViewportId, hasLoadedViewports]); // ~~ displaySets useEffect(() => { @@ -209,6 +222,9 @@ function PanelStudyBrowserTracking({ const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { + if (!hasLoadedViewports) { + return; + } const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { const displaySetInstanceUID = dSet.displaySetInstanceUID; @@ -428,7 +444,7 @@ function PanelStudyBrowserTracking({ onClickUntrack={displaySetInstanceUID => { onClickUntrack(displaySetInstanceUID); }} - onClickThumbnail={() => {}} + onClickThumbnail={() => { }} onDoubleClickThumbnail={onDoubleClickThumbnailHandler} activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs} /> diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts index 7cb9eda04..c303ee015 100644 --- a/modes/basic-test-mode/src/index.ts +++ b/modes/basic-test-mode/src/index.ts @@ -13,6 +13,7 @@ const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', thumbnailList: '@ohif/extension-default.panelModule.seriesList', + measurements: '@ohif/extension-default.panelModule.measurements', }; const tracked = { @@ -134,6 +135,9 @@ function modeFactory() { return { id: ohif.layout, props: { + // Use the first two for an untracked view + // leftPanels: [ohif.thumbnailList], + // rightPanels: [dicomSeg.panel, ohif.measurements], leftPanels: [tracked.thumbnailList], rightPanels: [dicomSeg.panel, tracked.measurements], // rightPanelClosed: true, // optional prop to start with collapse panels diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index e87dd647e..0c8982824 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -2,7 +2,8 @@ import MODULE_TYPES from './MODULE_TYPES'; import log from '../log'; import { PubSubService, ServiceProvidersManager } from '../services'; import { HotkeysManager, CommandsManager } from '../classes'; -import { DataSourceDefinition } from '../types'; +import type { DataSourceDefinition } from '../types'; +import type AppTypes from '../types/AppTypes'; /** * This is the arguments given to create the extension. @@ -48,7 +49,7 @@ export interface Extension { getSopClassHandlerModule?: (p: ExtensionParams) => unknown; getToolbarModule?: (p: ExtensionParams) => unknown; getPanelModule?: (p: ExtensionParams) => unknown; - onModeEnter?: () => void; + onModeEnter?: (p: AppTypes) => void; onModeExit?: () => void; } diff --git a/platform/docs/docs/migration-guide/from-3p7-to-3p8.md b/platform/docs/docs/migration-guide/from-3p7-to-3p8.md index 4e61de8c9..e35493efa 100644 --- a/platform/docs/docs/migration-guide/from-3p7-to-3p8.md +++ b/platform/docs/docs/migration-guide/from-3p7-to-3p8.md @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 2 sidebar_label: 3.7 -> 3.8 --- diff --git a/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md b/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md new file mode 100644 index 000000000..8a5c7b61f --- /dev/null +++ b/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md @@ -0,0 +1,215 @@ +--- +sidebar_position: 1 +sidebar_label: 3.8 -> 3.9-beta +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +# Migration Guide + + +## React 18 Migration Guide +As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version. +What's Changing? + + + +```md +- React 17 +- Using `defaultProps` +- `babel-inline-svg` for SVG imports +``` + + + + +```md +- React 18 +- Default parameters for props +- `svgr` for SVG imports +``` + + + + + + +### Update React version: +In your custom extensions and modes, change the version of react and react-dom to ^18.3.1. + +### Replace defaultProps with default parameters: + + + + +```jsx +const MyComponent = ({ prop1, prop2 }) => { + return
{prop1} {prop2}
+} + +MyComponent.defaultProps = { + prop1: 'default value', + prop2: 'default value' +} +``` + +
+ + +```jsx +const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => { + return
{prop1} {prop2}
+} +``` +
+
+ +### Update SVG imports: + + + + +```javascript +import arrowDown from './../../assets/icons/arrow-down.svg'; +``` + + + + +```javascript +import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg'; +``` + + + + + +
+ +--- + +
+ +## Renaming + +The panel in the default extension is renamed from `measure` to `measurements` to be more consistent with the rest of the extensions. + +**Action Needed** + +Update any references to the `measure` panel to `measurements` in your code. + +Find and replace + + + + @ohif/extension-default.panelModule.measure + + + @ohif/extension-default.panelModule.measurements + + + + +
+ +--- + +
+ +## RTStructure Set has transitioned from VTK actors to SVG. + +We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering. + +As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports. + + +Read more in Pull Requests: +- https://github.com/OHIF/Viewers/pull/4074 +- https://github.com/OHIF/Viewers/pull/4157 + +
+ +--- + +
+ +## Crosshairs + +They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off. + +To disable it, remove the configuration from the `initToolGroups` in your mode. + +``` +{ + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + } +} +``` + +
+ +--- + +
+ +## BulkDataURI Configuration + +We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one. + +### What's Changing? + + + + +```javascript +useBulkDataURI: false, +``` + + + + +```javascript +bulkDataURI: { + enabled: true, + // Additional configuration options +}, +``` + + + + + +Additional Notes: +- The new configuration allows for more granular control over BulkDataURI behavior. +- You can now add custom URL prefixing logic using the startsWith and prefixWith properties. +- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems. + + +
+ +--- + +
+ +## Polyfill.io + +We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-). + + +
+ +--- + +
+ +## Dynamic Modules + +TBD diff --git a/platform/docs/docs/migration-guide/from-v2.md b/platform/docs/docs/migration-guide/from-v2.md index e1a58e226..285ba3551 100644 --- a/platform/docs/docs/migration-guide/from-v2.md +++ b/platform/docs/docs/migration-guide/from-v2.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 sidebar_label: 2.x -> 3.5 --- diff --git a/yarn.lock b/yarn.lock index fcb857fa9..5607101d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2066,7 +2066,7 @@ "@docusaurus/theme-search-algolia" "2.4.3" "@docusaurus/types" "2.4.3" -"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": +"@docusaurus/react-loadable@5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== @@ -17966,6 +17966,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" +"react-loadable@npm:@docusaurus/react-loadable@5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" + integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== + dependencies: + "@types/react" "*" + prop-types "^15.6.2" + react-modal@3.11.2: version "3.11.2" resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4" @@ -19764,7 +19772,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19782,6 +19790,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -19868,7 +19885,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19889,6 +19906,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.0, strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -21961,7 +21985,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -21987,6 +22011,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"