Skip to content

NextJsWithCornerstone3D4Version#4

Open
radhe65gupta wants to merge 2 commits into
cornerstonejs:mainfrom
radhe65gupta:NextJsWithCornerstone3D4Version
Open

NextJsWithCornerstone3D4Version#4
radhe65gupta wants to merge 2 commits into
cornerstonejs:mainfrom
radhe65gupta:NextJsWithCornerstone3D4Version

Conversation

@radhe65gupta

@radhe65gupta radhe65gupta commented Jun 3, 2026

Copy link
Copy Markdown

Modernizes the template to Next.js 16 + Cornerstone3D 4.x and the current React / Tailwind / ESLint majors. Adds
next/dynamic({ ssr: false }) wrappers so cornerstone never runs
on the server, and pins the bundler to webpack (Next 16 ships
Turbopack as the default, but it can't handle the codec
require('fs') calls or the @icr/polyseg-wasm URL import).

Stack updates

  • next: 14 → 16.2.7 — Turbopack default overridden via
    --webpack in dev / build scripts
  • @cornerstonejs/core / tools / dicom-image-loader:
    2.19.11 → 4.22.13
  • react / react-dom: 18 → 19
  • tailwindcss: 3 → 4 (CSS-first config;
    @tailwindcss/postcss plugin; tailwind.config.ts dropped)
  • eslint: 8 → 9 + eslint-config-next 14 → 16 (flat
    config via FlatCompat; .eslintrc.jsoneslint.config.mjs)
  • typescript: 5 → 6; dcmjs and dicomweb-client bumped
    to current minors

Page structure

  • app/page.tsx and app/polyseg/page.tsx are thin "use client" wrappers that load sibling Viewer.tsx files via
    next/dynamic(..., { ssr: false }). Fixes the SSR/prerender
    crash on Next 16 where top-level cornerstone imports touch
    browser APIs.
  • Awaits viewport.setVolumes(...) before viewport.render()
    (the v3+ signature is async).
  • Both routes now point at the live
    d14fa38qiwhyfd.cloudfront.net endpoint; the older
    d3t6nz73ql33tx host no longer resolves.

node_modules patches (via patch-package)

Webpack-dev's eval(__webpack_require__.ts("...")) chunk format
can't be loaded as an ES module worker, and one specific
Emscripten codec (~125 KB openjph) won't parse inside that eval
wrapper. Three minimal patches land in patches/:

  1. @cornerstonejs/dicom-image-loaderinit.js: drop { type: 'module' } from the worker constructor.
  2. @cornerstonejs/toolsutilities/registerComputeWorker.js:
    same drop.
  3. @cornerstonejs/dicom-image-loader
    shared/decoders/decodeHTJ2K.js: stub out, removing the
    @cornerstonejs/codec-openjph/wasmjs import (HTJ2K decoding now
    throws if invoked — not used by the demo dataset). Restore
    upstream when a clean fix for the openjph dev-chunk parse is
    found.

"postinstall": "patch-package" keeps these applied across
reinstalls. Full rationale + refresh steps are in CLAUDE.md.

Test plan

  • npm install (verify patches apply cleanly)
  • npm run build succeeds and prerenders 5 routes
  • npx tsc --noEmit clean
  • npm run dev, load / — sagittal CT volume renders, no
    console errors
  • Load /polyseg — axial + sagittal + 3D viewports render;
    brush works; "Convert to 3D" produces a surface

Summary by CodeRabbit

  • Chores

    • Upgraded Next.js to v16.2, React to v19.2, and Cornerstone libraries to v4.22 with modernized build tooling.
    • Updated ESLint configuration format and PostCSS plugin setup for enhanced compatibility.
  • Refactor

    • Reorganized rendering logic into dedicated components with server-side rendering disabled for improved code structure.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR upgrades the project to Next.js 16, React 19, and Cornerstone3D 4.22.13, migrates to Tailwind v4 CSS imports and ESLint flat config, adds Turbopack configuration, and refactors app pages to extract Cornerstone3D initialization logic into separate dynamically-loaded Viewer components.

Changes

Upgrade and Refactoring

