Repository navigation
Conversation
…ntime - Remove the LegacyWorkList route and the workList.variant customization; WorkList is now always mounted at / - Drop the now-unused @ohif/ui workspace dependency from 11 packages - Switch @babel/preset-react to the automatic runtime in all config blocks, matching what the rsbuild/SWC dev pipeline already produced - Set tsconfig jsx to react-jsx - Replace platform/ui-next's drifted babel.config.js with the standard re-export of the root config Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
- Bump react/react-dom to 19.2.7 (exact pins) in all packages and add pnpm overrides so a single copy is guaranteed under the hoisted linker - @types/react 19.2.17, @types/react-dom 19.2.3 - @ohif/ui-next: react moves from dependencies to peerDependencies (^19) - platform/ui: react out of dependencies (UMD externals), peers widened, PortalTooltip ported from legacy ReactDOM.render to createRoot - @testing-library/react 16.3.2 (v13 relied on react-dom/test-utils, removed in react-dom 19); react-test-renderer and framer-motion deleted (zero imports) - next-themes 0.4.6, lucide-react 0.577.0, react-resize-detector 12.3.0, docs react-day-picker 9.12.0 - Apply types-react-codemod preset-19 (useRef initial values, ReactElement generics) Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
The rsbuild config that already powered dev:fast is now production-ready and pnpm run build produces the app dist through it. The rspack pipeline stays available as build:legacy (rollback) and keeps powering the classic dev servers and the e2e webServer until those migrate. - Extract InjectServiceWorkerManifestPlugin into a shared file that takes its bundler APIs from compiler.webpack, so the same plugin runs under the rspack versions bundled by both pipelines - Legacy-compatible dist layout: bundles at the dist root named [name].bundle.<hash>.js, HTML emitted as index.html - Disable rsbuild's built-in publicDir copy (it copied all of public/ including config/ and html-templates/); replicate the selective copy with explicit patterns instead - Parity with webpack.base.js: optimization.sideEffects false, noParse for dicomicc, fullySpecified off for .m?js, mainFields order, prod source-map devtool template, QUICK_BUILD support, TEST_ENV define, mode-dependent APP_CONFIG default, HTML_TEMPLATE/ENTRY_TARGET envs - legalComments none to match the rspack output (no *.LICENSE.txt in dist or the sw.js precache manifest) - Add analyze script (RSDOCTOR=true) Verified against the rspack output: identical file sets modulo the documented vendor-split chunks and static/* asset layout, byte-identical app-config.js, correctly prefixed sw.js manifest under PUBLIC_URL subpaths, rollbar HTML template switch, and a served-dist smoke test. Accepted diffs: HTML is no longer minified (rsbuild 1.x needs a plugin for that) and CSS is now minified (legacy prod CSS was not). Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
- babel.config.js: babel-plugin-react-compiler (target 19) runs first, covering the rspack legacy build, classic dev, jest, and the UMD package builds; REACT_COMPILER=off is the kill switch - rsbuild.config.ts: scoped @rsbuild/plugin-babel pass on workspace source (platform/extensions/modes src, excluding frozen platform/ui) layered on SWC for dev:fast and the production build; preset-typescript is reset to infer TS vs TSX per file extension because the plugin's forced isTSX rejects legal plain-.ts syntax (angle-bracket casts) - UMD package builds (ui, ui-next, core, i18n) run with REACT_COMPILER=off: their externals cover react/react-dom only, not react/compiler-runtime, so compiled output would inline React - New eslint.config.mjs (flat, eslint 10 + eslint-plugin-react-hooks 7) with the compiler-powered rules and a lint:compiler script; the diagnostics list is the do-not-touch gate for the upcoming manual memoization cleanup (baseline: 292 problems, 184 errors) - Ignore the font asset the ui-next UMD build emits at the package root Verified: compiled components present in 14 production bundles (memo_cache_sentinel), absent from the ui-next UMD dist; jest suite green with the compiler active; both dev servers boot and serve. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
…oization forwardRef -> ref-as-prop across all 28 component files (90 sites): components take ref as a regular prop typed via React.ComponentProps (which includes ref under the react 19 types) or an explicit ref?: React.Ref<...>; displayName assignments and useImperativeHandle bodies preserved. propTypes removed from all 27 files that still carried runtime prop-types (TypeScript types already cover them); the ProgressDropdownOptionPropType export and its imports removed with it. Manual memoization removed only where proven redundant: 50 useCallback/ useMemo sites across 21 files that (a) carry zero compiler-lint diagnostics and (b) were verified compiled by running the production babel transform per file and checking for memo-cache slots. Files the compiler bails on or does not recognize (factory-created components in lib/createContext, Clipboard, WorkflowsProvider, and the 22 files with compiler diagnostics) keep their manual memoization, as does the debounce-wrapping useMemo in InputFilter (recreating a debouncer per render would drop pending calls). SmartScrollbar's React.memo trio stays: SmartScrollbar.tsx fails its compiler gate. eslint.config.mjs now bans forwardRef and prop-types imports in ui-next scope so the removed patterns do not creep back in. New exhaustive-deps warnings (~18) are the classic rule not modeling compiler memoization of unwrapped effect dependencies - the affected files are all verified compiled, so effect cadence is unchanged at runtime; error-level lint surface is unchanged vs the phase-3 baseline. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
propTypes removed from the remaining 33 files across platform/app, platform/core, and all extensions (TypeScript types already cover them), and the prop-types dependency dropped from 15 package.jsons (kept in frozen platform/ui). MicroscopyPanel's interface used PropTypes members as TS types - replaced with real types. Manual memoization removed from the 18 files that pass both gates (zero compiler-lint diagnostics AND per-file verified compiler coverage): ~50 useCallback/useMemo sites across extensions/cornerstone, extensions/default, extensions/dicom-microscopy, platform/core, and platform/app hooks. The 45 other memoization-carrying files keep theirs (compiler bailouts or unrecognized components), as does ViewportWindowLevel's debounce chain. Every extension and mode UMD build script now runs REACT_COMPILER=off, matching the platform packages: their externals do not cover react/compiler-runtime, and the cornerstone extension UMD was found bundling it (verified absent after gating). Compiler-health guard rules (no forwardRef, no prop-types) now apply across the whole workspace, and a lint budget ratchet (scripts/reactCompilerLintBudget.mjs + .react-compiler-lint-budget.json, 186 errors / 135 warnings) runs in the CircleCI UNIT_TESTS job so the diagnostic count can only go down. Also removed a stale @types/react 18 entry from platform/app dependencies. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
…rfaced ManagedDialog mutated its position object in place and depended on a later render observing the mutation: compiled memoization kept explicitly positioned dialogs (e.g. the measurement context menu) clipped at the viewport edge, and the in-place mutation was also masking an infinite-setState loop in the dialog ref chain (the ref is re-attached on renders because useDraggable composes an unmemoized ref). Positions are now immutable, the state update bails out on equal coordinates, and the measure/reposition/reveal runs in a layout effect after content layout but before paint. PanelSegmentation read customizations once per render via customizationService.getCustomization, which races mode onModeEnter registrations: TMTV replaces panelSegmentation.onSegmentationAdd with its create-labelmap-from-PT handler, and when the panel's first render preceded that registration the compiler memoized the stale default handler permanently, so the segmentation was created from CT, SUV statistics (incl. lesion glycolysis) were never computed, and the TMTV CSV export crashed. The new useCustomization hook in @OHIF/core subscribes to the customization-modified events so consumers converge on the registered value regardless of mount order; PanelSegmentation now uses it for all of its customization reads. Verified with the previously failing Playwright specs: ContextMenu and TMTVCSVReport (5/5 with the compiler on). Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
…tomization useViewportHover is restored to its pre-cleanup form. The sweep had unwrapped setupListeners while leaving it in the effect dependency array, so every render re-attached the document-level mousemove/resize listeners; under the resulting churn the toolbar overlay and hotkey paths intermittently never dispatched their commands (rotate/flip/reset e2e failures). The manual memoization here is load-bearing and this file is excluded from the compiler-era cleanup. useCustomization now subscribes only to MODE_CUSTOMIZATION_MODIFIED: mode-scope registrations (mode.onModeEnter) are the ones that race component mounting, while global and default customizations are registered before the app renders. PanelSegmentation reads only panelSegmentation.onSegmentationAdd through the hook - the key TMTV overrides after mount - and keeps direct getCustomization reads for the five customizations that are registered before panels can mount, which keeps the panel's re-render surface unchanged. Verified with the Playwright regression targets: ContextMenu and TMTVCSVReport pass; jest suite and production build green. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
The COVERAGE babel rule in .webpack/webpack.base.js supplied its own inline presets (@babel/preset-typescript + classic-runtime @babel/preset-react) plus istanbul. Because babel-loader still loads the root babel.config.js, that inline classic-runtime preset-react shadowed the automatic-runtime one and, with it, babel-plugin-react-compiler never took effect in coverage builds. Every COVERAGE build (Cypress e2e, the Playwright e2e webServer, and coverage unit runs) therefore shipped the compiler-era cleanup components without the memoization the compiler is supposed to restore. Context providers whose manual useMemo was removed produced a new context value every render, cascading re-renders that broke behavior the production and dev:fast builds get right - most visibly the viewport orientation markers not updating after rotate/flip/reset, which is what the OHIFCornerstoneToolbar and OHIFCornerstoneHotkeys cypress specs assert. The rule now mirrors the non-coverage dev rule: delegate to the root babel config (which carries the compiler) and add only istanbul, so the coverage/e2e build exercises the same compiled output that ships. Verified: the previously failing OHIFCornerstoneToolbar and OHIFCornerstoneHotkeys cypress specs pass (10/10) with this change. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
The BUILD_PACKAGES_QUICK security-audit step runs pnpm audit --audit-level high only when pnpm-lock.yaml changes versus master. This PR touches the lockfile (the React 19 dependency bumps), so the step runs and trips on GHSA-xcpc-8h2w-3j85 (adm-zip), a pre-existing tree entry pulled transitively through dcmjs (@cornerstonejs) that master carries but never gates. dcmjs is unchanged here (pinned 0.52.0), so this advisory is not introduced by this PR; add it to the accepted ignoreGhsas list alongside the existing entry. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
…piler Root cause of the e2e failures (OHIFCornerstoneToolbar / OHIFCornerstoneHotkeys rotate/flip/reset): the React Compiler miscompiles the components under extensions/cornerstone/src/Viewport/. Those components read and mutate external, non-React state during render and through imperative cornerstone3D event handlers (the enabled element, the camera via canvasToWorld, GL actors). The compiler's memoization assumes referential purity, so the compiled output silently stops recomputing - most visibly ViewportOrientationMarkers keeps the pre-transform letters after a rotate/flip/reset even though the command ran and the camera changed. Bisected with a deterministic oracle in a clean worktree: the failure appears exactly at the compiler-enablement commit (the prior commit passes), reproduces with the compiler on, and disappears when extensions/cornerstone/src/Viewport is excluded from it. A "use no memo" directive on the marker alone was insufficient because the miscompiled component is the viewport wrapper, so the whole Viewport directory is scoped out. The rest of the workspace keeps the compiler. Applied to both pipelines: a babel overrides-exclude for the rspack path (dev / classic dev / coverage e2e / rspack builds) and a matching @rsbuild/plugin-babel exclude for dev:fast and the rsbuild production build. Verified: rotate, flip, and reset all update the orientation markers with this exclusion (previously stale); the compiler still applies everywhere else. Claude-Session: https://claude.ai/code/session_01KKhdKHKU26suaTRHFVyXdR
…ponent useWorkListToolbarActions invoked the ohif.dataSourceConfigurationComponent customization as a plain function, which spliced that component's hooks (useTranslation, useModal, useState/useEffect) into the caller's hook list and broke the Rules of Hooks — the WorkList route crashed with a hook-order violation and 'Cannot read properties of undefined (reading length)'. Render it as a component so its hooks get their own fiber, and gate the early return on the customization's presence instead of its render output.
ViewportSliceProgressScrollbar rebuilt imageIds and the imageIdToIndex Map on every render. The component lives in the Viewport directory that is excluded from the React Compiler, so nothing auto-memoizes them anymore; the byte-array seeding effects in useLoadedSliceBytes/useViewedSliceBytes list them as deps, re-ran each render, and their version bump re-rendered the component in an infinite loop (continuous 'Maximum update depth exceeded' errors on every viewer route). Same class of fix as the useViewportHover memoization restore.
Bump @rsbuild/core 1.7.3 -> 2.1.6, plugin-react -> 2.1.0, plugin-babel -> 2.0.1, plugin-node-polyfill -> 1.4.6. The config surface is unchanged in v2 except server.host, whose default flipped from 0.0.0.0 to localhost — pin it to keep the LAN Network URL. Rename the entry from app to index: rsbuild derives the dev-server route and printed URL from the entry name alone (only 'index' maps to '/'), so the old entry served and printed http://localhost:3000/app. The emitted bundles keep the rspack build's app.bundle.<hash>.js / app.bundle.css naming via function-form output.filename, and the index.html filename override is now redundant ([name].html already yields index.html).
…ngerouslyUseDynamicConfig
…normalize mode main fields All 26 per-package prod configs now import the shared externals contract (react/react-dom/react/jsx-runtime added as externals everywhere). Fixes the ohif-ui-next UMD library name collision with @ohif/ui. Normalizes the four non-conforming mode main fields to dist/ohif-<name>.umd.js and rewrites modes/segmentation's legacy standalone config to the standard mode shape.
pluginConfig.json gains a $schema pointer; the never-read version fields are removed. Schema is draft-07 with additionalProperties:false.
…ce-worker bypass Both nginx templates gain an opt-in CSP_HEADER add_header (envsubst allowlist updated) and a /plugins/ location block with explicit MIME types, cache tiers, and no SPA fallback. The service-worker precache excludes plugins/ and a NetworkOnly route bypasses /plugins/ fetches (rspack.pwa.js only; the rsbuild dev pipeline generates no service worker). netlify.toml's Report-Only header now mirrors the documented baseline with interim unsafe-inline.
…nConfig runtimeExtensionLoader.ts is the single gate for the loadModule fallthrough: URL-shaped specifiers must pass a deny-by-default origin allowlist (same-origin implicit, extras via window.config.runtimeExtensionOrigins) and unknown bare names throw a descriptive error. The codegen emits the gated epilogue and the config is structurally validated at build with actionable messages.
pnpm plugin add installs via pnpm and writes the pluginConfig entry in one step; doctor validates declared plugins, peer ranges against version.txt, dangling references, and singleton copies in directory plugins.
The published set is now exactly the shared-surface packages (@OHIF/core, @ohif/ui-next, @ohif/i18n, @ohif/extension-default, @ohif/extension-cornerstone) with repaired tarball metadata (publishConfig dist rewrites, files, keywords, peer hygiene). The remaining extensions, all modes, and @ohif/ui are private: true and stay version-stamped. verify-tarballs enforces the tier invariant and publish-list parity; verify-umd-global proves a built UMD assigns its window global. @ohif/app now devDepends on @ohif/ui.
Adds Track B runtime loading: descriptors in window.config.extensions/modes are loaded through runtimeExtensionLoader with a strict globalName discriminator (present => UMD global, absent => ESM default), a fail-closed coreVersionRange gate (includePrerelease), stylesheet injection, and a window.__ohif audit record reconciled and surfaced via NotificationService. runtimeShared.ts exposes the twelve host singletons (react, @OHIF/core, @ohif/ui-next, @ohif/i18n, @cornerstonejs/*, ...) on window so runtime bundles resolve them, with a parity test binding the list to pluginExternals.hostSharedPackages.
Shared resolveConfig gains exact-match dedupe aliases so directory and node_modules plugins resolve one copy of react/@OHIF/core/etc. through both pipelines. Tailwind content globs extend from pluginConfig entries so out-of-tree and installed plugins render styled. Adds the directory-plugin verification recipe.
…oyment templates pnpm create ohif scaffolds standalone extensions/modes, a full workspace (a user-owned repo with a committed ohif.config.json manifest and a managed .ohif viewer harness), or a config-only deployment. A migrate subcommand ports CLI-era extensions to the current contract. Templates vendor the externals contract; unit and e2e smoke tests cover scaffold, build, and UMD global.
…drop dead main/module fields"
… the de-forking guide - Mode template: extensionDependencies uses the peer range (versions are not checked at present); AGENTS.md rule on version checks names coreVersionRange, not the peer range. - Workspace template: add the `harness` script, so `pnpm run harness upgrade <tag>` works. - De-forking guide: workspaces need OHIF 3.14+, the scaffolded ohifVersion is the safe default, register plugins in ohif.config.json, and upgrade with `pnpm run harness upgrade <tag>`.
…st; flag it in migrate
…s in descriptor examples
'^a|b' anchored only "a", so "b" could match anywhere in ?configUrl=, letting a hostile URL pass a regex-only allowlist through its query string.
Mode.tsx caught registration errors, but recordRegistrationError only updated runtime-plugin audit records, so a built-in extension's failure was dropped without a log or notification. Log it, add an audit record when none exists, and document the built-in record in the audit reference.
{ type: 'filesystem' } is webpack's form; rspack 2 refuses it, so every
extension's and mode's `pnpm run dev` failed (on master too). Also add the
rspack.dev.js the basic and segmentation modes' dev scripts point at.
…rect peer-range wording in its report and docs
…ript calls to pnpm, and flags leftover yarn and jest scripts
…t/umd build output
…nt to the version guides for source changes
…rs too, optional ones skipped); doctor warns about uninstalled peers; unknown-module notice says how to fix it
…ps the npm and yarn engines
… create-ohif itself
…ckout after linking the plugin
…onfig.json and record it as duplicate-of-built-in
…date the CSP guidance for cross-origin plugins, and drop the Track B label
…ishing and give the commands for its checks
… and record each runtime plugin's kind
…t folder it lands in; document every option
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this branch does
A rework of how extensions and modes are built, distributed, and loaded, so plugins can live outside the monorepo:
Plugin externals contract:
.rspack/pluginExternals.jsis the single canonical list of packages the host shares with plugins at runtime; per-package builds, scaffolded templates, and the runtime loader all derive from it, with parity tests (platform/create-ohif/tests/externals-parity.test.mjs) enforcing that template externals, host-provided globals, and template peerDependencies stay in sync.@ohif/core,@ohif/extension-default, and@ohif/extension-cornerstone(through 3.14.0-beta.36) each bundle a full copy of React because their per-package externals omitted it. The tarball guard could not detect this because CI's quick builds produce no source maps;verify-tarballsnow also scans bundle text for React markers, and the PR gate builds with maps.rsbuild 2 pipeline consolidation: the
.webpackconfig dirs are renamed to.rspack, rsbuild is upgraded to 2.x, and production and dev builds converge on a single repo-rootrsbuild.config.tspipeline (fonts as native asset modules, e2e coverage instrumentation restored, singleton aliases deduped, Tailwind made plugin-aware).Runtime plugin loading with gating: prebuilt UMD plugins declared in
window.config.extensions[]load through runtime plugin descriptors with host-shared globals and an audit trail; dynamic loading is gated, invalidpluginConfig.jsonfails fast against a JSON schema, anddangerouslyUseDynamicConfigrequires an explicit regex.windowunder its package name (whole module, named exports included) once it has loaded, so UMD runtime plugins can import them. These globals carry no compatibility promise; the SDK list inruntimeShared.tsstays the stable contract. Within one load, built-in plugins load before runtime plugins. Extensions marked"default": falseload only when a mode needs them; to import one from a runtime plugin at startup, list it by name inwindow.config.extensions(see Runtime plugins in the docs).create-ohif scaffolding:
pnpm create ohiftemplates for extensions, modes, workspaces, and deployment, verified end to end by a scaffold smoke harness (scaffold, install, build, UMD global contract, in-scaffold tests).ohif-plugin CLI helper: one-command add/remove of plugins plus a
doctorcheck; the legacy OHIF CLI is removed and docs repointed at the pluginConfig flow.Publish surface tiering: only the SDK packages (
@ohif/core,@ohif/ui-next,@ohif/i18n,@ohif/extension-default,@ohif/extension-cornerstone) remain publishable; the rest of the workspace is marked private.@ohif/uiis deprecated and no longer published. The legacy UI library is frozen and markedprivate: true; no3.14.0or later version will appear on npm. It is also not on the host-shared list, so runtime plugins cannot import it. Integrators who depend on@ohif/uishould port to@ohif/ui-next(the APIs differ).create-ohif migrateflags every@ohif/uiimport and peer dependency it finds and says so. Inside the viewer,@ohif/uiremains a dependency ofplatform/apponly for the deprecatedLegacyWorkListroute.Docs: guides for out-of-tree development, scaffolding, publishing, runtime plugins and CSP deployment, porting/de-forking, and the extension gallery.
Merge Order / Rebase Details
Rebased onto master after #6164 landed. The reconcile keeps the
.rspack/layout and the single rsbuild pipeline, removesREACT_COMPILER=offfrom the UMD builds per master's compiler policy, pins Rspack to the 2.1.10 that@rsbuild/corebundles, and restoresLegacyWorkListbehindworkList.variantfollowing master's deprecation plan.Testing
platform/create-ohifsuite and the app contract suites pass locally.node platform/create-ohif/scripts/scaffold-smoke.mjsis the documented manual end-to-end gate for the scaffolding flow.Testing Performed
Verified locally on the merged tree:
/plugins/, with cache headers, 404 behavior, the version gate, and CSP on and off checked. The doc fixes in the d4d219b commit came from this run.Summary by CodeRabbit
New Features
create-ohifto scaffold workspaces, extensions, modes, and deployments, and migrate older plugins.Security
Build and Publishing
Documentation
Migration
create-ohifand the plugin management commands instead.