Skip to content

feat(web): web platform TS structural refactor#145

Open
ddfreiling wants to merge 30 commits into
mainfrom
agents/docs-web-ts-refactor-todo
Open

feat(web): web platform TS structural refactor#145
ddfreiling wants to merge 30 commits into
mainfrom
agents/docs-web-ts-refactor-todo

Conversation

@ddfreiling

@ddfreiling ddfreiling commented Jun 3, 2026

Copy link
Copy Markdown
Member

ℹ️ This PR is a test of CoPilot's Autopilot to drive a refactor plan devised by Claude Code Opus 4.8 from start to finished implementation and opened PR.

What this PR does

Follows #136 with a full TypeScript structural refactor that mirrors the OOP architecture of the native sides.


Part 2 — TypeScript structural refactor

The web implementation was a 991-line god class (_ReadiumReader) in a flat directory. This PR restructures it to mirror the native iOS/Android OOP architecture, following the migration plan in docs/web-ts-refactor-plan.md.

Directory layout (before → after)

web/_scripts/          →  web/src/
  ReadiumReader.ts        ReadiumReader.ts  (thin dispatcher, ~200 lines)
  helpers.ts              index.ts          (webpack entry)
  Audio/                  bridge/           ReadiumBridge.ts  (all window.* calls)
  Epub/                   publication/      PublicationManager.ts
  WebPub/                 navigators/       FlutterEpubNavigator.ts
  TTS/                                      FlutterWebPubNavigator.ts
  extensions/                               FlutterAudioNavigator.ts
                                            FlutterTTSNavigator.ts
                                            FlutterMediaOverlayNavigator.ts
                                            navigatorUtils.ts
                          mediaoverlay/     syncNarration.ts, guidedNavigation.ts
                          decorations/      DecorationController.ts, decorationOverrides.ts
                          preferences/      FlutterEpubPreferences.ts, FlutterWebPubPreferences.ts,
                                            FlutterAudioPreferences.ts, FlutterTTSPreferences.ts
                          model/            ReadiumReaderStatus.ts
                          utils/            ReadiumExtensions.ts, ReadiumPluginLogger.ts,
                                            colors.ts, manifest.ts, iframeInjection.ts,
                                            Peripherals.ts

Key changes:

  • helpers.ts god-file dissolved into focused sub-modules
  • Three Flutter*Navigator wrappers extracted from free init functions (match Flutter*-prefix convention from native)
  • ReadiumBridge.ts is the only module allowed to touch window.* callbacks — injected by dependency, not reached globally
  • PublicationManager owns publication lifecycle state
  • DecorationController owns decoration style/group state
  • Typo fixed: webPubPrefences.tsFlutterWebPubPreferences.ts
  • WebTTSEngine renamed → FlutterTTSNavigator
  • Dead code removed: 7 unreachable delegation methods per navigator wrapper, highlightSelection() export, underlying fields
  • ~270 lines of dead code removed; O(n) noisy debug loop replaced with single summary line

Invariant preserved

The Dart↔JS contract is byte-identical: globalThis.ReadiumReader method names/signatures and window.* callback names/JSON shapes are unchanged. No changes to Dart js_interop.


Verification

  • ✅ All 151 Jest unit tests pass (npm test)
  • bin/typecheck clean (tsc --noEmit)
  • bin/format clean (Dart)
  • bin/analyze exits 0
  • ✅ JS bundle rebuilt (bin/update_web_example)
  • ⚠️ Web/Chrome smoke test still pending — run the example app on Chrome and exercise: EPUB open/navigate/decorate, TTS play/pause/next, Media Overlay sync, audio seek, comic FXL. Per project conventions this must be verified manually before merging.

ddfreiling and others added 27 commits June 3, 2026 18:23
Conventional source directory name; disambiguates from the separate
assets/_helper_scripts/ webview-helper bundle.

Update path references in package.json (4 script paths), webpack.config.js
comment, .github/instructions/typescript.instructions.md, CLAUDE.md,
CONTRIBUTING.md, docs/architecture.md, docs/parity/*, and bin/typecheck.

No code changes — build, typecheck, and 151 Jest tests all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… utils/ and model/

Move:
- logger.ts → utils/ReadiumPluginLogger.ts
- peripherals.ts → utils/Peripherals.ts
- extensions/ReadiumPublication.ts → utils/ReadiumExtensions.ts
- enums.ts → model/ReadiumReaderStatus.ts

Re-export shims at old paths keep all existing imports unchanged.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract from helpers.ts into canonical locations:
- utils/manifest.ts: fetchManifest
- utils/colors.ts: dartColorToCss
- utils/iframeInjection.ts: injectFlutterReadiumHelperScripts + asset cache
- decorations/decorationOverrides.ts: UNDERLINE_GROUP_SUFFIX, sendDecorate,
  navIframeWindows, registerPendingDecorationGroup, injectDecorationOverrides,
  highlightSelection

Add mediaTypes + findLinkByHref to utils/ReadiumExtensions.ts.

helpers.ts is now a re-export barrel (keeping preferences helpers in-place
until Phase A3). All existing import paths continue to work.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move:
- Epub/epubPreferences.ts → preferences/FlutterEpubPreferences.ts
  (+ convertVerticalScroll/textAlignFromJson/normalizeTypes inlined from helpers)
- WebPub/webPubPrefences.ts → preferences/FlutterWebPubPreferences.ts (typo fixed)
- TTS/ttsPreferences.ts → preferences/FlutterTTSPreferences.ts

Update FlutterWebPubPreferences imports to come from FlutterEpubPreferences.
helpers.ts barrel re-exports convertVerticalScroll/textAlignFromJson/normalizeTypes
from their new home in FlutterEpubPreferences.
Shims at all old paths keep existing imports unchanged.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…module

Extract enrichWithTotalProgression, enrichWithTocHref, flattenToc into
navigators/locatorEnrich.ts (shared with WebPub in step 5).

Create navigators/FlutterEpubNavigator.ts: class wrapping EpubNavigator
with static create() factory (logic moved verbatim from free function).

Epub/epubNavigator.ts becomes a shim re-exporting FlutterEpubNavigator,
enrichWithTotalProgression, and a backwards-compatible
initializeEpubNavigatorAndPeripherals wrapper.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterWebPubNavigator.ts: class wrapping WebPubNavigator
with static create() factory. Imports locatorEnrich from the shared module
(removes the cross-Epub/epubNavigator import).

WebPub/webpubNavigator.ts becomes a shim re-exporting the class and a
backwards-compatible initializeWebPubNavigatorAndPeripherals wrapper.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterAudioNavigator.ts: class wrapping AudioNavigator
with static create() factory. All logic moved verbatim from audioNavigator.ts;
module-level _emissionsEnabled + setAudioEmissionsEnabled preserved.
Exports buildStatePayload, seekAudioAndResume, SeekableAudioNavigator,
AudioLocatorMapper, and __testing__ (makeAudioTotalProgressionFn, withTocHref).

Audio/audioNavigator.ts becomes a shim re-exporting everything from the
canonical location plus a backwards-compatible initializeAudioNavigator wrapper.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterTTSNavigator.ts: same class as WebTTSEngine but
renamed FlutterTTSNavigator, with imports updated to canonical paths
(utils/ReadiumExtensions, utils/ReadiumPluginLogger, preferences/FlutterTTSPreferences).

TTS/ttsNavigator.ts becomes a shim re-exporting FlutterTTSNavigator under
both the new name and the backwards-compatible WebTTSEngine alias.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterMediaOverlayNavigator.ts with logic from
Audio/mediaOverlayNavigator.ts. Imports updated to canonical paths.
Calls FlutterAudioNavigator.create() directly instead of the
initializeAudioNavigator free function.

Audio/mediaOverlayNavigator.ts becomes a shim re-exporting all public
symbols and __testing__ from the canonical location.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
git mv flutter.d.ts → bridge/window.d.ts; update import to use
model/ReadiumReaderStatus canonical path.

Create bridge/ReadiumBridge.ts: the single module allowed to call window.*
emit callbacks. Typed methods: emitReaderStatus, emitTextLocator,
emitTimebasedState, emitTextSelected, emitError. Navigators will be wired
to use the bridge in Phase D.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create publication/PublicationManager.ts: encapsulates the static
_publications cache and manifest-fetch glue from the god class.
Methods: fetchAndCache (used by getPublication), getOrFetch (used by
openPublication cache-or-fetch pattern), evict/evictAll.
Phase D will wire _ReadiumReader to delegate to this collaborator.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create decorations/DecorationController.ts: extracts applyDecorations,
setDecorationStyle, _subgroupFor, _decorationsByGroup, and style state from
_ReadiumReader into a focused collaborator class. Phase D will wire the god
class to delegate to this instance.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create preferences/FlutterAudioPreferences.ts: audioPreferencesFromJson
(extracted from FlutterAudioNavigator) and applyAudioPreferences (extracted
from god class setAudioPreferences). Normalizes Dart preference keys to
IAudioPreferences before submitting.

Update FlutterAudioNavigator.ts to import audioPreferencesFromJson from the
new module; remove the now-redundant private preferencesFromString function.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire _ReadiumReader to use all Phase C collaborators:
- ReadiumBridge: all window.* calls replaced with bridge.emit* methods
- PublicationManager: static _publications + fetch logic delegated
- DecorationController: applyDecorations/setDecorationStyle delegated
- applyAudioPreferences from FlutterAudioPreferences

Update imports to canonical paths (navigators/, preferences/, utils/, model/,
bridge/, publication/, decorations/). FlutterTTSNavigator replaces WebTTSEngine.
FlutterAudioNavigator.create()/FlutterEpubNavigator.create()/
FlutterWebPubNavigator.create() used directly instead of free-function shims.

Public method names/signatures unchanged (Dart↔JS contract intact).
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create web/src/index.ts as the new webpack entry point (re-exports from
ReadiumReader.ts). Update webpack.config.js entry from ReadiumReader.ts to
index.ts. Output path lib/helpers/readiumReader.js is unchanged.
Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove 13 re-export shims at legacy paths:
  logger.ts, peripherals.ts, enums.ts, helpers.ts,
  extensions/ReadiumPublication.ts,
  Epub/{epubNavigator,epubPreferences}.ts,
  WebPub/{webpubNavigator,webPubPrefences}.ts,
  TTS/{ttsNavigator,ttsPreferences}.ts,
  Audio/{audioNavigator,mediaOverlayNavigator}.ts

Update test imports to canonical paths:
  __tests__/audioNavigator.test.ts   → navigators/FlutterAudioNavigator
  __tests__/epubNavigator.test.ts    → navigators/locatorEnrich
  __tests__/helpers.test.ts          → utils/colors
  __tests__/mediaOverlayNavigator.test.ts → navigators/FlutterMediaOverlayNavigator
  __tests__/guidedNavigation.test.ts, mediaOverlayNavigator.test.ts → utils/ReadiumExtensions

Update Audio/guidedNavigation.ts + Audio/syncNarration.ts to import from
canonical utils/ paths (logger → ReadiumPluginLogger, extensions → ReadiumExtensions).
Fix implicit-any on Link callback parameter in guidedNavigation.ts.

Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
audioNavigator.test.ts        → FlutterAudioNavigator.test.ts
epubNavigator.test.ts         → locatorEnrich.test.ts
helpers.test.ts               → colors.test.ts (helpers.ts deleted; only tests dartColorToCss)
mediaOverlayNavigator.test.ts → FlutterMediaOverlayNavigator.test.ts
closePublication.test.ts      → ReadiumReader.test.ts (imports ReadiumReader.__testing__)

guidedNavigation.test.ts and syncNarration.test.ts kept — their source
modules (Audio/guidedNavigation.ts, Audio/syncNarration.ts) are unchanged.

Also remove stale untracked shim files left on disk after Phase D15 git rm
(enums.ts, logger.ts, peripherals.ts, Epub/, TTS/, WebPub/, extensions/).

151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Audio/syncNarration.ts  → mediaoverlay/syncNarration.ts
Audio/guidedNavigation.ts → mediaoverlay/guidedNavigation.ts

These are media-overlay helpers, not generic audio utilities; grouping them
with FlutterMediaOverlayNavigator (navigators/) better reflects ownership.

Update all import paths in:
  ReadiumReader.ts
  navigators/FlutterMediaOverlayNavigator.ts
  __tests__/syncNarration.test.ts
  __tests__/FlutterMediaOverlayNavigator.test.ts
  __tests__/guidedNavigation.test.ts

Typecheck clean; 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ReadiumReader.ts: merge two split imports from @readium/shared into one
- FlutterWebPubNavigator.ts: add missing load error log + remove no-op try/catch
- FlutterMediaOverlayNavigator.ts: replace Manifest.deserialize()! + null check
  with the correct null-guard pattern (non-null assertion is redundant when the
  runtime check follows immediately)
- DecorationController.ts: remove stale 'Phase D' forward-reference from JSDoc
- PublicationManager.ts: remove 'god class' / 'Extracts from' impl-note language

No logic changes. 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EpubNavigator and WebPubNavigator had three identical blocks:
  - iframe up/down scroll (17 lines × 2)
  - handleLocator external-URL check (12 lines × 2)
  - textSelected payload builder (9 lines × 2)

Extract to navigators/navigatorUtils.ts:
  scrollVisibleIframes(direction)    — iframe content scroll
  handleExternalLocator(href)        — confirm-open / warn
  buildTextSelectionPayload(locator, selection) — selection JSON

Both navigator create() functions now import and call these helpers,
removing ~38 lines of duplicated code from each.

151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
_emitState takes 8 parameters; each listener repeated the same 5 captured
context arguments (locatorMapper, alsoText, computeTotalProgression,
onTextLocatorChanged, getTocHref) verbatim.

Introduce a local emit(state, locator, alsoText) closure inside create()
that closes over the 5 context args. Listeners now only supply the 3
values they actually vary per event, making each callback self-explanatory.

No logic change. 151 Jest tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- FlutterEpubNavigator/FlutterWebPubNavigator: remove the 7-method
  delegation façade (goRight/Left/Forward/Backward, goLink, go, destroy,
  currentLocator). ReadiumReader never stores the returned instance; it
  receives the raw upstream navigator via the setNav callback, so all
  wrapper methods were dead code.
- FlutterWebPubNavigator.create() return type changed Promise<Wrapper>→
  Promise<void> to match actual usage (return value was always discarded).
- Fixed copy-paste log message: 'EpubNavigator loaded' → 'WebPubNavigator
  loaded' in FlutterWebPubNavigator.
- decorationOverrides.ts: remove highlightSelection() and its exclusive
  imports (BasicTextSelection, Width, Layout, Locator, LocatorText,
  ReadiumPublication). The function was exported but never called anywhere
  in the codebase.
- syncNarration.ts: replace per-item debug log loop with a single summary
  line; the old loop emitted one log entry per cue which is hundreds of
  lines on a typical audiobook, drowning useful output.
- ReadiumExtensions.ts: merge duplicate @readium/shared imports; rewrite
  mediaTypes() as a single chained expression with const instead of let.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
FlutterAudioNavigator.create() return value is always discarded by both
ReadiumReader and FlutterMediaOverlayNavigator — the actual navigator is
delivered via the setNav callback. Remove the vestigial class constructor
and underlying field; change Promise<FlutterAudioNavigator> → Promise<void>
to match actual usage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merge duplicate FlutterAudioNavigator import (class + functions were on
  separate import lines; now collapsed into one).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pre-PR formatting pass via bin/format. All changes are line-wrapping
only — no logic changes. These files had accumulated formatting drift
since the last dart format run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
subosito/flutter-action v2.23.0 treats flutter-version-file as a
semver constraint and resolves it to the latest matching stable
(3.41.9 → 3.44.1), ignoring the intended pin. Fix: read the file
in a dedicated step and pass the exact string via flutter-version:.
Remove channel: stable (inferred from the exact version).

Document the pitfall in ci.instructions.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ddfreiling ddfreiling changed the title feat(web): web platform feature parity + TS structural refactor feat(web): web platform TS structural refactor Jun 3, 2026
ddfreiling and others added 2 commits June 3, 2026 22:53
analysis_options.base.yaml sets formatter.page_width: 120 via flutter_lints.
bin/format was not running pub get first, so the lints package could not be
resolved and dart format fell back to 80-char width. CI runs pub get before
dart format and correctly uses 120-char width, causing 69+ files to appear
changed in CI despite looking correct locally.

Fix bin/format to run flutter pub get --directory before each dart format call
so local and CI formatting always agree. Apply the correct 120-char formatting
to flutter_readium_platform_interface (69 files), flutter_readium (27 files),
and flutter_readium/example (20 files).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings the Spotlight decoration feature (#142), iOS event buffering (#152),
and the page_width-120 reformat from main into the web-TS refactor branch.

Conflict resolutions:
- Spotlight ported into the refactored web modules: body-dimming + CSS Custom
  Highlight restore lives in decorations/decorationOverrides.ts; group
  activation lives in decorations/DecorationController.ts. The thin
  ReadiumReader facade delegates to DecorationController (no direct spotlight
  imports).
- FlutterTTSNavigator / FlutterEpubNavigator keep the shared
  navigators/locatorEnrich.ts (flattenToc / enrichWithTocHref) instead of
  main's per-module duplicates.
- Navigators drop the focus-stealing window.focus() in positionChanged;
  FlutterWebPubNavigator gains the same 200ms trailing-edge text-locator
  debounce as EPUB.
- Dart conflicts (reader_decoration isActive, method-channel formatting,
  example spotlight segment, integration-test list-stable waits) take main.
- Web bundle regenerated from web/src and copied into example/web.

Stale web/_scripts cleanup:
- bin/install: install web deps from the plugin root where package.json lives.
- Removed the orphaned readiumReader.js.LICENSE.txt (a Terser artifact the dev
  build no longer emits; all its module paths were stale).
- Updated stale path/name references in a few source comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old name described only one of the module's exports
(injectDecorationOverrides). The module is really a collection of
decoration plumbing that operates on iframe windows — sendDecorate,
navIframeWindows, the pending-group pairing, and the spotlight DOM
helpers — so decorationFrameUtils.ts reflects its actual scope and
matches the navigators/navigatorUtils.ts naming precedent.

File rename + import-path updates only; no behavior change. The
injectDecorationOverrides function keeps its name (it accurately
describes that single function). Bundle regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ddfreiling ddfreiling marked this pull request as ready for review June 11, 2026 07:58
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