Skip to content

Add web platform support with navigation shim and Nuxt integration - #2

Draft
Huxpro wants to merge 9 commits into
mainfrom
claude/nuxtjs-sparkling-navigation-rn285r
Draft

Add web platform support with navigation shim and Nuxt integration#2
Huxpro wants to merge 9 commits into
mainfrom
claude/nuxtjs-sparkling-navigation-rn285r

Conversation

@Huxpro

@Huxpro Huxpro commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds comprehensive web platform support to Sparkling, enabling Lynx bundles to run in browsers alongside native Android/iOS targets. The implementation includes:

  1. web-navigation-shim: A framework-agnostic History/Location API implementation that bridges web routing frameworks (vue-router, Nuxt) to a pluggable NavigationHost contract, enabling cross-page navigation in multi-page app (MPA) models.

  2. nuxt-sparkling: A Nuxt module that transforms file-based pages/ routing into a Sparkling route manifest, enabling Nuxt apps to drive native navigation while supporting deep linking and route matching.

  3. sparkling-navigation web handlers: Browser implementations of router.open, router.close, and router.navigate using the History API.

  4. sparkling-web-shell: A minimal web host for rendering Lynx web bundles in browsers, with method handler registration and bundle loading.

  5. Web method implementations: Added web support for storage (localStorage) and media (file picker) methods.

  6. CLI and tooling updates: Extended sparkling-app-cli with dev:web, build:web, and run:web commands; updated create-sparkling-app to optionally scaffold web support.

Related issues

Enables web platform development and preview for Sparkling apps without native simulators.

Changes

  • packages/web-navigation-shim/: New package providing ShimHistory, ShimLocation, ShimDocument, and ShimWindow interfaces with full WHATWG semantics (pushState/replaceState synchronous, go() async with popstate, proper state round-tripping).
  • packages/nuxt-sparkling/: New Nuxt module with pagesToSparklingManifest() transform, route matching, and diagnostics for unsupported features (nested outlets, named views, redirects).
  • packages/methods/sparkling-navigation/src/web/: Web handler for router methods using History API.
  • packages/methods/sparkling-storage/src/web/: localStorage-backed storage implementation.
  • packages/methods/sparkling-media/src/web/: File picker implementation using <input type="file">.
  • packages/sparkling-web-shell/: New web host shell with Lynx web runtime integration and bundle loading.
  • packages/sparkling-app-cli/: Added run-web.ts, extended build/dev commands with --platform web, added web checks to doctor command.
  • packages/create-sparkling-app/: Added askWebPlatform() prompt and web scaffold removal logic.
  • Documentation: Added guides for web platform, web method implementations, Nuxt compatibility, and web limitations.
  • Template updates: Extended app.config.ts and package.json with web build/dev scripts.

How to test

  1. Unit tests: Run pnpm test — new test suites cover:

    • web-navigation-shim history/location semantics with vue-router integration
    • nuxt-sparkling manifest generation and route matching
    • Web method handlers (navigation, storage, media)
    • Sparkling NavigationHost adapter
  2. Integration test: packages/nuxt-sparkling/__tests__/integration.spec.ts proves end-to-end MPA wiring (Nuxt pages → manifest → NavigationHost → shim → vue-router).

  3. Manual verification:

    npm create sparkling-app@latest test-app -- --web
    cd test-app
    npm run dev:web  # Preview in browser at http://localhost:5969
    npm run build:web
  4. Existing tests: All existing Android/iOS tests remain unaffected; web is opt-in.

Checklist

  • I read CONTRIBUTING.md.
  • I linked a related issue (web platform enablement).
  • I ran relevant checks (unit tests, integration tests pass).
  • I updated docs/examples (web platform guide, method implementations, Nuxt compatibility, CLI docs).
  • I

https://claude.ai/code/session_01RaEVSkjEsyQM37Qr2JFXmm

Huxpro added 9 commits July 12, 2026 03:36
Ports the experimental web platform support from tiktok#22
(sparkling-web-shell, web method bridge/registry, method web
implementations, CLI web autolink/build/dev/run:web/doctor support,
create-app --web option, template + playground web environments, docs)
onto the current main, adapting for changes that landed since:

