diff --git a/packages/ai/babel.config.js b/packages/ai/babel.config.js new file mode 100644 index 0000000000..325ca2a8ee --- /dev/null +++ b/packages/ai/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/packages/ai/jest.config.js b/packages/ai/jest.config.js new file mode 100644 index 0000000000..c853281819 --- /dev/null +++ b/packages/ai/jest.config.js @@ -0,0 +1,18 @@ +/* eslint-disable */ +const base = require('../../jest.config.base.js'); +const path = require('path'); + +module.exports = { + ...base, + displayName: 'ai', + testMatch: [ + ...base.testMatch, + '/src/**/*.test.ts', + '/src/**/*.spec.ts', + ], + moduleNameMapper: { + ...base.moduleNameMapper, + '^@cornerstonejs/(\\w+)/(.+)$': path.resolve(__dirname, '../$1/src/$2'), + '^@cornerstonejs/(.*)$': path.resolve(__dirname, '../$1/src'), + }, +}; diff --git a/packages/ai/src/ONNXSegmentationController.ts b/packages/ai/src/ONNXSegmentationController.ts index 6cad9e50d8..906f9ea8e6 100644 --- a/packages/ai/src/ONNXSegmentationController.ts +++ b/packages/ai/src/ONNXSegmentationController.ts @@ -14,6 +14,7 @@ import { LabelmapBaseTool, } from '@cornerstonejs/tools'; import { Events as aiEvents } from './enums'; +import getOrtWasmPaths from './utils/getOrtWasmPaths'; const { strategies } = cstSegmentation; const { fillInsideCircle } = strategies; @@ -1641,7 +1642,13 @@ export default class ONNXSegmentationController { } config.threads = parseInt(String(config.threads)); config.local = parseInt(config.local); - ort.env.wasm.wasmPaths = 'ort/'; + // Leave a location the application assigned alone. Otherwise take the + // system-level wasm directory, or resolve the copy of + // `onnxruntime-web/dist` against the application rather than against the + // current route. See `getOrtWasmPaths`. + if (!ort.env.wasm.wasmPaths) { + ort.env.wasm.wasmPaths = getOrtWasmPaths(); + } ort.env.wasm.numThreads = config.threads; ort.env.wasm.proxy = config.provider == 'wasm'; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 253080862d..61097f1c3f 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -2,10 +2,19 @@ import ONNXSegmentationController from './ONNXSegmentationController'; import LabelmapSlicePropagationTool from './LabelmapSlicePropagationTool'; import MarkerLabelmapTool from './MarkerLabelmapTool'; import { Events } from './enums'; +import getOrtWasmPaths, { + DEFAULT_ORT_WASM_DIRECTORY, +} from './utils/getOrtWasmPaths'; export { ONNXSegmentationController, LabelmapSlicePropagationTool, MarkerLabelmapTool, Events, + // Exported to answer where the ONNX Runtime binaries will be looked for, not + // to configure it: the location comes from `init({ wasmBasePath })` on the + // DICOM image loader, from `PUBLIC_URL`, or from assigning + // `ort.env.wasm.wasmPaths` directly, which the controller leaves alone. + getOrtWasmPaths, + DEFAULT_ORT_WASM_DIRECTORY, }; diff --git a/packages/ai/src/utils/getOrtWasmPaths.test.ts b/packages/ai/src/utils/getOrtWasmPaths.test.ts new file mode 100644 index 0000000000..ba0d0420f5 --- /dev/null +++ b/packages/ai/src/utils/getOrtWasmPaths.test.ts @@ -0,0 +1,49 @@ +import { utilities } from '@cornerstonejs/core'; +import getOrtWasmPaths from './getOrtWasmPaths'; + +/** + * How a base is resolved is `resolveWasmBasePath` in `@cornerstonejs/core`, + * covered by `packages/core/test/wasmBasePath.jest.js`. What is left here is the + * one thing this module decides - the standard directory - and that the two ways + * an application declares a location both reach the ONNX Runtime. + */ +describe('getOrtWasmPaths', () => { + const publicUrl = process.env.PUBLIC_URL; + + beforeEach(() => { + delete (globalThis as { PUBLIC_URL?: string }).PUBLIC_URL; + delete process.env.PUBLIC_URL; + utilities.setWasmBasePath(undefined); + window.history.replaceState(null, '', '/'); + }); + + afterAll(() => { + if (publicUrl === undefined) { + delete process.env.PUBLIC_URL; + } else { + process.env.PUBLIC_URL = publicUrl; + } + }); + + it('looks in ort/ under the application base', () => { + process.env.PUBLIC_URL = '/pacs/'; + // The bug this guards: `'ort/'` used to resolve against the route, so a + // viewer on /pacs/viewer/dicomweb fetched /pacs/viewer/ort/ and got + // index.html back. + window.history.replaceState(null, '', '/pacs/viewer/dicomweb'); + + expect(getOrtWasmPaths()).toBe('http://localhost/pacs/ort/'); + }); + + it('loads from the configured wasm directory, the way the codecs do', () => { + utilities.setWasmBasePath('/assets/cs-wasm/'); + + expect(getOrtWasmPaths()).toBe('http://localhost/assets/cs-wasm/'); + }); + + it('serves the binaries from a CDN the configured directory names', () => { + utilities.setWasmBasePath('https://cdn.example.com/wasm/'); + + expect(getOrtWasmPaths()).toBe('https://cdn.example.com/wasm/'); + }); +}); diff --git a/packages/ai/src/utils/getOrtWasmPaths.ts b/packages/ai/src/utils/getOrtWasmPaths.ts new file mode 100644 index 0000000000..ff3a48a0a0 --- /dev/null +++ b/packages/ai/src/utils/getOrtWasmPaths.ts @@ -0,0 +1,47 @@ +/** + * ONNX Runtime fetches its WebAssembly binaries from the prefix held in + * `ort.env.wasm.wasmPaths` and feeds whatever comes back straight to + * `WebAssembly.instantiateStreaming`. + * + * Every other wasm binary in this repository is located with + * `new URL(, import.meta.url)` — the bundler resolves the file, + * emits it, and hands back an absolute URL that does not depend on the page the + * user is currently on. `onnxruntime-web@1.17` cannot be addressed that way: + * its `exports` map publishes only the JavaScript entry points, so + * `new URL('onnxruntime-web/dist/ort-wasm-simd.jsep.wasm', import.meta.url)` + * fails to resolve. Applications copy `onnxruntime-web/dist` somewhere they + * serve instead — the example runner copies it to `/ort` + * (`utils/ExampleRunner/template-config.js`) — and point the runtime there. + * + * That makes it the same problem the codec binaries have, and it gets the same + * answer: `utilities.resolveWasmBasePath` in `@cornerstonejs/core`, which + * prefers the wasm directory the application declared and otherwise resolves + * the standard directory against the application's base. All this module adds + * is the standard directory name. + * + * Those two are the whole story, and there is deliberately no third way to name + * the location here. Since the binaries cannot be reached from this module, + * there is nothing to fall back to: an application serving them from somewhere + * else says so with `init({ wasmBasePath })`, or declares where it is mounted + * with `PUBLIC_URL`, or assigns `ort.env.wasm.wasmPaths` itself — which the + * controller leaves alone. + * + * When `onnxruntime-web` is eventually bumped to >= 1.21 its + * `*.bundle.min.mjs` builds resolve their own `.wasm` through `import.meta.url` + * and this module can be deleted. + */ +import { utilities } from '@cornerstonejs/core'; + +/** Directory applications copy `onnxruntime-web/dist` into. */ +export const DEFAULT_ORT_WASM_DIRECTORY = 'ort/'; + +/** + * Absolute URL prefix for the ONNX Runtime wasm binaries: the wasm directory the + * application configured, or `ort/` under the application's base. + * + * @returns the prefix as an absolute URL, or the directory unchanged when there + * is nothing to resolve it against (a non-browser context). + */ +export default function getOrtWasmPaths(): string { + return utilities.resolveWasmBasePath(DEFAULT_ORT_WASM_DIRECTORY); +} diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json index bc915f1e65..a954f9c713 100644 --- a/packages/ai/tsconfig.json +++ b/packages/ai/tsconfig.json @@ -4,5 +4,6 @@ "outDir": "./dist/esm", "rootDir": "./src" }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts", "./src/**/*.test.ts"] } diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index 3ee7e1af25..4de96fc15b 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -140,6 +140,12 @@ import { viewportIsInStackMode, } from './viewportCapabilities'; import { getNormalizedAspectRatio } from './getNormalizedAspectRatio'; +import { + getWasmBasePath, + resolveWasmBasePath, + setWasmBasePath, +} from './wasmBasePath'; +import resolveApplicationUrl, { getPublicUrl } from './resolveApplicationUrl'; export { updatePlaneRestriction } from './updatePlaneRestriction'; const getViewportModality = (viewport: IViewport, volumeId?: string) => _getViewportModality(viewport, volumeId, cache.getVolume); @@ -276,4 +282,9 @@ export { viewportIsInVolumeMode, viewportIsInStackMode, getNormalizedAspectRatio, + getWasmBasePath, + setWasmBasePath, + resolveWasmBasePath, + resolveApplicationUrl, + getPublicUrl, }; diff --git a/packages/core/src/utilities/resolveApplicationUrl.ts b/packages/core/src/utilities/resolveApplicationUrl.ts new file mode 100644 index 0000000000..72b59a553f --- /dev/null +++ b/packages/core/src/utilities/resolveApplicationUrl.ts @@ -0,0 +1,100 @@ +/** + * Resolves a path against the base the *application* is served from, rather + * than against the current document. + * + * A document-relative path resolves against the current route, so it only finds + * what it is looking for while the page happens to sit at the right depth. An + * application served from `/viewer/dicomweb` that asks for `assets/x` requests + * `/viewer/assets/x` and receives whatever the SPA fallback answers with, which + * for a binary asset means a corrupt file rather than an error. Anything an + * application stores beside its bundle and fetches by name — wasm binaries + * above all, since `WebAssembly.instantiateStreaming` reports the resulting + * `index.html` as a magic-word failure — has to be resolved against the + * application instead. + * + * `PUBLIC_URL` is where that base comes from: the value an application already + * injects to say where it is mounted, defaulting to the server root. + */ + +/** + * Declared because `process` does not exist in a browser, and the bundlers that + * substitute `process.env.PUBLIC_URL` do it by matching that exact expression — + * so it has to be spelled out rather than reached through `globalThis`. + */ +declare const process: { env: Record }; + +/** Base assumed when nothing declares one: applications sit at the root. */ +export const DEFAULT_PUBLIC_URL = '/'; + +/** + * `PUBLIC_URL` as a build-time substitution (Create React App and friends). + * + * A `typeof process !== 'undefined'` guard would be the obvious way to write + * this and does not work: bundlers rewrite `process.env.PUBLIC_URL` but leave + * the `typeof` check alone, and it is false in every browser bundle, so the + * substituted value would never be read. Catching the reference error is what + * is left. + */ +function getBuildTimePublicUrl(): string | undefined { + try { + return process.env.PUBLIC_URL || undefined; + } catch { + return undefined; + } +} + +/** + * The base the application declares for itself, defaulting to the server root. + * + * `PUBLIC_URL` on the global is the runtime spelling + * `utils/demo/helpers/initDemo.ts` sets and `dicom-microscopy-viewer` reads; + * `config.path` is the same value carried in a viewer's configuration object. + */ +export function getPublicUrl(): string { + const globals = globalThis as { + PUBLIC_URL?: string; + config?: { path?: string }; + }; + + return ( + globals.PUBLIC_URL || + globals.config?.path || + getBuildTimePublicUrl() || + DEFAULT_PUBLIC_URL + ); +} + +/** + * The page's origin, and nothing else. `PUBLIC_URL` is usually a path + * (`/pacs/`) and needs something absolute to resolve against, but the route is + * exactly what must stay out of the result — so hand over the protocol and host + * alone rather than `location.href` or `document.baseURI`. + */ +function getOrigin(): string | undefined { + const { location } = globalThis; + + return location ? `${location.protocol}//${location.host}` : undefined; +} + +/** + * Resolves a path against the application's base. + * + * @param path - a relative path is relative to the application, an absolute + * path is relative to the server root, and a full URL (a CDN) is used as + * given. An empty path yields the application's base itself. + * @returns an absolute URL, or `path` unchanged when there is nothing to + * resolve it against (a non-browser context). + */ +export default function resolveApplicationUrl(path = ''): string { + const publicUrl = getPublicUrl(); + // `PUBLIC_URL=/pacs` is a common spelling, and URL resolution would treat + // that last segment as a file name and discard it. + const base = publicUrl.endsWith('/') ? publicUrl : `${publicUrl}/`; + const origin = getOrigin(); + + try { + return new URL(path, origin ? new URL(base, origin) : new URL(base)).href; + } catch { + return path; + } +} diff --git a/packages/core/src/utilities/wasmBasePath.ts b/packages/core/src/utilities/wasmBasePath.ts new file mode 100644 index 0000000000..b8724ddb31 --- /dev/null +++ b/packages/core/src/utilities/wasmBasePath.ts @@ -0,0 +1,66 @@ +/** + * Where Cornerstone loads its WebAssembly binaries from. + * + * A wasm binary cannot be located the way a bundled asset is. The usual + * `new URL(, import.meta.url)` only works when the bundler resolves, + * emits and hashes the file, and several of the binaries Cornerstone needs + * cannot be reached that way: the decoders name their codecs with bare + * `@cornerstonejs/codec-...` specifiers, which bundlers do not rewrite inside + * `new URL(...)`, and `onnxruntime-web@1.17` publishes only JavaScript entry + * points in its `exports` map. An application therefore copies those binaries + * somewhere it serves and *declares* where they are. + * + * Declaring it once is the point of `wasmBasePath`: one directory holding every + * binary, set through `init({ wasmBasePath })` on + * `@cornerstonejs/dicom-image-loader` (see `LoaderOptions.wasmBasePath`) and + * recorded here rather than privately in the loader, so that every package + * locating a binary honours the same directory. + * + * When nothing declares one, each set of binaries falls back to the standard + * directory its owner copies them into, resolved against the application rather + * than against the current document — see `resolveApplicationUrl` for why that + * distinction is the whole point. + */ +import resolveApplicationUrl from './resolveApplicationUrl'; + +/** Directory every wasm binary is loaded from, or undefined for the default. */ +let wasmBasePath: string | undefined; + +/** + * Sets the directory every wasm binary is loaded from. Pass undefined (or an + * empty string) to restore each consumer's own default resolution. + * + * `init({ wasmBasePath })` on `@cornerstonejs/dicom-image-loader` calls this, + * so applications configuring the loader do not need to call it themselves. + */ +export function setWasmBasePath(basePath?: string): void { + wasmBasePath = basePath || undefined; +} + +/** The configured wasm directory, or undefined when nothing has set one. */ +export function getWasmBasePath(): string | undefined { + return wasmBasePath; +} + +/** + * Absolute URL of the directory a set of wasm binaries loads from. + * + * @param defaultDirectory - directory to use when nothing has declared one: the + * standard location its owner copies the binaries into, e.g. `ort/` for the + * ONNX Runtime. Both it and a configured `wasmBasePath` are resolved the same + * way — relative to the application, or to the server root when absolute, or + * used as given when a full URL. + * @returns the directory as an absolute URL with a trailing slash, or the + * unresolved directory when there is nothing to resolve it against (a + * non-browser context). + */ +export function resolveWasmBasePath(defaultDirectory = ''): string { + const directory = wasmBasePath || defaultDirectory; + + // A trailing slash, so the value resolves as a directory rather than having + // its last segment discarded as a file name. An empty directory stays empty: + // it resolves to the application's base itself, not to the server root. + return resolveApplicationUrl( + !directory || directory.endsWith('/') ? directory : `${directory}/` + ); +} diff --git a/packages/core/test/wasmBasePath.jest.js b/packages/core/test/wasmBasePath.jest.js new file mode 100644 index 0000000000..e54fc755de --- /dev/null +++ b/packages/core/test/wasmBasePath.jest.js @@ -0,0 +1,195 @@ +import { + getWasmBasePath, + resolveWasmBasePath, + setWasmBasePath, +} from '../src/utilities/wasmBasePath'; +import resolveApplicationUrl, { + getPublicUrl, +} from '../src/utilities/resolveApplicationUrl'; + +/** Puts the document on `route` the way a client-side router would. */ +function navigateTo(route) { + window.history.replaceState(null, '', route); +} + +describe('wasm base path', () => { + const publicUrl = process.env.PUBLIC_URL; + + beforeEach(() => { + delete globalThis.PUBLIC_URL; + delete globalThis.config; + delete process.env.PUBLIC_URL; + setWasmBasePath(undefined); + navigateTo('/'); + }); + + afterAll(() => { + if (publicUrl === undefined) { + delete process.env.PUBLIC_URL; + } else { + process.env.PUBLIC_URL = publicUrl; + } + }); + + describe('getPublicUrl', () => { + it('defaults to the server root', () => { + expect(getPublicUrl()).toBe('/'); + }); + + it('reads the runtime global', () => { + globalThis.PUBLIC_URL = '/pacs/'; + + expect(getPublicUrl()).toBe('/pacs/'); + }); + + it('reads a viewer configuration object', () => { + globalThis.config = { path: '/pacs/' }; + + expect(getPublicUrl()).toBe('/pacs/'); + }); + + it('reads the build-time substitution', () => { + process.env.PUBLIC_URL = '/pacs/'; + + expect(getPublicUrl()).toBe('/pacs/'); + }); + + it('prefers the runtime value over the build-time one', () => { + globalThis.PUBLIC_URL = '/runtime/'; + process.env.PUBLIC_URL = '/build/'; + + expect(getPublicUrl()).toBe('/runtime/'); + }); + }); + + describe('resolveApplicationUrl', () => { + it('resolves against the server root when nothing declares a base', () => { + expect(resolveApplicationUrl('assets/x.wasm')).toBe( + 'http://localhost/assets/x.wasm' + ); + }); + + it('ignores the route', () => { + // The bug this guards: a document-relative path resolves against the + // route, so a viewer on /viewer/dicomweb fetched /viewer/assets/ and got + // index.html back. + navigateTo('/viewer/dicomweb/studies/1.2.3'); + + expect(resolveApplicationUrl('assets/x.wasm')).toBe( + 'http://localhost/assets/x.wasm' + ); + }); + + it('resolves against a sub-path base', () => { + process.env.PUBLIC_URL = '/pacs/'; + navigateTo('/pacs/viewer/dicomweb'); + + expect(resolveApplicationUrl('assets/x.wasm')).toBe( + 'http://localhost/pacs/assets/x.wasm' + ); + }); + + it('tolerates a base without its trailing slash', () => { + process.env.PUBLIC_URL = '/pacs'; + + expect(resolveApplicationUrl('assets/x.wasm')).toBe( + 'http://localhost/pacs/assets/x.wasm' + ); + }); + + it('keeps an absolute path at the server root', () => { + process.env.PUBLIC_URL = '/pacs/'; + + expect(resolveApplicationUrl('/assets/x.wasm')).toBe( + 'http://localhost/assets/x.wasm' + ); + }); + + it('uses a full URL as given', () => { + expect(resolveApplicationUrl('https://cdn.example.com/x.wasm')).toBe( + 'https://cdn.example.com/x.wasm' + ); + }); + + it('resolves a full URL base as given', () => { + globalThis.PUBLIC_URL = 'http://cdn.example.com/app/'; + + expect(resolveApplicationUrl('assets/x.wasm')).toBe( + 'http://cdn.example.com/app/assets/x.wasm' + ); + }); + + it('yields the base itself for an empty path', () => { + process.env.PUBLIC_URL = '/pacs/'; + + expect(resolveApplicationUrl()).toBe('http://localhost/pacs/'); + }); + }); + + describe('resolveWasmBasePath', () => { + it('resolves the default directory against the application', () => { + navigateTo('/viewer/dicomweb'); + + expect(resolveWasmBasePath('ort/')).toBe('http://localhost/ort/'); + }); + + it('resolves the default directory against a sub-path base', () => { + process.env.PUBLIC_URL = '/pacs/'; + navigateTo('/pacs/viewer/dicomweb'); + + expect(resolveWasmBasePath('ort/')).toBe('http://localhost/pacs/ort/'); + }); + + describe('with a configured directory', () => { + it('takes it in preference to the default one', () => { + setWasmBasePath('/assets/cs-wasm/'); + process.env.PUBLIC_URL = '/pacs/'; + navigateTo('/pacs/viewer/dicomweb'); + + expect(resolveWasmBasePath('ort/')).toBe( + 'http://localhost/assets/cs-wasm/' + ); + }); + + it('uses a full URL as given', () => { + setWasmBasePath('https://cdn.example.com/wasm/'); + + expect(resolveWasmBasePath('ort/')).toBe( + 'https://cdn.example.com/wasm/' + ); + }); + + it('adds the trailing slash it may be missing', () => { + setWasmBasePath('/assets/cs-wasm'); + + expect(resolveWasmBasePath('ort/')).toBe( + 'http://localhost/assets/cs-wasm/' + ); + }); + + it('resolves a relative one against the application', () => { + setWasmBasePath('cs-wasm/'); + process.env.PUBLIC_URL = '/pacs/'; + navigateTo('/pacs/viewer/dicomweb'); + + expect(resolveWasmBasePath('ort/')).toBe( + 'http://localhost/pacs/cs-wasm/' + ); + }); + + it('reports it unresolved through getWasmBasePath', () => { + setWasmBasePath('/assets/cs-wasm/'); + + expect(getWasmBasePath()).toBe('/assets/cs-wasm/'); + }); + + it('is cleared by an empty value', () => { + setWasmBasePath('/assets/cs-wasm/'); + setWasmBasePath(''); + + expect(getWasmBasePath()).toBeUndefined(); + expect(resolveWasmBasePath('ort/')).toBe('http://localhost/ort/'); + }); + }); + }); +}); diff --git a/packages/dicomImageLoader/src/imageLoader/createImage.ts b/packages/dicomImageLoader/src/imageLoader/createImage.ts index 1b33cc4db2..7605c93a0f 100644 --- a/packages/dicomImageLoader/src/imageLoader/createImage.ts +++ b/packages/dicomImageLoader/src/imageLoader/createImage.ts @@ -86,7 +86,11 @@ async function createImage( } } - const { decodeConfig, wasmBasePath } = getOptions(); + // `setOptions` publishes the loader option system-wide, so the two agree; + // reading the system value as the fallback also picks up a directory set + // through `utilities.setWasmBasePath` alone. + const { decodeConfig, wasmBasePath = utilities.getWasmBasePath() } = + getOptions(); // Forward the WASM base path to the worker, where the decoders resolve their // binaries. The loader-level option wins over one set in decodeConfig. const taskDecodeConfig = diff --git a/packages/dicomImageLoader/src/imageLoader/internal/options.ts b/packages/dicomImageLoader/src/imageLoader/internal/options.ts index 31b6850f86..8070827126 100644 --- a/packages/dicomImageLoader/src/imageLoader/internal/options.ts +++ b/packages/dicomImageLoader/src/imageLoader/internal/options.ts @@ -1,3 +1,4 @@ +import { utilities } from '@cornerstonejs/core'; import type { LoaderOptions } from '../../types'; let options: LoaderOptions = { @@ -22,6 +23,14 @@ let options: LoaderOptions = { export function setOptions(newOptions: LoaderOptions): void { options = Object.assign(options, newOptions); + + // The wasm directory is not private to this loader: it is where every + // Cornerstone package looks for its binaries, so publish it system-wide. + // Options without the key leave the current value alone, matching the + // decode-config behaviour in `shared/wasmBasePath`. + if (newOptions.wasmBasePath !== undefined) { + utilities.setWasmBasePath(newOptions.wasmBasePath); + } } export function getOptions(): LoaderOptions { diff --git a/packages/docs/docs/getting-started/vue-angular-react-vite.md b/packages/docs/docs/getting-started/vue-angular-react-vite.md index ef45d1ba23..8b3ca3f2a3 100644 --- a/packages/docs/docs/getting-started/vue-angular-react-vite.md +++ b/packages/docs/docs/getting-started/vue-angular-react-vite.md @@ -151,6 +151,10 @@ dicomImageLoaderInit({ A relative `wasmBasePath` resolves against the decode worker's location, and an absolute path or full URL (e.g. a CDN) is used as given. When the option is unset, the default `import.meta.url` resolution applies, which is what unbundled and script-tag usage relies on. +The path is system-wide rather than loader-specific, so it is also where `@cornerstonejs/ai` looks for the ONNX Runtime binaries — copy `onnxruntime-web/dist` into the same directory and there is nothing further to configure. With no `wasmBasePath` set, those binaries are expected in `ort/` under the application's base, which is taken from `PUBLIC_URL` (`window.PUBLIC_URL`, `window.config.path` or the build-time `process.env.PUBLIC_URL`) and defaults to the server root. + +A **subpath** deployment therefore has to declare one of the two. `onnxruntime-web@1.17` exports only its JavaScript entry points, so its binaries cannot be located relative to the module that loads them the way the codecs' can — there is no base to derive and nothing to fall back to. Set `wasmBasePath`, or set `PUBLIC_URL` to where the application is mounted. Either way the location stops depending on the route the user happens to be on. + --- ## Vite diff --git a/utils/demo/helpers/initDemo.ts b/utils/demo/helpers/initDemo.ts index 8f940e5e15..48b34cb0c7 100644 --- a/utils/demo/helpers/initDemo.ts +++ b/utils/demo/helpers/initDemo.ts @@ -24,6 +24,15 @@ import { window.cornerstone = cornerstone; window.cornerstoneTools = cornerstoneTools; +// Examples are served from the root by the example dev server and from +// /live-examples/ on the docs site, and each deployment copies the wasm +// binaries it needs (onnxruntime-web, dicom-microscopy-viewer) next to the +// page. Declaring the page's own directory as the public URL is what makes +// those copies findable from either location; a page that declares its own +// PUBLIC_URL keeps it. Examples are single pages rather than routed +// applications, so the page directory *is* the application root here. +window.PUBLIC_URL ||= window.location.pathname.replace(/[^/]*$/, ''); + export default async function initDemo(config: any = {}) { const urlParams = new URLSearchParams(window.location.search); const debugEnabled = urlParams.get('debug') === 'true'; @@ -78,8 +87,7 @@ export default async function initDemo(config: any = {}) { */ export async function peerImport(moduleId) { if (moduleId === 'dicom-microscopy-viewer') { - // The microscopy viewer loads relative to the public URL - window.PUBLIC_URL ||= '/'; + // The microscopy viewer loads relative to the public URL, declared above. // Use a relative library path that includes the component name window.PUBLIC_LIB_URL ||= './${component}/'; return importGlobal(