Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions docs/en/guide/sparkling-router-prototype.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Sparkling Router Prototype Findings

Status: prototype
Date: 2026-07-25

This prototype evaluates a URL-first declarative router over
`sparkling-navigation`, with native containers as hard navigation boundaries
and memory history inside each container.

## Conclusions

### 1. Reuse TanStack core, but do not import it as a complete router API

`@tanstack/router-core` contains the matching, loading, navigation, search, and
route-tree machinery. It intentionally does not export framework-neutral
`createRouter`, `createRoute`, or `createRootRoute` factories. TanStack's own
framework packages subclass `RouterCore`, `BaseRoute`, and `BaseRootRoute` to
provide those factories and framework extensions.

For ReactLynx, the lowest-risk architecture is therefore:

- use the official `@tanstack/react-router` binding;
- keep `@tanstack/router-core` and `@tanstack/history` pinned as the underlying
core contracts;
- implement only Sparkling's `RouterHistory`, serializable manifest, and native
host adapter.

Creating a separate Sparkling subclass layer would copy internal framework
binding work and make upstreaming harder without improving the ReactLynx
authoring experience.

### 2. TanStack file routing is the primary authoring path

The official `@tanstack/router-generator` and
`@tanstack/router-plugin/rspack` work with Rspeedy. They should continue to own
`routeTree.gen.ts`, typed routes, params, search, loaders, and links.

Sparkling adds one orthogonal compiler pass:

- find `_container.tsx` or `_container.modal.tsx` boundaries;
- partition routes into native bundles;
- emit a serializable global manifest and an entry map.

The prototype's compiler uses the TypeScript AST for static route/config
extraction. It does not evaluate application source.

### 3. A Next app-directory frontend can target the same core

The second scanner maps `app/**/page.tsx`, dynamic `[id]` segments, route groups,
and optional `container.ts` boundaries into the same manifest schema. Tests
prove equivalent TanStack and Next directory trees produce equivalent
container manifests.

This validates "one core, two authoring frontends" at the routing-data layer.
It does not yet implement a Next runtime or compatibility components such as
`next/link`, `useRouter`, `loading.tsx`, and `error.tsx`.

## Evidence

- `sparkling-history`: 29 tests cover in-container history parity, native page
forwarding, scheme transport, and back-at-root behavior.
- `sparkling-router`: 3 tests run a real TanStack router over Sparkling history,
including cross-container navigation.
- `sparkling-router-plugin`: 3 tests cover TanStack boundaries, equivalent Next
output, and non-evaluating AST extraction.
- `tanstack-router-demo`: 16 tests cover generated route trees, params, search,
loaders, redirects, errors, blockers, and MPA forwarding.
- Rspeedy builds four native bundles successfully: `spike`, `home`, `detail`,
and `settings`.

## Remaining Gate

This prototype does not freeze the native stack protocol. The next gate is the
iOS minimum implementation and conformance test for:

- stack state and monotonic versions;
- push, pop, replace, reset, and getState;
- native gesture-driven `stackchanged`;
- `syncOwnLocation`;
- prefetch and result delivery.

Only after that gate should `sparkling-history` be promoted to the full
`CompositeHistory` described by the RFC.
79 changes: 79 additions & 0 deletions packages/sparkling-history/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# sparkling-history

A reusable **web-history shim** that lets URL-driven routers — TanStack Router,
React Router, or your own — drive **native multi-page navigation**, where each
route subtree runs in its own container / JS context.

This is the opposite of the SPA-in-one-view model: instead of one long-lived
router in one JS heap, each page is a separate view/VM, and navigating between
pages is a native container `open`. Because pages cannot share memory, they are
connected by pre-generated file-based metadata (a route→page manifest) rather
than an in-process history stack.

## Layers

```
your router (TanStack Router / React Router / ...)
│ consumes a RouterHistory
┌───────────▼───────────┐
│ createMpaHistory │ web-history shim (this package)
└───────────┬───────────┘
│ calls a NavigationHost
┌────────────────▼─────────────────┐
│ createSparklingHost / your own │ platform binding
└────────────────┬─────────────────┘
native container (sparkling-navigation)
```

- **`NavigationHost`** — the contract a platform implements:
`getInitialHref()`, `getStackDepth()`, `getInitialState()`, `open()`,
`close()`. Anything satisfying it can host a URL-driven router.
- **`createMpaHistory(opts)`** — implements the `RouterHistory` shape from
`@tanstack/history`, so its result can be passed straight to
`createRouter({ history })`. In-page navigations behave like a memory
history; cross-page navigations (decided by a `PageResolver`) are forwarded
to `host.open()`; `back()` at the page root becomes `host.close()`.
- **`createSparklingHost(opts)`** (`sparkling-history/sparkling`) — the
`sparkling-navigation` binding.

## Usage

```ts
import { createRouter } from '@tanstack/react-router';
import { createMpaHistory, createManifestPageResolver } from 'sparkling-history';
import { createSparklingHost } from 'sparkling-history/sparkling';
import * as navigation from 'sparkling-navigation';
import { routeTree, manifest } from './routes';

const host = createSparklingHost({
navigation,
getQueryItems: () => lynx.__globalProps.queryItems,
});

const router = createRouter({
routeTree,
history: createMpaHistory({ host, resolvePage: createManifestPageResolver(manifest) }),
isServer: false,
origin: 'http://sparkling.local', // router-core reads window.origin otherwise
});
```

## Key behaviors

| You call | In-page (same page) | Cross-page (different page) |
| --- | --- | --- |
| `router.navigate({ to })` | memory-history push/replace | `host.open()` → native page open |
| `router.history.back()` | local pop | `host.close()` → native pop |
| initial location | seeded from `getInitialHref()` | same — each page boots from its launch params |

`__TSR_index` is seeded with the native stack depth so `canGoBack()` /
`useCanGoBack()` stay correct across page boundaries. Navigation blockers run
with **no** global `document` (unlike `@tanstack/history`, which gates blocker
execution on `typeof document !== 'undefined'`).

## Tests

`pnpm --filter sparkling-history test` — 28 tests: in-page parity with
`@tanstack/history`'s memory history (ported verbatim), MPA boundary behavior,
and sparkling scheme round-tripping, all in a plain node environment.
50 changes: 50 additions & 0 deletions packages/sparkling-history/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "sparkling-history",
"version": "2.1.0-rc.12",
"description": "Reusable web-history shim that lets URL-driven routers (TanStack Router, React Router, ...) drive native multi-page navigation through a pluggable NavigationHost contract",
"homepage": "https://tiktok.github.io/sparkling/",
"repository": {
"type": "git",
"url": "https://github.com/tiktok/sparkling",
"directory": "packages/sparkling-history"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./sparkling": {
"types": "./dist/hosts/sparkling.d.ts",
"default": "./dist/hosts/sparkling.js"
}
},
"typesVersions": {
"*": {
"sparkling": ["dist/hosts/sparkling.d.ts"]
}
},
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"peerDependencies": {
"sparkling-navigation": "*"
},
"peerDependenciesMeta": {
"sparkling-navigation": {
"optional": true
}
},
"devDependencies": {
"sparkling-navigation": "workspace:*",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"license": "Apache-2.0"
}
Loading