Layer / File(s) Summary
Dependencies and TypeScript/ESLint configuration
package.json, tsconfig.json, eslint.config.mjs, .eslintrc.json
Dependencies upgraded: Next.js 14.2.4 → 16.2.7, React 18 → 19.2.7, Cornerstone3D 2.19.11 → 4.22.13, and DICOM packages updated. Lint script changed from next lint to eslint. TypeScript JSX transform updated to react-jsx with ES2017 target. ESLint migrated to flat config in eslint.config.mjs with core-web-vitals preset and ignores patterns; .eslintrc.json removed.
Build configuration and Tailwind v4 styling
next.config.mjs, empty.ts, postcss.config.mjs, app/globals.css, tailwind.config.ts
Turbopack configuration added to next.config.mjs for lightningcss server package, fs module aliasing to empty.ts, and *.wasm asset handling. Global CSS updated to Tailwind v4 @import "tailwindcss" syntax. PostCSS plugin switched from tailwindcss to @tailwindcss/postcss. tailwind.config.ts removed as Tailwind v4 uses defaults.
Root Viewer component extraction
app/Viewer.tsx, app/page.tsx
New client-side Viewer component initializes Cornerstone3D rendering engine, registers tools, builds DICOM imageIds and in-memory volume, creates single orthographic CT viewport, and renders 512×512 black viewport container. app/page.tsx simplified to dynamic import Viewer with SSR disabled and render it.
Polyseg Viewer component extraction with segmentation
app/polyseg/Viewer.tsx, app/polyseg/page.tsx
New client-side Viewer component initializes Cornerstone3D with three viewports (axial, sagittal, 3D), loads DICOM volume with CT-Bone preset, creates derived labelmap segmentation volume, applies labelmap representation to orthographic viewports, and provides "Convert to 3D" button to add surface representation to 3D viewport. app/polyseg/page.tsx simplified to dynamic import Viewer with SSR disabled.

🎯 4 (Complex) | ⏱️ ~75 minutes

