From 4653c8b13c8faefd0551e7da0eebd01fa30da4f1 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 5 Jul 2024 11:42:25 -0400 Subject: [PATCH] feat: Use a global import function to perform lazy loading (#1344) --- common/reviews/api/core.api.md | 8 +++- .../core/src/RenderingEngine/WSIViewport.ts | 40 ++++++++++++------ packages/core/src/index.ts | 2 + packages/core/src/init.ts | 25 +++++------ .../core/src/types/Cornerstone3DConfig.ts | 6 +++ packages/docs/docs/contribute/linking.md | 7 ++++ playwright.config.ts | 2 +- utils/ExampleRunner/example-runner-cli.js | 3 +- utils/ExampleRunner/template-config.js | 6 +++ utils/demo/helpers/initDemo.js | 21 +++++++++- yarn.lock | 41 +++++++++++++++++-- 11 files changed, 129 insertions(+), 32 deletions(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index be620add35..9f921eca9a 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -411,6 +411,7 @@ type Cornerstone3DConfig = { strictZSpacingForVolumeViewport: boolean; }; enableCacheOptimization: boolean; + peerImport?: (moduleId: string) => any; }; // @public (undocumented) @@ -2574,6 +2575,9 @@ type OrientationVectors = { viewUp: Point3; }; +// @public (undocumented) +export function peerImport(moduleId: string): any; + // @public (undocumented) function performCacheOptimizationForVolume(volume: any): void; @@ -4404,6 +4408,8 @@ export class WSIViewport extends Viewport implements IWSIViewport { // (undocumented) getCurrentImageIdIndex(): number; // (undocumented) + static getDicomMicroscopyViewer: () => Promise; + // (undocumented) getFrameNumber(): number; // (undocumented) getFrameOfReferenceUID: () => string; @@ -4480,7 +4486,7 @@ export class WSIViewport extends Viewport implements IWSIViewport { // (undocumented) setCamera(camera: ICamera): void; // (undocumented) - setDataIds(imageIds: string[]): void; + setDataIds(imageIds: string[]): Promise; // (undocumented) setFrameNumber(frame: number): Promise; // (undocumented) diff --git a/packages/core/src/RenderingEngine/WSIViewport.ts b/packages/core/src/RenderingEngine/WSIViewport.ts index 74e0568762..2474326e93 100644 --- a/packages/core/src/RenderingEngine/WSIViewport.ts +++ b/packages/core/src/RenderingEngine/WSIViewport.ts @@ -9,18 +9,26 @@ import { WSIViewportInput, VOIRange, } from '../types'; +import uuidv4 from '../utilities/uuidv4'; import * as metaData from '../metaData'; import { Transform } from './helpers/cpuFallback/rendering/transform'; import Viewport from './Viewport'; import { getOrCreateCanvas } from './helpers'; import { EPSILON } from '../constants'; import { triggerEvent } from '../utilities'; +import { peerImport } from '../init'; const _map = Symbol.for('map'); const EVENT_POSTRENDER = 'postrender'; /** - * An object representing a single stack viewport, which is a camera - * looking into an internal scene, and an associated target output `canvas`. + * A viewport which shows a microscopy view using the dicom-microscopy-viewer + * library. This viewport accepts standard CS3D annotations, and responds + * similar to how the other types of viewports do for things like zoom/pan. + * + * This viewport required the `dicom-microscopy-viewer` import to be available + * from the peerImport function in the CS3D init configuration. See the + * example `initDemo.js` for one possible implementation, but the actual + * implementation of this will depend on your platform. */ class WSIViewport extends Viewport implements IWSIViewport { public modality; @@ -76,7 +84,7 @@ class WSIViewport extends Viewport implements IWSIViewport { // use absolute positioning internally. this.element.style.position = 'relative'; this.microscopyElement = document.createElement('div'); - this.microscopyElement.id = crypto.randomUUID(); + this.microscopyElement.id = uuidv4(); this.microscopyElement.innerText = 'Initial'; this.microscopyElement.style.background = 'grey'; this.microscopyElement.style.width = '100%'; @@ -209,8 +217,11 @@ class WSIViewport extends Viewport implements IWSIViewport { public getImageData() { const { metadata } = this; + if (!metadata) { + return; + } - const spacing = metadata.spacing; + const { spacing } = metadata; return { dimensions: metadata.dimensions, @@ -342,11 +353,15 @@ class WSIViewport extends Viewport implements IWSIViewport { }; /** - * Need to return this as a function to prevent webpack from munging it. + * Encapsulate the dicom microscopy fetch so that it can be replaced + * with the browser import function. Webpack munges this and then throws + * exceptions trying to get this working, so this has to be provided externally + * as a globalThis.browserImportFunction taking the package name, and a set + * of options defining how to get the value out of the package. */ - private getImportPath() { - return '/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js'; - } + public static getDicomMicroscopyViewer = async () => { + return peerImport('dicom-microscopy-viewer'); + }; /** * The FOR for whole slide imaging is the frame of reference in the DICOM @@ -434,16 +449,15 @@ class WSIViewport extends Viewport implements IWSIViewport { ); } - this.setWSI(imageIds, webClient); + // Returns the Promise from the child element. + return this.setWSI(imageIds, webClient); } public async setWSI(imageIds: string[], client) { this.microscopyElement.style.background = 'red'; this.microscopyElement.innerText = 'Loading'; this.imageIds = imageIds; - // Import the straight module so that webpack doesn't touch it. - await import(/* webpackIgnore: true */ this.getImportPath()); - const DicomMicroscopyViewer = (window as any).dicomMicroscopyViewer; + const DicomMicroscopyViewer = await WSIViewport.getDicomMicroscopyViewer(); this.frameOfReferenceUID = null; const metadataDicomweb = this.imageIds.map((imageId) => { @@ -479,6 +493,8 @@ class WSIViewport extends Viewport implements IWSIViewport { const imageFlavor = image.ImageType[2]; if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { volumeImages.push(image); + } else { + console.log('Unknown image type', image.ImageType); } }); this.metadataDicomweb = volumeImages; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1cf7dba3b5..d6533898a2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,6 +44,7 @@ import { setConfiguration, getWebWorkerManager, canRenderFloatTextures, + peerImport, } from './init'; // Classes @@ -87,6 +88,7 @@ export { // init init, isCornerstoneInitialized, + peerImport, // configs getConfiguration, setConfiguration, diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 2e1ac8e50d..30f02b2fa4 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -23,21 +23,17 @@ const defaultConfig: Cornerstone3DConfig = { }, // cache enableCacheOptimization: true, + /** + * Imports peer modules. + * This may just fallback to the default import, but many packaging + * systems don't deal with peer imports properly. + */ + peerImport: (moduleId) => null, }; let config: Cornerstone3DConfig = { - gpuTier: undefined, - detectGPUConfig: {}, - isMobile: false, // is mobile device - rendering: { - useCPURendering: false, - // GPU rendering options - preferSizeOverAccuracy: false, - useNorm16Texture: false, - strictZSpacingForVolumeViewport: true, - }, - // cache - enableCacheOptimization: true, + ...defaultConfig, + rendering: { ...defaultConfig.rendering }, }; let webWorkerManager = null; @@ -313,6 +309,10 @@ function getWebWorkerManager() { return webWorkerManager; } +function peerImport(moduleId: string) { + return config.peerImport(moduleId); +} + export { init, getShouldUseCPURendering, @@ -327,4 +327,5 @@ export { setConfiguration, getWebWorkerManager, canRenderFloatTextures, + peerImport, }; diff --git a/packages/core/src/types/Cornerstone3DConfig.ts b/packages/core/src/types/Cornerstone3DConfig.ts index a38671e98c..0b22d687e0 100644 --- a/packages/core/src/types/Cornerstone3DConfig.ts +++ b/packages/core/src/types/Cornerstone3DConfig.ts @@ -68,6 +68,12 @@ type Cornerstone3DConfig = { * buffers. */ enableCacheOptimization: boolean; + /** + * This function returns an imported module for the given module id. + * It allows replacing broken packing system imports with external importers + * that perform lazy imports. + */ + peerImport?: (moduleId: string) => any; }; export default Cornerstone3DConfig; diff --git a/packages/docs/docs/contribute/linking.md b/packages/docs/docs/contribute/linking.md index 66f064d399..b4e85de111 100644 --- a/packages/docs/docs/contribute/linking.md +++ b/packages/docs/docs/contribute/linking.md @@ -44,6 +44,13 @@ comes with various webpack configurations, you can use/run the force the reflected changes to be built again which is faster than `yarn build`. and it also watches for changes to the source code and rebuilds the package. +## External Components + +Some components such as the `dicom-microscopy-viewer` are linked externally as +optional inclusions in the overall `cornerstone3D` package. You will need to +add a peerImport function which can import the required modules, and register +your function with the cornerstone init method. + ## Tips 1. `yarn link` is actually a symlink between packages. If your linking is not working, diff --git a/playwright.config.ts b/playwright.config.ts index 973a99d5e6..696f2e6959 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -38,6 +38,6 @@ export default defineConfig({ command: 'yarn build-and-serve-static-examples', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, + timeout: 150 * 1000, }, }); diff --git a/utils/ExampleRunner/example-runner-cli.js b/utils/ExampleRunner/example-runner-cli.js index 7bc14185a5..ec1d51ee7f 100755 --- a/utils/ExampleRunner/example-runner-cli.js +++ b/utils/ExampleRunner/example-runner-cli.js @@ -20,6 +20,7 @@ const rootPath = path.resolve(path.join(__dirname, '../..')); program .option('-c, --config [file.js]', 'Configuration file') .option('--no-browser', 'Do not open the browser') + .option('--https', 'Enable https') .parse(process.argv); const options = program.opts(); @@ -283,7 +284,7 @@ function run() { // You can run this with --no-cache after the serve to prevent caching // which can help when doing certain types of development. shell.exec( - `webpack serve --host 0.0.0.0 --progress --config ${webpackConfigPath}` + `webpack serve --host 0.0.0.0 ${options.https ? '--https' : ''} --progress --config ${webpackConfigPath}` ); } else { console.log('=> To run an example:'); diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index 34003955c6..82b55dea75 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -105,6 +105,12 @@ module.exports = { open: false, port: ${process.env.CS3D_PORT || 3000}, historyApiFallback: true, + allowedHosts: [ + '127.0.0.1', + 'localhost', + // Change the next line to add your localhostname to run via localhostname + // 'braveheart2', + ], headers: { "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Opener-Policy": "same-origin" diff --git a/utils/demo/helpers/initDemo.js b/utils/demo/helpers/initDemo.js index 5d39d7fe9b..5d7b69f1fc 100644 --- a/utils/demo/helpers/initDemo.js +++ b/utils/demo/helpers/initDemo.js @@ -8,6 +8,25 @@ export default async function initDemo() { initProviders(); initCornerstoneDICOMImageLoader(); initVolumeLoader(); - await csRenderInit(); + await csRenderInit({ peerImport }); await csToolsInit(); } + +/** + * This is one example of how to import peer modules that works with webpack + * It in fact just uses the default import from the browser, so it should work + * on any standards compliant ecmascript environment. + */ +export async function peerImport(moduleId) { + if (moduleId === 'dicom-microscopy-viewer') { + return importGlobal( + '/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js', + 'dicomMicroscopyViewer' + ); + } +} + +async function importGlobal(path, globalName) { + await import(/* webpackIgnore: true */ path); + return window[globalName]; +} diff --git a/yarn.lock b/yarn.lock index 31f096aec0..eda5afb6ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2130,7 +2130,7 @@ "@docusaurus/theme-search-algolia" "2.3.1" "@docusaurus/types" "2.3.1" -"@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== @@ -18931,6 +18931,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-resize-detector@6.7.8: version "6.7.8" resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.8.tgz#318c85d1335e50f99d4fb8eb9ec34e066db597d0" @@ -20793,7 +20801,7 @@ string-similarity@^4.0.4: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -"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== @@ -20811,6 +20819,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.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -20884,7 +20901,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== @@ -20917,6 +20934,13 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.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" @@ -22982,7 +23006,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"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== @@ -23008,6 +23032,15 @@ wrap-ansi@^6.0.1: 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"