chore: Backend HMR (expriemental) (#14074)

**What**

 This PR introduces experimental Hot Module Replacement (HMR) for the Medusa backend, enabling developers to see code changes reflected immediately without restarting the server. This significantly improves the development experience by reducing iteration time.

### Key Features

  - Hot reload support for:
    - API Routes
    - Workflows & Steps
    - Scheduled Jobs
    - Event Subscribers
    - Modules
  - IPC-based architecture: The dev server runs in a child process, communicating with the parent watcher via IPC. When HMR fails, the child process is killed and restarted, ensuring
  clean resource cleanup.
  - Recovery mechanism: Automatically recovers from broken module states without manual intervention.
  - Graceful fallback: When HMR cannot handle a change (e.g., medusa-config.ts, .env), the server restarts completely.


### Architecture
```mermaid
  flowchart TB
      subgraph Parent["develop.ts (File Watcher)"]
          W[Watch Files]
      end

      subgraph Child["start.ts (HTTP Server)"]
          R[reloadResources]
          R --> MR[ModuleReloader]
          R --> WR[WorkflowReloader]
          R --> RR[RouteReloader]
          R --> SR[SubscriberReloader]
          R --> JR[JobReloader]
      end

      W -->|"hmr-reload"| R
      R -->|"hmr-result"| W
```

### How to enable it

Backend HMR is behind a feature flag. Enable it by setting:

```ts
  // medusa-config.ts
  module.exports = defineConfig({
    featureFlags: {
      backend_hmr: true
    }
  })
```

or

```bash
export MEDUSA_FF_BACKEND_HMR=true
```

or

```
// .env
MEDUSA_FF_BACKEND_HMR=true
```

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-12-08 08:48:36 +00:00
committed by GitHub
co-authored by Oli Juhl Carlos R. L. Rodrigues
parent 4de555b546
commit fe49b567d6
38 changed files with 2222 additions and 61 deletions
+34 -15
View File
@@ -203,6 +203,20 @@ class MedusaModule {
return [...MedusaModule.moduleResolutions_.values()]
}
public static unregisterModuleResolution(moduleKey: string): void {
MedusaModule.moduleResolutions_.delete(moduleKey)
MedusaModule.joinerConfig_.delete(moduleKey)
const moduleAliases = MedusaModule.modules_
.get(moduleKey)
?.map((m) => m.alias || m.hash)
if (moduleAliases) {
for (const alias of moduleAliases) {
MedusaModule.instances_.delete(alias)
}
}
MedusaModule.modules_.delete(moduleKey)
}
public static setModuleResolution(
moduleKey: string,
resolution: ModuleResolution
@@ -516,25 +530,27 @@ class MedusaModule {
}
const resolvedServices = await promiseAll(
loadedModules.map(async ({
hashKey,
modDeclaration,
moduleResolutions,
container,
finishLoading,
}) => {
const service = await MedusaModule.resolveLoadedModule({
loadedModules.map(
async ({
hashKey,
modDeclaration,
moduleResolutions,
container,
})
finishLoading,
}) => {
const service = await MedusaModule.resolveLoadedModule({
hashKey,
modDeclaration,
moduleResolutions,
container,
})
MedusaModule.instances_.set(hashKey, service)
finishLoading(service)
MedusaModule.loading_.delete(hashKey)
return service
})
MedusaModule.instances_.set(hashKey, service)
finishLoading(service)
MedusaModule.loading_.delete(hashKey)
return service
}
)
)
services.push(...resolvedServices)
@@ -590,7 +606,10 @@ class MedusaModule {
try {
// TODO: rework that to store on a separate property
joinerConfig = await services[keyName].__joinerConfig?.()
joinerConfig =
typeof services[keyName].__joinerConfig === "function"
? await services[keyName].__joinerConfig?.()
: services[keyName].__joinerConfig
} catch {
// noop
}