Commit 8dd6985
Enable webpack HMR for CSS/SCSS in dev (#68)
## Summary
Turns on webpack Hot Module Replacement for SCSS/CSS in the dev server.
Style edits now apply in place without a full page reload β scroll
position and in-app state are preserved.
## Changes
- `devServer.hot: false` β `true` in the `configureWebpack` devServer
block.
- Pass `{esModule: false}` to `style-loader`, matching the option
already set on `css-loader`.
## Why the `esModule` change is needed
Just flipping `hot: true` is not enough on its own. With it enabled,
webpack-dev-server *does* push hot-updates for SCSS changes, but the
page reloads anyway. Tracing the abort with a temporary `sessionStorage`
capture in `webpack/hot/dev-server.js` showed:
```
Aborted because ./src/desktop/App.scss is not accepted
Update propagation: ./src/desktop/App.scss -> ./src/desktop/AppComponent.ts -> ./src/apps/app.ts
```
The SCSS wrapper module was self-accepting via `style-loader`, but then
*invalidating itself* mid-update. The trigger is in `style-loader@4`'s
HMR template:
```js
if (!isEqualLocals(oldLocals, isNamedExport ? namedExport : content.locals, isNamedExport)) {
module.hot.invalidate();
return;
}
```
With `style-loader`'s default `esModule: true`, `isNamedExport` is set
to `!content.locals` β true for non-CSS-modules SCSS β so the comparison
runs against the inner module's *named exports* rather than its `locals`
object. Each rebuild produces fresh named-export bindings that fail the
`isEqualLocals` check, so `invalidate()` fires on every update and the
HMR client falls back to a full reload via the abort path in
`webpack/hot/dev-server.js`.
Setting `esModule: false` on `style-loader` routes the check through the
`content.locals` path instead. For SCSS without CSS Modules, `locals` is
`undefined` on both sides β equal β so `invalidate()` doesn't fire, the
wrapper's `module.hot.accept()` handler applies the new styles in place,
and propagation stops. (`css-loader` was already configured with
`esModule: false`, so this is consistent across the pipeline.)
Verified by editing `App.scss` repeatedly with a `window.__marker` set
before each edit: the marker survives every time, indicating no page
reload.
## Why only CSS β JS/TS still requires a full reload
CSS HMR works because style-loader generates a small self-accepting
wrapper module whose only job is to swap a `<style>` tag. There is no
application state in that wrapper to preserve across the swap, so the
replacement is safe and local.
Bringing the same experience to JS/TS modules requires React Fast
Refresh, which is a much larger change to the framework:
- **Component detection** β the Fast Refresh Babel plugin identifies
components by `PascalCase` exports and a small set of recognized HOC
patterns (`React.memo`, `React.forwardRef`). Hoist components are
exported as `lowercase` element factories returned by
`hoistCmp.factory(...)`, which the standard detector skips. We would
need either a custom detector or a marker that `hoistCmp` attaches to
its returns so the runtime recognizes them.
- **Model lifecycle** β `HoistModel` instances live outside React, are
tracked by `InstanceManager`, hold `@observable` / `@bindable` state via
MobX, own `@managed` children, and have explicit `destroy()` semantics.
When HMR swaps a class definition, the existing instance's decorators
and observable identity don't migrate cleanly. `InstanceManager` would
need a hot-reload hook that disposes and recreates linked models when
their defining module is replaced.
- **Service singletons** β `HoistService` instances are installed once
at boot and exposed on `XH`. HMR has no story for re-registering them
without breaking every model that captured a reference, so services
would likely need to be treated as a hard boundary that forces a full
reload when their module changes.
None of that is impossible, but it's framework work β days, not hours β
and would want an RFC. The CSS change here is independent and unblocks
the most common iteration pain point (style tweaks) right away.
## Test plan
- [x] Edit an `.scss` file in a Hoist dev app and confirm styles update
without page reload.
- [x] Confirm in-app state (open tabs, scroll position, form values)
survives the edit.
- [x] Edit a `.tsx` file and confirm the existing full-reload behavior
is unchanged.
- [ ] Production build (`yarn build`) still uses `MiniCssExtractPlugin`
(the `style-loader` block is dev-only) β confirm no regression.
π€ Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 899c39c commit 8dd6985
2 files changed
Lines changed: 12 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
3 | 11 | | |
4 | 12 | | |
5 | 13 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
527 | 527 | | |
528 | 528 | | |
529 | 529 | | |
530 | | - | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
531 | 533 | | |
532 | 534 | | |
533 | 535 | | |
| |||
716 | 718 | | |
717 | 719 | | |
718 | 720 | | |
719 | | - | |
| 721 | + | |
720 | 722 | | |
721 | 723 | | |
722 | 724 | | |
| |||
0 commit comments