Skip to content

Extract the browser tier, behind a config seam - #22

Merged
playforge-coding merged 1 commit into
core-reconcile-richtextfrom
core-extract-browser-tier
Aug 5, 2026
Merged

Extract the browser tier, behind a config seam#22
playforge-coding merged 1 commit into
core-reconcile-richtextfrom
core-extract-browser-tier

Conversation

@playforge-coding

@playforge-coding playforge-coding commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Fifth and final extraction layer. Stacked on #20.

Moves imageStore, imageRef, storage, docxExport, docxImport, htmlPreview, pdfExport and the download half of jsonExport under @spelling-creator/core/browser/*.

Why, given none of this is shareable

Unlike the earlier layers, this shares nothing with the Worker — nothing else can use a module that needs IndexedDB. The point is that these are browser-dependent but framework-agnostic: they're the code that survives a rewrite of the React layer intact, which is the reason for extracting lib/ in the first place.

It was blocked, and not by React

imageRef read import.meta.env.VITE_API_URL at module scope, and storage, docxExport, docxImport, htmlPreview, pdfExport and imagesClient all sit downstream of it. One line pinned the entire tier.

18 modules in apps/web do the same thing. That's the pattern, not an isolated case — and it's a bigger obstacle to ever changing build tool or framework than React is.

So core takes its configuration through a seam:

// apps/web/src/main.jsx, once
configureCore({ apiUrl: import.meta.env.VITE_API_URL });

Readers resolve lazily, and that's load-bearing. ES imports are hoisted, so configureCore runs after every module in the graph has been evaluated — anything capturing the value at import time would capture "". I checked every read is inside a function body rather than at module scope, and config.test.js pins the behaviour with a test that configures after import.

Two modules from #16 now split properly

image and jsonExport were moved wholesale in #16 with a note that they'd split later. They split along exactly the line they were always going to:

Before Neutral Browser
image sizing arithmetic (fitWithin, imageSizeScale, defaults) browser/imageFilereadImageFile (canvas, FileReader)
jsonExport lessonFile — the envelope browser/jsonExport — the part that clicks an <a>

This removes a real layering violation: jsonImport is runtime-neutral but needed two constants from each, and would otherwise have had to import from the browser tier.

lessonFile is also a latent third duplication — apps/mcp/src/doc.js has its own buildLessonFile, and the old comment asked for them to be kept in sync by hand. Not deduped here; worth a follow-up.

The lint rule is now structural

#16 opted two files into the browser env by name. That list is replaced by src/browser/**, so the boundary is a directory rather than a list someone has to remember to update.

Verification

pnpm run lint, pnpm run build, pnpm run build:docs pass; core 38, api 27, mcp 14. Bundle total unchanged (4095.8 kB vs 4096.0 kB).

mammoth and html2pdf.js move to core's dependencies; web keeps docx, which googleDrive.js still uses directly.

Stack created with GitHub Stacks CLIGive Feedback 💬

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: 310979b6-f9c9-419f-a01f-46000d2f2407

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

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

Fifth and final extraction layer: imageStore, imageRef, storage, docxExport,
docxImport, htmlPreview, pdfExport and the download half of jsonExport, under
@spelling-creator/core/browser/*.

Unlike the earlier layers this shares nothing with the Worker — nothing else can
use a module that needs IndexedDB. The point is that these are browser-dependent
but framework-agnostic: they are the code that survives a rewrite of the React
layer intact, which is the reason for extracting lib/ in the first place.

It was blocked, though, and not by React. imageRef read
import.meta.env.VITE_API_URL at module scope, and storage, docxExport,
docxImport, htmlPreview, pdfExport and imagesClient all sit downstream of it —
so one line pinned the whole tier. 18 modules in apps/web do the same thing;
this is the pattern, not an isolated case.

So core now takes its configuration through a seam instead:

  configureCore({ apiUrl: import.meta.env.VITE_API_URL });   // main.jsx, once

Readers resolve lazily rather than capturing at import time, which is load
bearing: ES imports are hoisted, so configureCore runs *after* every module in
the graph has been evaluated, and anything capturing the value would capture "".
config.test.js pins that, and every read was checked to be inside a function
body rather than at module scope.

Two modules that step 1 moved wholesale are now split along the same line they
were always going to split on. image keeps the pure sizing arithmetic and gives
up readImageFile (canvas, FileReader) to browser/imageFile; jsonExport becomes
lessonFile (the envelope, which jsonImport and the MCP server also want) and
browser/jsonExport (the part that clicks an <a>). That removes the layering
violation where a runtime-neutral module would have had to import from the
browser tier for two constants.

The lint rule from step 1 is now structural rather than a list of filenames:
src/browser/** is the browser env, everything else in core is worker.

mammoth and html2pdf.js move to core's dependencies; web keeps docx, which
googleDrive.js still uses directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@playforge-coding
playforge-coding force-pushed the core-extract-browser-tier branch from e00cba4 to 4e29a02 Compare August 5, 2026 23:30
@playforge-coding
playforge-coding merged commit cde68dc into master Aug 5, 2026
2 of 3 checks passed
@playforge-coding
playforge-coding deleted the core-extract-browser-tier branch August 5, 2026 23:31
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