frontend module conversion)
ui-state was the smallest, most isolated module: 35 lines, zero legacy-
global dependencies, single concern (localStorage helpers under the
'ped_ui/' namespace). Ideal first migration to validate the strangler-
fig pattern end-to-end.
Changes:
- New: client/ui-state.ts — exports get/set/del + UIState bundle.
Keeps (window as any).UIState = UIState assignment so the 10+ legacy
consumers (sub-nav.js, peGuide.js, app.js, etc.) keep working without
modification. As each consumer is migrated, it will replace the
window.UIState read with an explicit import.
- client/main.ts: import './ui-state' so it gets bundled into main.js.
- scripts/build-client.mjs: explicit ENTRIES list (just 'main.ts' for
now) — was scanning every top-level .ts as an entry, which produced
duplicate ui-state.js bundle.
- public/index.html: <script defer src="/js/ui-state.js"> removed; the
TS bundle (/dist/main.js) now provides window.UIState.
- public/js/ui-state.js: deleted (now lives in client/).
Verification:
- npm run typecheck:client → 0 errors
- npm run build:client → 349-byte main.js bundle (gzipped will be ~200B)
- 46/46 unit tests pass
- The 10 cross-file consumers of UIState.get/set/del still work because
the window assignment lives in the bundle.
Pattern documented for next migrations: each new client/X.ts is
imported as a side-effect from main.ts. The window-bridge stays until
every consumer also moves to ES imports.
Strangler-fig migration setup for the frontend. Legacy public/js/*.js
keeps working unchanged; new TypeScript code lives under client/ and
gets bundled into public/dist/ at image-build time.
Adds:
- esbuild@0.28 as a regular dep (so docker build can run the bundler
without --include-dev). It's small; tsx already pulled in a pinned
version transitively.
- scripts/build-client.mjs: bundles every top-level client/*.ts file
into public/dist/<name>.js as ESM with sourcemap. --watch mode for
dev. Sub-directories under client/ are bundled by their parent
entry, not separately.
- tsconfig.client.json: browser target (lib: DOM), bundler module
resolution, isolatedModules:true so esbuild + tsc agree.
- Two npm scripts: build:client, typecheck:client.
- Dockerfile: 'RUN node scripts/build-client.mjs' between COPY and CMD,
so the prod image ships with the built bundle.
- .gitignore: public/dist/ — build output, never committed.
- public/index.html: <script type="module" src="/dist/main.js">
appended after legacy script tags.
- client/main.ts: minimal entry point with a console.log marker
(window.__clientBundle = true) so we can verify the bundle loaded.
Sacred files (per project memory) kept off the migration path:
- public/js/encounters.js (save/idempotency)
- voiceDictation.js, sickVisit.js recording paths (STT plumbing)
These need explicit per-file approval from Daniel before migration.
Verification:
- npm run typecheck → 0 errors (backend)
- npm run typecheck:client → 0 errors (frontend)
- npm run build:client → public/dist/main.js + .map in 3 ms
- 46/46 unit tests pass
Next session: migrate the first real frontend module (small + isolated,
likely extensions.js or memories.js) into client/ as ES module + TS.