🐰 Cornerstone ascends to version four,
React and Next join the migration,
Tailwind imports anew,
Two Viewer components extracted clean—
Ready for the CLI linter's gaze! 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'NextJsWithCornerstone3D4Version' is directly related to the PR's main objective: modernizing Next.js to v16.2.7 and Cornerstone3D to v4.x, along with related major dependency upgrades.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/polyseg/Viewer.tsx`:
- Around line 194-203: The "Convert to 3D" button can be clicked before async
initialization completes causing
segmentation.addSegmentationRepresentations(viewportId3, ...) to run on
uninitialized state; add a boolean ready flag (e.g., is3DReady or
segmentationReady) that is set true only after your async effect successfully
registers segmentation and the 3D viewport, use that flag to disable the button
(disabled={!is3DReady}) and also guard the onClick handler to no-op if not
ready; set the flag to false on init start and only true on success (and keep
false on failure) so calls to segmentation.addSegmentationRepresentations,
viewportId3, and csToolsEnums.SegmentationRepresentations.Surface are only
invoked when ready.
- Around line 46-190: The setup() async block should be made resilient: surround
its body with try/catch/finally, ensuring you reset running.current = false in
the finally block so a failed init can be retried; catch should log or surface
the error. Add a useEffect cleanup function that runs on unmount to tear down
Cornerstone resources created during setup: call renderingEngine.destroy() (or
equivalent) if renderingEngine was created, and remove/destroy toolGroup1 and
toolGroup2 from ToolGroupManager (or call any ToolGroupManager.destroy/remove
methods) to avoid leaking tool groups; store renderingEngine, toolGroup1,
toolGroup2 in refs so the cleanup can access them. Also ensure any UI actions
that operate on viewports/segmentations check running.current or a "ready" flag
before executing.

In `@app/Viewer.tsx`:
- Around line 20-81: Wrap the async init in a try/finally so running.current is
reset on any error (set running.current = true before try, set to false in
finally) and add a cleanup function returned from the useEffect that tears down
the created resources: call renderingEngine.destroy() (and undo enableElement if
needed) and stop/cleanup the volume/loader if applicable; locate the async setup
in Viewer.tsx (the setup function inside useEffect), the running ref
(running.current), the RenderingEngine instance (new
RenderingEngine(renderingEngineId) / renderingEngine.enableElement), and the
created volume (volume.load / volumeId) and ensure those are cleaned up in the
returned cleanup callback and on init failure.

In `@package.json`:
- Line 9: Update the package.json "lint" script to call ESLint directly instead
of the removed "next lint"; replace the value of the "lint" script key so it
invokes the eslint CLI against the project source (include the relevant file
globs/extensions like .js, .jsx, .ts, .tsx and any desired flags such as --ext
and optionally --fix or --max-warnings=0) so running npm run lint works under
Next.js 16.

In `@patches/`@cornerstonejs+dicom-image-loader+4.22.13.patch:
- Around line 126-135: The patch unconditionally disables HTJ2K by making
initialize() a no-op and making decodeAsync() always throw, which will cause
runtime failures for HTJ2K-encoded studies; update the module so it either
proactively rejects unsupported transfer syntaxes with a clear error or restores
codec support. Modify decodeAsync (and/or the codec selection path that calls
initialize) to inspect the incoming transferSyntaxUID or codec identifier and:
if it is HTJ2K (e.g., the HTJ2K/JP2K transferSyntax UID), return a rejected
Promise with a descriptive Error stating that HTJ2K is unsupported in this build
(including the UID); otherwise, preserve existing behavior for other codecs (or
re-wire to call the real `@cornerstonejs/codec-openjph` decoder if you choose to
restore support). Ensure initialize still advertises availability state
consistent with decodeAsync (so callers can preflight availability).
- Around line 8-10: The worker is being instantiated without the ESM flag which
breaks module-based workers; restore the module worker contract by adding back
the `{ type: 'module' }` option when constructing the Worker in the workerFn
that loads `decodeImageFrameWorker.js` (i.e., ensure new Worker(new
URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' }) is
used), and instead address bundler/asset resolution (or produce a non-ESM build)
rather than changing the runtime worker invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4022501a-aeaa-4895-bb25-9a519959c4e6

📥 Commits

Reviewing files that changed from the base of the PR and between 51b5573 and 7b15e8a.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (13)
  • .eslintrc.json
  • app/Viewer.tsx
  • app/globals.css
  • app/page.tsx
  • app/polyseg/Viewer.tsx
  • app/polyseg/page.tsx
  • eslint.config.mjs
  • package.json
  • patches/@cornerstonejs+dicom-image-loader+4.22.13.patch
  • patches/@cornerstonejs+tools+4.22.13.patch
  • postcss.config.mjs
  • tailwind.config.ts
  • tsconfig.json
💤 Files with no reviewable changes (2)
  • .eslintrc.json
  • tailwind.config.ts

Comment thread app/polyseg/Viewer.tsx
Comment on lines +46 to +190
useEffect(() => {
const setup = async () => {
if (running.current) {
return
}
running.current = true

await csRenderInit()
await csToolsInit()
dicomImageLoaderInit({ maxWebWorkers: 1 })
// Add tools to Cornerstone3D
addTool(BrushTool)

// Define tool groups to add the segmentation display tool to
toolGroup1 = ToolGroupManager.createToolGroup(toolGroupId)
toolGroup2 = ToolGroupManager.createToolGroup(toolGroupId2)

// Segmentation Tools
toolGroup1.addToolInstance("SphereBrush", BrushTool.toolName, {
activeStrategy: "FILL_INSIDE_SPHERE",
})
toolGroup1.addToolInstance("EraserBrush", BrushTool.toolName, {
activeStrategy: "ERASE_INSIDE_SPHERE",
})

toolGroup1.setToolActive("SphereBrush", {
bindings: [
{
mouseButton: MouseBindings.Primary, // Middle Click
},
],
})

// Get Cornerstone imageIds for the source data and fetch metadata into RAM
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
SeriesInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
})

// Define a volume in memory
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds,
})

// Instantiate a rendering engine
const renderingEngineId = "myRenderingEngine"
renderingEngine = new RenderingEngine(renderingEngineId)

const viewportInputArray = [
{
viewportId: viewportId1,
type: ViewportType.ORTHOGRAPHIC,
element: elementRef1.current,
defaultOptions: {
orientation: Enums.OrientationAxis.AXIAL,
},
},
{
viewportId: viewportId2,
type: ViewportType.ORTHOGRAPHIC,
element: elementRef2.current,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
},
},
{
viewportId: viewportId3,
type: ViewportType.VOLUME_3D,
element: elementRef3.current,
defaultOptions: {
background: CONSTANTS.BACKGROUND_COLORS.slicer3D,
},
},
]

renderingEngine.setViewports(viewportInputArray)

toolGroup1.addViewport(viewportId1, renderingEngineId)
toolGroup1.addViewport(viewportId2, renderingEngineId)
toolGroup2.addViewport(viewportId3, renderingEngineId)

// Set the volume to load
await volume.load()

// Set volumes on the viewports
await setVolumesForViewports(
renderingEngine,
[{ volumeId }],
[viewportId1, viewportId2, viewportId3]
)

// set the anatomy at first invisible
const volumeActor = renderingEngine
.getViewport(viewportId3)
.getDefaultActor().actor as Types.VolumeActor
utilities.applyPreset(
volumeActor,
CONSTANTS.VIEWPORT_PRESETS.find((preset) => preset.name === "CT-Bone")
)
volumeActor.setVisibility(false)

// Add some segmentations based on the source data volume
// Create a segmentation of the same resolution as the source data
await volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, {
volumeId: segmentationId,
})

// Add the segmentations to state
await segmentation.addSegmentations([
{
segmentationId,
representation: {
// The type of segmentation
type: csToolsEnums.SegmentationRepresentations.Labelmap,
// The actual segmentation data, in the case of labelmap this is a
// reference to the source volume of the segmentation.
data: {
volumeId: segmentationId,
},
},
},
])

// Add the segmentation representation to the viewports
const segmentationRepresentation = {
segmentationId,
type: csToolsEnums.SegmentationRepresentations.Labelmap,
}

await segmentation.addLabelmapRepresentationToViewportMap({
[viewportId1]: [segmentationRepresentation],
[viewportId2]: [segmentationRepresentation],
})

// Render the image
renderingEngine.render()
}

setup()

// Create a stack viewport
}, [elementRef1, elementRef2, elementRef3, running])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add init error recovery and Cornerstone teardown to prevent stale/non-retryable viewer state

In app/polyseg/Viewer.tsx the useEffect async setup sets running.current = true, performs multiple awaited Cornerstone init/cache/render steps, but provides no try/finally to clear running.current on failure and no effect cleanup to destroy the created RenderingEngine/tool groups. This can leak resources and leave the viewer stuck after transient init errors or unmount/remount (Fast Refresh/navigation).

Wrap setup in try/catch/finally (reset running.current in finally) and add useEffect cleanup that destroys the RenderingEngine and any created ToolGroups (or avoid long-lived module singletons by keeping them in component refs). Also gate any UI actions that depend on the viewports/segmentations so they can’t run until setup finishes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/polyseg/Viewer.tsx` around lines 46 - 190, The setup() async block should
be made resilient: surround its body with try/catch/finally, ensuring you reset
running.current = false in the finally block so a failed init can be retried;
catch should log or surface the error. Add a useEffect cleanup function that
runs on unmount to tear down Cornerstone resources created during setup: call
renderingEngine.destroy() (or equivalent) if renderingEngine was created, and
remove/destroy toolGroup1 and toolGroup2 from ToolGroupManager (or call any
ToolGroupManager.destroy/remove methods) to avoid leaking tool groups; store
renderingEngine, toolGroup1, toolGroup2 in refs so the cleanup can access them.
Also ensure any UI actions that operate on viewports/segmentations check
running.current or a "ready" flag before executing.