- autolink: coexists with Lynx library autolink (devtool modules,
  maven deps); web autolink keeps reading platforms/web keys from
  module.config.json
- dev: --platform web|native|all maps to rspeedy --environment on top
  of the new config-watching restart loop
- create-app: askWebPlatform rewritten with @clack/prompts (inquirer
  was removed on main)
- playground: web/lynx environments layered over lynx.shared.config.ts
  of the 13-page demo app
- website: Web Platform sidebar entries in current rspress structure
- sparkling-media module.config.json: platforms/web keys on top of the
  simplified config format

Verified end-to-end: playground web bundles build, web shell renders
main page in Chromium, router.open navigates across pages
(?page=nav-chain) and browser back returns.
Bugfix for the original web support (upstream PR #22), kept as a
separate commit so it can be stacked as its own PR:

- sparkling-web-shell was marked private with a stale version while
  the app template depends on it from the registry, so 'run:web' could
  never resolve the shell in a created app. Make it publishable and
  align the version with the workspace.
- repository.url pointed at a personal fork; point it at
  tiktok/sparkling.
- move @rsbuild/core from devDependencies to dependencies: run:web
  executes 'rsbuild dev' inside the installed package, where
  devDependencies are never installed for consumers.
- declare published files (src, rsbuild.config.ts, tsconfig.json) —
  the shell runs from source via rsbuild.
Bugfix for the original web support (upstream PR #22), kept as a
separate commit so it can be stacked as its own PR:

'sparkling run:web --port N' and '--no-open' pass PORT/BROWSER env
variables to the shell, but rsbuild reads neither, so the flags were
silently ignored and the server always started on 4200. Read them in
rsbuild.config.ts (verified: PORT=4300 binds to 4300).
…vigationHost contract

Framework-agnostic package implementing the Web history/location/popstate
surface on top of a minimal NavigationHost contract (initialUrl, open,
close, optional go/reload/isSameDocument). This is the layer that lets
web routing frameworks (vue-router/Nuxt today, others tomorrow) run
inside non-browser JS contexts (Lynx pages) while cross-document
navigations are delegated to the platform's real page stack (Sparkling
native navigation) — the MPA model, not an in-memory SPA router.

Semantics implemented per the Nuxt 4 / vue-router 5 browser-API audit:
synchronous event-free pushState/replaceState with forward truncation,
async go() with exactly one popstate per call (location/state updated
before listeners), out-of-range no-op at the top, native traversal when
crossing the bottom of the local stack, hash-only same-document policy
(host-overridable via route-manifest policy), pagehide/visibilitychange
scroll-persistence path, and boot stubs (document gate, navigator, rAF,
scroll) behind a non-clobbering install().

Includes MemoryDocumentStack: an in-heap reference host modeling a
native stack of isolated documents for tests/SSR.

Verified by 37 tests in plain Node (no jsdom), including integration
tests running real vue-router 5 createWebHistory entirely on the shim:
boot, two-write push protocol, back/forward, guard rollback via
go(-delta), and the manifest-guard MPA handoff pattern.
Companion to the web platform port: the /web method handlers added by
upstream PR #22 shipped with no unit tests, which drops the
sparkling-navigation/storage/media coverage gates below threshold once
those files are collected. Add node jest tests that mock the browser
globals (window.history, localStorage, file input, fetch/FormData) and
exercise every handler branch, restoring the packages above their
coverage thresholds.

Kept separate so it can stack onto the web-support PR.
… manifest

Adds the Sparkling implementation of web-navigation-shim's NavigationHost
contract, plus the route-manifest format that connects file-based routes
to Lynx bundles across isolated JS heaps (the MPA model).

- manifest.ts: SparklingRouteManifest format + a vue-router-style path
  matcher (static/param/optional/splat segments, specificity ordering),
  bundle resolution, and reverse bundle→route lookup.
- shim-host/index.ts: createSparklingNavigationHost() maps in-app URLs to
  router.open scheme calls (carrying the full app URL in a __path query
  item so a freshly-opened page can seed its own shim location), routes
  off-origin URLs through the webview container scheme, backs close()/
  reload() on router.close, and implements isSameDocument (hash-only by
  default; optionally same-Lynx-bundle for SPA-inside-MPA hybrids).
- deriveInitialUrl() reconstructs a page's initial app URL from container
  query items (__path, then bundle/url lookup, then base).

Exposed via the sparkling-navigation/shim-host subpath export. This is
the only Sparkling-aware layer: the shim above it and the methods below
it stay platform-agnostic.

Verified by 26 new jest tests (manifest matching + host behavior);
sparkling-navigation coverage stays above threshold.
The Nuxt integration layer that ties the shim + host to Nuxt's
file-based routing, per the MPA (multi-page, isolated-heap) model.

- manifest.ts: pagesToSparklingManifest() flattens Nuxt's nested
  NuxtPage[] tree (as delivered to pages:extend) into a flat Sparkling
  route manifest — each URL maps to exactly one bundle, nested
  <NuxtPage> outlets collapse (index child wins its parent URL), and
  every construct that cannot cross a JS heap is reported as a
  diagnostic (degraded vs blocking).
- module.ts: createNuxtSparklingModule() hooks pages:extend to emit the
  manifest as a virtual module, prints a per-feature support report, and
  disables browser-only Nuxt subsystems (emitRouteChunkError,
  appManifest, navigationRepaint, restoreState). analyzePages() exposes
  the same analysis for tooling/tests.
- runtime/history.ts: sparklingHistory() installs the shim over the
  Sparkling host and returns a vue-router history (web or memory);
  sparklingScrollBehavior() returns false so vue-router never touches a
  DOM viewport.

Also fixes web-navigation-shim install() to tolerate read-only host
globals (e.g. Node's navigator getter) instead of throwing, and points
its package exports at the composite-build dist/src output.

Verified: ports Nuxt's official pages.test.ts fixtures (v4.4.8) through
the generator (12 cases across every routing pattern) and an end-to-end
integration test running real vue-router over the real
NativeModules.spkPipe bridge — deep-link boot, cross-bundle router.open
handoff, native router.close, external webview handoff. 15 tests.
Task deliverable: the acceptance traversal answering, per Nuxt routing
feature, whether it is supported / degraded / unsupported under the MPA
model, with rationale traced to the isolated-JS-heap constraint.

- docs/en/guide/nuxt-sparkling-compat.md: feature-by-feature verdicts
  (route generation, runtime navigation, layout/outlet, state), the
  verification method (ported Nuxt fixtures + runtime matching + e2e),
  and the summary that Nuxt's whole routing surface is supported while
  the degraded/unsupported cases all reduce to shared-heap requirements.
- docs/en/guide/nuxt-web-api-dependencies.md: the Web history/location/
  popstate/URL dependency inventory behind web-navigation-shim, with the
  minimum NavigationHost contract for web vs memory history.
- website sidebar: new 'Nuxt (MPA)' section linking both.

Also fixes a real matcher bug surfaced while writing the report: the
sparkling-navigation route matcher split paths on every '/', mangling a
mid-path catch-all whose custom regex body contains a slash
(Nuxt's [...id]/suffix → /:id([^/]*)*/suffix). Segment splitting is now
parenthesis-depth aware; regression test added ('/a/b/suffix' →
id: ['a','b'], plain catch-all still wins '/a/b').
Classifies the rest of Nuxt (beyond routing) for portability into
Lynx+Sparkling: portable as-is (auto-imports, runtimeConfig,
definePageMeta, pure composables), portable behind a shim ($fetch/
useFetch/useAsyncData, route middleware, plugins, error pages), remap to
native (useHead→container scheme, cross-page useState→sparkling-storage,
useCookie→storage, layouts/transitions→native container/animation), and
not applicable (Nitro server routes→keep as backend, SSR/payload/
hydration, chunk prefetch). Ends with a prioritized follow-up order
(useSparklingState, $fetch, useHead→scheme, global middleware/plugin
duplication). Added to the website sidebar.
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