Skip to content

feat: environment variables (to support fhircast)#5988

Draft
awatson1978 wants to merge 12 commits into
OHIF:masterfrom
awatson1978:fhircast-mvd
Draft

feat: environment variables (to support fhircast)#5988
awatson1978 wants to merge 12 commits into
OHIF:masterfrom
awatson1978:fhircast-mvd

Conversation

@awatson1978

@awatson1978 awatson1978 commented Apr 30, 2026

Copy link
Copy Markdown

Context

Environment-variable plugin composition (for the FHIRcast demo, and useful generally).

The bulk of the FHIRcast feature lives in an external OHIF extension:
https://github.com/node-on-fhir/nof-ohif-viewer

This PR adds only the upstream-side hooks that such an extension needs, with a deliberately small, merge-friendly surface:

  1. Environment-variable plugin injection — add extensions/modes at build time without editing tracked files.
  2. A tracked, layered plugin-config override (APP_PLUGIN_CONFIG) — pin a build's plugin set in its own file that includes and merges over the default, instead of editing the shared pluginConfig.json.
  3. A guard for late-registered data sources — let a mode activate a data source that an extension registers (e.g. during a SMART on FHIR launch) without crashing on first render.

A deployment opts in entirely through environment variables; the demo's extension, mode, and FHIR data source stay in nof-ohif-viewer.

Environment variables

This PR introduces three new environment variables. All are optional — none are required to build or run OHIF normally — and all are read at build time by platform/app/.webpack/writePluginImportsFile.js.

Variable Default Purpose
APP_PLUGIN_CONFIG '' (none) Select a tracked override plugin-config file that includes + merges over the default
EXTRA_EXTENSIONS '' (none) Inject extension package names into the plugin config
EXTRA_MODES '' (none) Inject mode package names into the plugin config
  • EXTRA_EXTENSIONS — Comma-separated extension package names appended to pluginConfig.extensions. Each entry may be suffixed with =<directory> to
    point at an out-of-tree package root (. resolves to the repo root, ~ to the home directory). Companion-mode auto-detection: if an injected extension bundles a mode in a mode/ subdirectory (with its own package.json), that mode is registered automatically — so EXTRA_EXTENSIONS=<ext> alone usually
    brings in both the extension and its mode.
  • EXTRA_MODES — Comma-separated mode package names, same =<directory> suffix option. Usually unnecessary thanks to companion-mode auto-detection above; reach for it to add a standalone mode (not bundled in an extension) or to override the auto-detected one.
  • APP_PLUGIN_CONFIG — Path to a tracked override plugin-config file (relative to platform/app/, or absolute, or ~-prefixed). The file may include other config files (a string or array of paths, resolved relative to the file itself) — typically the default pluginConfig.json — and layer its own
    extensions / modes / public over them. Plain arrays append (de-duped by package name; an override entry with the same name replaces the base one, so you can pin a version or flip default). immutability-helper command objects like { "$set": [...] } replace a list wholesale — the same notation OHIF's CustomizationService uses.

Precedence

The plugin config is assembled in three layers at build time, each overriding the previous:

1. pluginConfig.json            static, tracked default (never edited)
        ▼ included + merged by
2. APP_PLUGIN_CONFIG=<file>     tracked per-build override (include + $set / append)
        ▼ then appended by
3. EXTRA_EXTENSIONS / EXTRA_MODES   env name-injection, the last word

So a tracked file pins the reproducible baseline, and the env vars stay available for ad-hoc, 12-Factor-style additions on top.

Examples

1. Inject the FHIRcast extension — its mode is auto-detected:

EXTRA_EXTENSIONS=@ohif/extension-nof-ohif-viewer pnpm dev

2. Inject an out-of-tree extension by directory:

EXTRA_EXTENSIONS=@acme/extension-foo=~/code/acme-foo pnpm dev

3. Pin a build's plugin set in a tracked override file.

platform/app/pluginConfig.fhircast.json:

{
  "include": "./pluginConfig.json",
  "extensions": [
    { "packageName": "@ohif/extension-nof-ohif-viewer" }
  ],
  "modes": [
    { "packageName": "fhir-viewer" }
  ]
}
APP_PLUGIN_CONFIG=./pluginConfig.fhircast.json pnpm dev

4. Ship a minimal build — $set replaces the mode list wholesale:

{
  "include": "./pluginConfig.json",
  "modes": { "$set": [ { "packageName": "@ohif/mode-basic" } ] }
}

5. Combine a tracked baseline with an ad-hoc addition (env appended last):

APP_PLUGIN_CONFIG=./pluginConfig.fhircast.json \
EXTRA_EXTENSIONS=@acme/extension-experimental \
pnpm dev

A runnable, generic example ships at platform/app/pluginConfig.example.json, and all three variables are documented in platform/app/.env.