Comment thread app/polyseg/Viewer.tsx
Comment on lines +194 to +203
<button
onClick={() => {
// add the 3d representation to the 3d toolgroup
segmentation.addSegmentationRepresentations(viewportId3, [
{
segmentationId,
type: csToolsEnums.SegmentationRepresentations.Surface,
},
])
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable “Convert to 3D” until initialization finishes.

The button is active on first paint, but the segmentation state and 3D viewport are only registered later in the async effect. A quick click can call addSegmentationRepresentations against uninitialized state and break the main demo flow. Track a ready flag and disable the button until setup succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/polyseg/Viewer.tsx` around lines 194 - 203, The "Convert to 3D" button
can be clicked before async initialization completes causing
segmentation.addSegmentationRepresentations(viewportId3, ...) to run on
uninitialized state; add a boolean ready flag (e.g., is3DReady or
segmentationReady) that is set true only after your async effect successfully
registers segmentation and the 3D viewport, use that flag to disable the button
(disabled={!is3DReady}) and also guard the onClick handler to no-op if not
ready; set the flag to false on init start and only true on success (and keep
false on failure) so calls to segmentation.addSegmentationRepresentations,
viewportId3, and csToolsEnums.SegmentationRepresentations.Surface are only
invoked when ready.

Comment thread app/Viewer.tsx
Comment on lines +20 to +81
useEffect(() => {
const setup = async () => {
if (running.current) {
return
}
running.current = true

await csRenderInit()
await csToolsInit()
dicomImageLoaderInit({ maxWebWorkers: 1 })

// Get Cornerstone imageIds and fetch metadata into RAM
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
SeriesInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
})

// Instantiate a rendering engine
const renderingEngineId = "myRenderingEngine"
const renderingEngine = new RenderingEngine(renderingEngineId)
const viewportId = "CT"

const viewportInput = {
viewportId,
type: Enums.ViewportType.ORTHOGRAPHIC,
element: elementRef.current,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
},
}

renderingEngine.enableElement(viewportInput)

// Get the stack viewport that was created
const viewport = renderingEngine.getViewport(
viewportId
) as Types.IVolumeViewport

// Define a volume in memory
const volumeId = "streamingImageVolume"
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds,
})

// Set the volume to load
// @ts-ignore
volume.load()

// Set the volume on the viewport and it's default properties
await viewport.setVolumes([{ volumeId }])

// Render the image
viewport.render()
}

setup()

// Create a stack viewport
}, [elementRef, running])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect app/Viewer.tsx around the referenced lines
ls -la
sed -n '1,200p' app/Viewer.tsx

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 3937


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any existing cleanup/destroy patterns for RenderingEngine
rg -n "RenderingEngine|destroy\(\)|enableElement\(" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' app/polyseg/Viewer.tsx

echo "---- destroy() usages ----"
rg -n "destroy\(" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 6148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- app/Viewer.tsx: useEffect cleanup / return () ----"
rg -n "useEffect\\(|return \\(\\) =>|return \\(\\s*\\)" -S app/Viewer.tsx

echo "---- app/polyseg/Viewer.tsx: effect cleanup patterns ----"
rg -n "useEffect\\(|return \\(\\) =>|return \\(\\s*\\)|destroy\\(" -S app/polyseg/Viewer.tsx

echo "---- repo-wide destroy() search (should confirm none) ----"
rg -n "destroy\\(" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "myRenderingEngine" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the RenderingEngine implementation in `@cornerstonejs/core`
rg -n "class RenderingEngine" node_modules/@cornerstonejs/core -S || true
rg -n "RenderingEngine.*destroy\\(|destroy\\(\\).*RenderingEngine|destroy\\(" node_modules/@cornerstonejs/core -S | head -n 50

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 2532


Return cleanup from the effect + reset running.current on init failure to avoid leaks and “stuck” init.

In app/Viewer.tsx, running.current is set to true and never cleared if any awaited init step throws (no try/catch), so the viewer can get stuck in the “already running” state. Also, the effect doesn’t return a cleanup, so the created RenderingEngine is never torn down on unmount—yet RenderingEngine in @cornerstonejs/core provides a destroy() API and the repo currently has no destroy() calls.

♻️ Proposed fix
   useEffect(() => {
+    let cancelled = false
+    let renderingEngine: RenderingEngine | undefined
+
     const setup = async () => {
-      if (running.current) {
+      if (running.current || !elementRef.current) {
         return
       }
+
       running.current = true
-
-      await csRenderInit()
-      await csToolsInit()
-      dicomImageLoaderInit({ maxWebWorkers: 1 })
-
-      // Get Cornerstone imageIds and fetch metadata into RAM
-      const imageIds = await createImageIdsAndCacheMetaData({
-        StudyInstanceUID:
-          "1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
-        SeriesInstanceUID:
-          "1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
-        wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
-      })
-
-      // Instantiate a rendering engine
-      const renderingEngineId = "myRenderingEngine"
-      const renderingEngine = new RenderingEngine(renderingEngineId)
-      const viewportId = "CT"
-
-      const viewportInput = {
-        viewportId,
-        type: Enums.ViewportType.ORTHOGRAPHIC,
-        element: elementRef.current,
-        defaultOptions: {
-          orientation: Enums.OrientationAxis.SAGITTAL,
-        },
-      }
-
-      renderingEngine.enableElement(viewportInput)
-
-      // Get the stack viewport that was created
-      const viewport = renderingEngine.getViewport(
-        viewportId
-      ) as Types.IVolumeViewport
-
-      // Define a volume in memory
-      const volumeId = "streamingImageVolume"
-      const volume = await volumeLoader.createAndCacheVolume(volumeId, {
-        imageIds,
-      })
-
-      // Set the volume to load
-      // `@ts-ignore`
-      volume.load()
-
-      // Set the volume on the viewport and it's default properties
-      await viewport.setVolumes([{ volumeId }])
-
-      // Render the image
-      viewport.render()
+
+      try {
+        await csRenderInit()
+        await csToolsInit()
+        dicomImageLoaderInit({ maxWebWorkers: 1 })
+
+        const imageIds = await createImageIdsAndCacheMetaData({
+          StudyInstanceUID:
+            "1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
+          SeriesInstanceUID:
+            "1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
+          wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
+        })
+
+        const renderingEngineId = "myRenderingEngine"
+        renderingEngine = new RenderingEngine(renderingEngineId)
+        const viewportId = "CT"
+
+        renderingEngine.enableElement({
+          viewportId,
+          type: Enums.ViewportType.ORTHOGRAPHIC,
+          element: elementRef.current,
+          defaultOptions: {
+            orientation: Enums.OrientationAxis.SAGITTAL,
+          },
+        })
+
+        const viewport = renderingEngine.getViewport(
+          viewportId
+        ) as Types.IVolumeViewport
+
+        const volumeId = "streamingImageVolume"
+        const volume = await volumeLoader.createAndCacheVolume(volumeId, {
+          imageIds,
+        })
+
+        // `@ts-ignore`
+        volume.load()
+
+        if (cancelled) {
+          return
+        }
+
+        await viewport.setVolumes([{ volumeId }])
+
+        if (!cancelled) {
+          viewport.render()
+        }
+      } catch (error) {
+        running.current = false
+        throw error
+      }
     }
 
-    setup()
+    void setup()
 
-    // Create a stack viewport
-  }, [elementRef, running])
+    return () => {
+      cancelled = true
+      running.current = false
+      renderingEngine?.destroy()
+    }
+  }, [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Viewer.tsx` around lines 20 - 81, Wrap the async init in a try/finally so
running.current is reset on any error (set running.current = true before try,
set to false in finally) and add a cleanup function returned from the useEffect
that tears down the created resources: call renderingEngine.destroy() (and undo
enableElement if needed) and stop/cleanup the volume/loader if applicable;
locate the async setup in Viewer.tsx (the setup function inside useEffect), the
running ref (running.current), the RenderingEngine instance (new
RenderingEngine(renderingEngineId) / renderingEngine.enableElement), and the
created volume (volume.load / volumeId) and ensure those are cleaned up in the
returned cleanup callback and on init failure.

Comment thread package.json Outdated
Comment on lines +8 to +10
const workerFn = () => {
- const instance = new Worker(new URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' });
+ const instance = new Worker(new URL('./decodeImageFrameWorker.js', import.meta.url));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the patch content
sed -n '1,160p' "patches/@cornerstonejs+dicom-image-loader+4.22.13.patch"

# 2) Locate any references to decodeImageFrameWorker and worker creation
rg -n "decodeImageFrameWorker\.js|new Worker\(" -S . || true

# 3) If the patched output exists in-repo, inspect the worker file content
# (common locations: dist/esm, dist/cjs, or similar)
fd -a "decodeImageFrameWorker\.js" . || true

# 4) If present, check whether decodeImageFrameWorker.js contains ESM imports/exports
# and whether any other build keeps { type: 'module' }
worker_file="$(fd -a "decodeImageFrameWorker\.js" . | head -n 1 || true)"
if [[ -n "${worker_file}" ]]; then
  echo "---- inspecting: ${worker_file} ----"
  # Print first ~120 lines to spot imports/exports and module usage
  sed -n '1,140p' "$worker_file"
fi

# 5) Search for HTJ2K removal/stub throw in the codebase
rg -n "HTJ2K|tj2k|htj2k|throw new Error|stub" -S . || true

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 1990


🌐 Web query:

@cornerstonejs/dicom-image-loader@4.22.13 decodeImageFrameWorker.js new Worker type: 'module'

💡 Result:

The code snippet new Worker(new URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' }) is used within @cornerstonejs/dicom-image-loader (specifically in the init function) to instantiate a Web Worker as an ES module [1][2][3]. This implementation relies on the host environment (such as Vite or Webpack) to resolve the URL and bundle the worker file correctly [2][3]. Common technical issues and solutions related to this implementation include: 1. Build Tool Integration: In many modern frameworks (like Vite), this pattern can cause issues where the worker file is not correctly located, served, or bundled, leading to 404 errors or module resolution failures [2][4][3]. 2. Dependency Optimization: Users often resolve these errors by configuring their build tool (e.g., vite.config.ts) to: - Exclude @cornerstonejs/dicom-image-loader from optimizeDeps [2][4][3]. - Include dicom-parser in optimizeDeps [2][3]. - Use plugins like @originjs/vite-plugin-commonjs to handle interoperability issues between CommonJS dependencies and ESM [2][4][3]. 3. Customization: Recent updates have introduced the ability to customize the webWorkerUrl via the init configuration, allowing developers to manually define how the worker is instantiated if the default behavior fails in their specific build environment [5]. If you are encountering errors related to this worker, ensure your build configuration aligns with the recommended settings for Cornerstone3D, as the library expects the build environment to handle the import.meta.url resolution for the worker script [1][3].

Citations:


🌐 Web query:

cornerstonejs dicom-image-loader decodeImageFrameWorker type module import.meta.url

💡 Result:

In Cornerstone3D (@cornerstonejs/dicom-image-loader), the use of new URL('./decodeImageFrameWorker.js', import.meta.url) is part of the library's internal mechanism for initializing web workers as ES modules [1]. This specific syntax relies on the bundler (e.g., Vite, Webpack) to resolve the worker file path relative to the module's location at build time [2][3]. Common issues and resolutions related to this implementation include: 1. Bundler Configuration: Developers frequently encounter issues where the bundler fails to correctly resolve the worker path or misinterprets the module type [4][5]. Official documentation recommends specific configurations for popular frameworks [2]. For example, when using Vite, you must often exclude @cornerstonejs/dicom-image-loader from dependency optimization (optimizeDeps) to ensure the web worker is handled correctly [5][2]. 2. Customization: Due to the challenges of static path resolution in different project environments, newer versions of the library (introduced in PR #1982) provide the ability to customize the path to the web worker [6]. You can override the default worker loading behavior by providing a custom webWorkerUrl function in the init configuration [6]: init({ webWorkerUrl: => { return new Worker( new URL('../path-to-your-worker.ts', import.meta.url), { type: 'module' }); }, //... other configuration }); 3. Environment Awareness: The use of import.meta.url can behave unexpectedly in certain environments (e.g., some SSR frameworks or specific dev-server configurations), sometimes resulting in undefined values [4]. If you are experiencing runtime errors where the worker fails to load, ensure your bundler is configured to support ES module workers and that any necessary WASM assets are being correctly copied to your public/assets directory [6][2].

Citations:


🌐 Web query:

site:github.com cornerstonjs dicom-image-loader decodeImageFrameWorker.js

💡 Result:

The decodeImageFrameWorker.js file is a critical component of the @cornerstonejs/dicom-image-loader library in the Cornerstone3D ecosystem [1]. It serves as the entry point for a dedicated Web Worker that performs CPU-intensive tasks—specifically, the decompression and decoding of DICOM image frames—off the main browser thread [2]. By utilizing this worker, Cornerstone3D ensures that high-latency operations, such as decompressing complex formats (e.g., JPEG 2000, JPEG-LS), do not block the UI thread, thereby maintaining smooth application performance [2]. Key aspects of decodeImageFrameWorker.js include: Functionality: It executes the 'decodeTask', which takes raw DICOM pixel data and applies the necessary codec logic to generate displayable image frames [3][2]. Implementation: The loader initializes this worker via the library's WebWorkerManager [1]. In recent versions, this is often handled by creating a new Worker instance using a dynamic import: const workerFn = => { const instance = new Worker( new URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' }); return instance; }; Common Configuration & Build Issues: Because this file is a separate module, developers frequently encounter build-related issues when using modern bundlers like Vite, Webpack, or Angular's build system [4][5]. If the build tool does not correctly resolve or include the worker file in the output assets, the application will fail to load the worker, resulting in pending promises or runtime errors [4][5]. To resolve these issues, developers often need to: 1. Exclude the dicom-image-loader package from the bundler's dependency optimizer (e.g., in Vite) [4]. 2. Manually configure the path to the worker or override the webWorkerFn to ensure the file is served correctly by the application's dev server or build pipeline [6]. If you are experiencing issues with the worker failing to load, ensure that your build configuration specifically accounts for the worker module and that any required WebAssembly (.wasm) codec files are accessible at the specified paths [6][4].

Citations:


🌐 Web query:

@cornerstonejs/dicom-image-loader 4.22.13 patches module worker type module

💡 Result:

In @cornerstonejs/dicom-image-loader, the use of Web Workers with the { type: 'module' } option is a core part of its architecture for offloading CPU-intensive image decompression [1][2][3]. The library utilizes a WebWorkerManager, which internally initializes workers using the ES module syntax [1][2]. Specifically, the initialization logic often employs the following pattern to create a worker as a module: const workerFn = => { const instance = new Worker( new URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' }); return instance; }; This implementation allows the worker to leverage ESM features, which is essential for the library's integration with modern bundlers like Vite [2][4]. Regarding version 4.22.13: - Version 4.22.13 is a specific release within the 4.x series of @cornerstonejs/dicom-image-loader [5]. - The library does not typically list "patches for module worker type module" as a standalone feature in changelogs for minor version bumps, as this architecture has been the established standard since the transition to the 2.x API [6][2]. - If you are encountering issues related to the worker's type: 'module' configuration (such as 404 errors during asset loading or bundler resolution issues), these are often related to path resolution—specifically how the bundler resolves the location of the worker file relative to the entry point [7][4]. - Users can override the default worker loading behavior by providing a custom webWorkerUrl factory function in the init configuration, which allows for explicit path handling for different build environments (e.g., Angular or Vite) [8]. For most production use cases, ensuring your bundler is configured to handle.worker.js files and correctly emitting them to your output directory is critical for resolving errors related to the worker's module type [2][4].

Citations:


Don’t remove { type: "module" } from the ESM decode worker creation.

@cornerstonejs/dicom-image-loader initializes decodeImageFrameWorker.js as an ES-module worker (new URL(..., import.meta.url), plus { type: 'module' }). Dropping { type: 'module' } can make the browser interpret the worker as a classic script and fail at startup when ESM syntax is present. Fix the worker asset/bundling resolution (or target a non-ESM build) instead of changing the worker contract in dist/esm.

Relevant snippet
 const workerFn = () => {
-    const instance = new Worker(new URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' });
+    const instance = new Worker(new URL('./decodeImageFrameWorker.js', import.meta.url));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/`@cornerstonejs+dicom-image-loader+4.22.13.patch around lines 8 - 10,
The worker is being instantiated without the ESM flag which breaks module-based
workers; restore the module worker contract by adding back the `{ type: 'module'
}` option when constructing the Worker in the workerFn that loads
`decodeImageFrameWorker.js` (i.e., ensure new Worker(new
URL('./decodeImageFrameWorker.js', import.meta.url), { type: 'module' }) is
used), and instead address bundler/asset resolution (or produce a non-ESM build)
rather than changing the runtime worker invocation.

Comment thread patches/@cornerstonejs+dicom-image-loader+4.22.13.patch Outdated
@radhe65gupta

Copy link
Copy Markdown
Author

Now i remove patches

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
package.json (1)

9-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

eslint command missing target path.

The lint script runs eslint without specifying files to lint. While ESLint 9+ will lint the current directory by default when no files are specified, explicitly providing a path is clearer and avoids ambiguity. The previous review suggested eslint . which is the recommended pattern for Next.js 16 migration.

Proposed fix
-    "lint": "eslint"
+    "lint": "eslint ."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 9, The package.json "lint" script currently runs
"eslint" with no target; update the "lint" script value (the "lint" npm script)
to explicitly target the project root (e.g., change the command to "eslint .")
so ESLint runs against the repository files consistently and matches the
recommended Next.js migration pattern.
🧹 Nitpick comments (1)
eslint.config.mjs (1)

1-6: 💤 Low value

Typo in import variable name.

The import name nextCoreWebVitls is missing the 'a' in "Vitals". While this doesn't affect functionality, it reduces readability.

Proposed fix
-import nextCoreWebVitls from "eslint-config-next/core-web-vitals"
+import nextCoreWebVitals from "eslint-config-next/core-web-vitals"
 const eslintConfig = [
-    ...nextCoreWebVitls,
+    ...nextCoreWebVitals,
     {
         ignores: [".next/**","node_modules/**", "next-env.d.ts"],
-    },]
+    },
+]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@eslint.config.mjs` around lines 1 - 6, The import binding nextCoreWebVitls
has a typo; rename it to nextCoreWebVitals everywhere it's used (both in the
import statement and in the spread within eslintConfig) so the symbol matches
the intended word "Vitals" and improves readability; ensure you update the
identifier in the import line and the spread ...nextCoreWebVitals in the
eslintConfig array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@package.json`:
- Line 9: The package.json "lint" script currently runs "eslint" with no target;
update the "lint" script value (the "lint" npm script) to explicitly target the
project root (e.g., change the command to "eslint .") so ESLint runs against the
repository files consistently and matches the recommended Next.js migration
pattern.

---

Nitpick comments:
In `@eslint.config.mjs`:
- Around line 1-6: The import binding nextCoreWebVitls has a typo; rename it to
nextCoreWebVitals everywhere it's used (both in the import statement and in the
spread within eslintConfig) so the symbol matches the intended word "Vitals" and
improves readability; ensure you update the identifier in the import line and
the spread ...nextCoreWebVitals in the eslintConfig array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 79a3449e-0d3e-48e8-8b5f-3f3f2fa956c5

📥 Commits

Reviewing files that changed from the base of the PR and between 7b15e8a and ee769a2.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • empty.ts
  • eslint.config.mjs
  • next.config.mjs
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • empty.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant