Skip to content

feat(chat): bundle the WebView renderers' JavaScript into the app - #368

Merged
garfiec merged 1 commit into
developfrom
feat/bundle-web-assets
Aug 28, 2026
Merged

feat(chat): bundle the WebView renderers' JavaScript into the app#368
garfiec merged 1 commit into
developfrom
feat/bundle-web-assets

Conversation

@garfiec

@garfiec garfiec commented Aug 27, 2026

Copy link
Copy Markdown
Owner

The artifact, diagram and math renderers are WebViews that loaded KaTeX, mermaid, marked, highlight.js, Tailwind, Babel and React from public CDNs at render time. All of it now ships inside the app; nothing executable is fetched at runtime.

This removes the runtime dependency on third-party hosts. The renderers no longer wait on the network to draw a diagram or a formula, so they work offline, and what the app executes is fixed at build time rather than decided by whatever a CDN serves that day — an unversioned URL had already drifted a major version without failing a build or a test. Pinned files in git turn that class of change into a reviewable diff.

Part of #288. F-Droid's policy asks that an app not download executable code without explicit opt-in consent; vendoring satisfies that, and an opt-in gate would have been the alternative route.

Vendoring

  • scripts/web-assets.json — registry of the 9 assets: version, license, source, what uses it, and a pin_reason. Several of those reasons are hard constraints rather than notes — mermaid must stay on the v10 line (v11+ is ESM-only and fails in Android WebView), react is bounded by UMD availability and react-dom must match it exactly, highlight.js must come from @highlightjs/cdn-assets because the main package is CommonJS-only, and Tailwind cannot move to v4 without changing which utilities resolve.
  • /update-web-assets — the skill that performs a repin: it reads each pin_reason before proposing a bump, re-downloads from the official source, regenerates the lock and manifest, and ends by naming what has to be checked on a device. Both CLAUDE.md files document the registry and the rule that vendored files are never hand-edited.
  • scripts/vendor-web-assets.py--check, --sync, --outdated, --bump, --list. --check verifies the tree against a sha256 lock and the generated Kotlin manifest, and runs in CI's lint job.
  • 41 files land in feature/chat/src/commonMain/composeResources/files/web/, each project's LICENSE alongside it. +2.47 MB compressed in the APK.

Renderers

  • Every document references assets by a path relative to a base URL the platform host supplies; the CDN origins are gone from the CSPs.
  • Android resolves that base from Compose Resources to file:///android_asset/….
  • iOS copies the assets out of the read-only bundle into caches on first use and loads pages with loadFileURL(_:allowingReadAccessTo:), because loadHTMLString cannot load local subresources at any base URL.
  • React 18 publishes no browser ESM, so its UMD builds are bridged into an import map generated at runtime. Artifacts still import React from 'react' unchanged.

Behaviour changes

  • Syntax highlighting in markdown artifacts now works. It ran through marked's highlight option, removed in v5 — setOptions accepts one and never calls it — and the highlight.js URL served CommonJS, which throws in a WebView. Now wired through the marked-highlight extension against the team's browser build.
  • A React artifact importing an npm package that isn't bundled fails with a message naming the package instead of loading it from a CDN. Restoring those behind an opt-in consent toggle is the intended follow-up.
  • React artifacts can no longer pull a remote stylesheet or web font: their CSP previously allowed style-src https: and font-src https:, which is the same permission the vendoring removes. Such an artifact renders with fallback styling rather than failing.
  • On iOS the KaTeX and mermaid WebViews now reload only when their document changes. Rebuilding the page is no longer free — it writes a file — and KaTeX's own height measurement recomposes the view, so an unguarded reload retriggered itself.

Tests

  • VendoredWebAssetInstrumentedTest — loads each renderer's real HTML into a WebView with the settings the scheme depends on, allowFileAccess = false among them, and asserts against the live DOM that each library defined itself and produced output: KaTeX's bundled fonts resolve, both the chat-message and artifact mermaid renderers emit SVG, markdown code carries hljs markup coloured by the theme, Tailwind compiles a utility class, and a React artifact mounts and evaluates a hook.
  • VendoredAssetReferenceTest — no document references a remote origin, no CSP permits one to execute, and every path a document references exists in the generated manifest.
  • ReactArtifactRenderTest — rewritten for local resolution.

Verification

  • Full unit suite, both detekt variants, :app:assembleDebug, and the iOS simulator compile pass.
  • scripts/vendor-web-assets.py --check passes; all 41 files confirmed present in the built APK.
  • Instrumented tests pass on an API 37 emulator and on a physical Android device, and the build was exercised by hand.

Notes

  • iOS writes one page-*.html per distinct document into its asset cache. They are cleared when a repin changes the assets, so between repins the count grows with the number of distinct documents rendered.
  • The WebView hosts render nothing while the base URL resolves. On iOS the first resolve copies ~7.5 MB out of the bundle.
  • iOS has not been exercised on hardware.
  • Also wanted for [Feature]: publish on F-Droid #288: the Google favicon service in the web-search card maps to the Tracking anti-feature, and release builds fall back to the committed debug key when no signing config is present, where F-Droid needs an unsigned release APK.

The artifact, diagram and math renderers loaded KaTeX, mermaid, marked,
highlight.js, Tailwind, Babel and React from public CDNs at render time.
Vendoring them fixes what the app executes at build time, lets the
renderers work offline, and satisfies F-Droid's rule against downloading
executable code without explicit opt-in consent.

Part of #288.
@github-actions

Copy link
Copy Markdown
Contributor

Android debug APK

Artifact: switchboard-android-debug-368
Download: switchboard-android-debug-368.zip
Retention: 90 days
Commit: 5820e62089d692056a3365da6eb4c892d3224d1c

Download requires a GitHub login. Installs over previous debug builds without uninstalling (stable signing key).

@garfiec
garfiec merged commit 828ec00 into develop Aug 28, 2026
6 checks passed
@garfiec
garfiec deleted the feat/bundle-web-assets branch August 28, 2026 18:24
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