6. Load a remotely hosted extension:

platform/app/pluginConfig.remoteExtension.json:

{
  "include": "./pluginConfig.json",
  "extensions": [
    {
      "packageName": "@acme/extension-remote",
      "importPath": "https://cdn.example.com/acme-remote.umd.js",
      "globalName": "AcmeRemote"
    }
  ]
}
APP_PLUGIN_CONFIG=./pluginConfig.remoteExtension.json pnpm dev

Late-registered data source guard

platform/app/src/routes/Mode/Mode.tsx — A data source registered by an extension (for example one created during a SMART on FHIR launch) may not be active yet on the render where extension dependencies finish loading. ModeRoute now bails out and re-runs once the active data source appears (dataSource was
added to the effect's dependency array) instead of dereferencing an undefined data source. Without this guard, a mode that activates an extension-provided data source in onModeInit can crash on first render.

Changes & Results

Here's a summary of the changes — 4 source files, plus the regenerated lockfile:

platform/app/.webpack/writePluginImportsFile.js

Two build-time composition mechanisms, both feeding the same in-memory pluginConfig before the name, alias, asset-copy, and pluginImports.js caches are
built — so injected/merged plugins are treated identically to those declared in pluginConfig.json:

  • EXTRA_EXTENSIONS / EXTRA_MODES injection. Comma-separated package names — each optionally suffixed with =<directory> to point at an out-of-tree
    plugin root — are appended to the config. This is what lets the demo register @ohif/extension-nof-ohif-viewer and the node-on-fhir mode without editing
    tracked files. A companion mode bundled in the extension's mode/ subdirectory is auto-registered, so EXTRA_EXTENSIONS alone usually suffices.
  • APP_PLUGIN_CONFIG layered override. A new loadPluginConfig() assembles the config in three layers — static default → APP_PLUGIN_CONFIG override
    file → EXTRA_* env injection (last). mergePluginConfig() appends plain arrays (de-duped by package name, same-name entries replacing the base) and
    applies immutability-helper $set/$push/etc. command objects — the same notation CustomizationService uses. loadConfigWithIncludes() resolves an
    override's include chain (string or array, relative to the file) with a cycle guard and fail-fast errors. A deployment can thus pin a build's plugin set in
    its own tracked file without touching the shared default.

platform/app/pluginConfig.example.json (new)

A generic, runnable override that includes the default and demonstrates appending plugins plus the $set replace notation. Doubles as living
documentation.

platform/app/.env

Documents all three environment variables (EXTRA_EXTENSIONS, EXTRA_MODES, APP_PLUGIN_CONFIG) as commented examples.

platform/app/src/routes/Mode/Mode.tsx

Guards against dataSource being undefined at the point extension dependencies finish loading — which happens when a data source is registered late (e.g.
one entered via a SMART on FHIR launch). The effect now bails and re-runs once dataSource appears (it is added to the effect's dependency array) instead of
dereferencing undefined.

pnpm-lock.yaml

Regenerated against the workspace so pnpm install --frozen-lockfile stays green in CI (transitive @babel/* supports-color peer resolution; no dependency
additions or removals — immutability-helper was already a dependency via @ohif/core).


Overall: this PR integrates the "Node on FHIR" extension and mode into OHIF through environment-variable injection, adding SMART on FHIR client configuration, a WebSocket-capable FHIR proxy for local development, and a guard for data sources registered at SMART-launch time — without modifying any tracked plugin or data-source configuration files.

OHIF Preferences panels supports SMART registration

Screenshot 2026-04-30 at 3 48 31 AM

RIS reading worklist supports a SMART Launch functionality

Screenshot 2026-04-30 at 3 53 04 AM

OHIF extension receives the SMART Launch URL and fetches the FHIR data; Subscribe to FHIRcast

Screenshot 2026-04-30 at 3 52 21 AM

RIS updates the underlying ImagingStudy status; event bus triggers FHIRCast

Screenshot 2026-04-30 at 3 54 48 AM

OHIF receives events via FHIRCast

Screenshot 2026-04-30 at 3 55 18 AM

Testing

  1. Install Node on FHIR
  2. Install radiology-workflow, fhircast, and record-lifecycle repos into npmPackages directory
    3a.. See the following RIS Installation Instructions for a more detailed walkthrough.

3b.

# create a working directory 
mkdir demo
cd demo

# clone the node-on-fhir template
git clone https://github.com/node-on-fhir/core
cd core

# install the radiology workflow and fhircast modules
cd extensions/
git clone https://github.com/node-on-fhir/radiology-workflow
git clone https://github.com/node-on-fhir/fhircast
git clone https://github.com/node-on-fhir/record-lifecycle
  1. Run the RIS, probably with a command similar to this:
INITIALIZE_CONSENT_ENGINE=true CORS=http://localhost:3000 EXTRA_WORKFLOWS=@node-on-fhir/radiology-workflow,@node-on-fhir/fhircast-module,@node-on-fhir/record-lifecycle,@node-on-fhir/clinical:us-core,@node-on-fhir/admin-tools,@node-on-fhir/data-importer,@node-on-fhir/international-patient-summary meteor run --settings npmPackages/fhircast/settings/settings.fhircast.json --port 3100

Install OHIF with FHIR Datastore and FHIRCast

  1. Install OHIF/Viewers
  2. Change to this branch and fetch data
  3. Install the nof-ohif-viewer extension
# create a working directory 
cd demo

# install OHIF
git clone https://github.com/OHIF/Viewers
cd extensions
git clone https://github.com/node-on-fhir/ohif-fhir-viewer

The extension and mode are injected at dev-server start via the EXTRA_EXTENSIONS / EXTRA_MODES env vars.

EXTRA_EXTENSIONS=@ohif/extension-nof-ohif-viewer yarn dev
  1. Register OHIF/Viewers as a SMART on FHIR client (from Preferences config page)

  2. In the RIS:
    5.a Register a new user
    5.b Load a Synthea sample patient in with the data-importer
    5.c Select the patient from the Patient Directory
    5.d Use the Order Entry page to add an XR order
    5.e Go to the Tech Worklist, start and complete the exam; attach a .dcm file
    5.f Go to the Reading Worklist, launch the exam in OHIF

  3. Back in OHIF
    6.a Verify that dicom image loads
    6.b Subscribe to the patient (third tab)

  4. RIS again
    7.a. Read the exam; finalize

  5. Very that OHIF receives the event

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: macOS Tahoe (26.3.1)
  • Node version: v25.9.0
  • Browser: Chrome 147.0.7727.103 (arm64)

@netlify

netlify Bot commented Apr 30, 2026

Copy link
Copy Markdown

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit c9327d2
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a3b16df911951000814b107
😎 Deploy Preview https://deploy-preview-5988--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@awatson1978 awatson1978 marked this pull request as draft April 30, 2026 08:44
Comment thread platform/app/.env
Comment thread platform/app/public/config/default.js Outdated
Comment thread platform/app/pluginConfig.json Outdated
Comment thread platform/app/pluginConfig.json Outdated
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e7e710d-a3ac-4563-800e-c8cff12e520a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@awatson1978

Copy link
Copy Markdown
Author

FHIRCast support for MedPlum staged as draft pull request.
medplum/medplum#9353

@awatson1978 awatson1978 changed the title Feature/fhircast feature/fhircast Jun 1, 2026
@awatson1978 awatson1978 changed the title feature/fhircast feature/fhircast-v3.0.0 Jun 1, 2026
…lient id plumbing

Platform enablers for FHIRcast v3.0.0 / SMART on FHIR integrations that live
in out-of-tree plugins (e.g. github.com/node-on-fhir/nof-ohif-viewer):

- EXTRA_EXTENSIONS / EXTRA_MODES env vars append plugins to pluginConfig at
  build time, including out-of-tree packages via name=directory entries, so
  deployments can add plugins without editing tracked files
- /fhir-proxy dev-server proxy (FHIR_SERVER env var, WebSocket-enabled for
  FHIRcast subscriptions); env-configured PROXY_* entries now prepend to the
  proxy list instead of replacing it
- SMART_CLIENT_ID added to the DefinePlugin whitelist with a documented .env
  placeholder
- ModeRoute guards against the active data source not yet being registered
  when extension dependencies finish loading (SMART launch flows)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@awatson1978

Copy link
Copy Markdown
Author

Looks like we have a stale cache, due to the pnpm migration (#6031). If somebody with Netlify access could click the "Clear cache and retry" button, that will unjam this.

@wayfarer3130

The fix is one button, no code change: "Retry with clear cache" on deploy 6a2c945f309e05000815a199 in the ohif-dev Netlify project (Deploys → that deploy →
Retry → Clear cache and retry). With a fresh cache, pnpm lays down node_modules itself and the exec bits are correct.

If you don't have access to the ohif-dev project, this needs an OHIF maintainer — and it's worth telling them regardless: every PR preview whose cache
predates the pnpm migration will hit this same wall after a rebase. That context will help them recognize it instead of debugging each PR individually.

If you have a Netlify personal access token for that team, I can also trigger it via API:
curl -X POST -H "Authorization: Bearer $NETLIFY_TOKEN"
"https://api.netlify.com/api/v1/deploys/6a2c945f309e05000815a199/retry?clear_cache=true"

Nothing to change on our branch — it's correct as pushed. Still holding the two queued actions for your OK: posting the rewritten PR description (could fold
the cache note into it for the maintainers), and pushing audit-cleanup to the extension repo.

awatson1978 and others added 6 commits June 21, 2026 09:21
The chore(version) release-bot commits rewrote internal @ohif/* deps from
workspace:* to concrete 3.13.0-beta.92 across the workspace package.json
files but never regenerated the lockfile, so the lockfile specifiers still
read workspace:*. CI's default --frozen-lockfile install aborted with
ERR_PNPM_OUTDATED_LOCKFILE. Regenerate so specifiers match the manifests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@awatson1978 awatson1978 changed the title feature/fhircast-v3.0.0 feat: environment variables (to support fhircast) Jun 21, 2026
awatson1978 and others added 2 commits June 21, 2026 14:57
…IR extension

SMART_CLIENT_ID was a build-time DefinePlugin value and FHIR_SERVER / the
/fhir-proxy dev-server route were FHIR-specific hooks that don't belong in
upstream OHIF. Build-time bake-in also can't be changed without a rebuild,
which is wrong for a deployment-specific client ID. These now live in the
ohif-fhir-viewer extension as runtime config (window.config / Preferences /
URL params), with the dev proxy replaced by server-side CORS.

PR OHIF#5988 now carries only the generic core improvements: EXTRA_EXTENSIONS /
EXTRA_MODES injection and the Mode.tsx late-dataSource guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tension

When an EXTRA_EXTENSIONS package contains a `mode/` subdirectory with a
package.json, register that mode automatically — so `EXTRA_EXTENSIONS=<ext>`
alone pulls in the extension and its bundled mode, with no separate EXTRA_MODES
entry. An explicit EXTRA_MODES still wins (a mode already present by name is
left untouched), and extensions without a `mode/` are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
const os = require('os');
const path = require('path');

// Extra plugins injected via environment variables rather than

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My preference would be to have another pluginConfig.json file that gets pointed at with a separate ENV value, and can include the default pluginConfig. That way it can be tracked in the specific build without hitting the main set.
Then, as a customization, allow the modes and extensions for each mode list to be customizeable. The default versus optional is a bit tricky since it affects build time, but it could be setup so that a second declaration over-ride or merges with the first one using the customization $set notation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd also suggest a way to include an https:// referenced plugin that gets dynamically loaded - see the customization PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, I think I'm following. Let me tinker with it a bit, and see what I can come up with.

(I'm chuffed that I've been able to reduce the ~100 file changes to one essential change.)

@awatson1978 awatson1978 Jun 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So, just added a APP_PLUGIN_CONFIG environment variable.

Example customPlugin.json:

  {
    "include": "./pluginConfig.json",
    "extensions": [
      {
        "packageName": "@acme/extension-remote",
        "importPath": "https://cdn.example.com/acme-remote.umd.js",
        "globalName": "AcmeRemote"
      }
    ]
  }

Still testing/confirming everything works.

awatson1978 and others added 3 commits June 22, 2026 22:59
Adds a third composition layer to the plugin-import generator so a build can
pin exactly which extensions/modes it ships in its own tracked file, without
editing the shared default pluginConfig.json. Keeps EXTRA_EXTENSIONS /
EXTRA_MODES unchanged (they still append last).

Layers, each overriding the previous:
  1. pluginConfig.json            static default
  2. APP_PLUGIN_CONFIG=<file>     tracked override; may `include` the default
                                  (string or array), merges over it — plain
                                  arrays append (de-duped by package name),
                                  immutability-helper commands like { $set: [] }
                                  replace a list. Same notation the
                                  CustomizationService uses.
  3. EXTRA_EXTENSIONS / EXTRA_MODES  env name-injection, appended last.

Implemented entirely in writePluginImportsFile.js; all downstream resolution,
aliasing and asset copying already operate on the in-memory config, so they
inherit the merged result unchanged. Includes a cycle guard and fail-fast
errors. Per-mode dependency override left as a documented TODO(scope-B).

Adds pluginConfig.example.json (generic example) and documents the env vars
in platform/app/.env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ension

The ohif-fhir-viewer extension declares jszip and dcmjs-ecg as runtime
dependencies, but pnpm-lock.yaml predated them. With frozenLockfile: true,
a plain `pnpm install` refused to add them, so webpack failed to resolve
both modules. Regenerate the lockfile so a frozen install pulls them in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-resolve the extensions/ohif-fhir-viewer importer after switching its
@ohif/* peerDependencies from published semver ranges to workspace:*. This
prunes the published @ohif/*@3.12.5 + @cornerstonejs/*@4.15.29 subtree that
the extension was pulling in (the broken nested @cornerstonejs/core that
failed the webpack build) and links @ohif/core, extension-cornerstone,
extension-default, and i18n to the in-tree workspace packages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants