Skip to content

modern-spf — Design

The shared-understanding record from the design-grilling session. Each branch of the design tree that was settled is listed here; the reasons behind the surprising ones live in adr/.

Decisions

BranchDecision
GoalProduction-quality personal library (tested, documented, npm-publishable)
CompatibilityConcept-compatible with SPF — not drop-in (ADR 0001)
BackboneNavigation API (window.navigation, intercept, transitionWhile) — Baseline Jan 2026
BrowsersEvergreen-only (latest + 1–2 of Chrome/Edge/Firefox/Safari), zero polyfills
DependenciesZero runtime dependencies
LanguageTypeScript 7 (native compiler), ESM-first + CJS + IIFE (spf global), built with Vite+ (vp pack → Rolldown + tsdown, .d.ts emitted by TS7)
FragmentsPer-ID fragments, innerHTML replacement; response body is {id: html}
StreamingNDJSON parts over one request (title→url→head→attr→body→foot→done); classic single JSON always accepted
RequestURL identifier ?spf=navigate | prefetch | load (configurable) + Accept negotiation
CacheHybrid: in-memory LRU + Cache API; SPF new/history eligibility split with cache-unified opt-out
ScriptsESM-native manager: import(), name→specifier registry, import-map-driven resolution (ADR 0002, ADR 0003)
StylesConstructable stylesheets (adoptedStyleSheets) with <link> fallback
Fragment scriptsUnmanaged classic <script> tags still execute (in order, wait unless async)
EventsSame spf* names; typed detail; Promise-aware dispatch; spf.navigate returns a Promise
Prefetchspf.prefetch + in-viewport trigger (IntersectionObserver); speculation rules when available; fetch fallback; prerender opt-in
TransitionsSame-document view transitions, default-on, prefers-reduced-motion respected, spf-no-transition class + per-call option
FailureFall back to full browser navigation; spferror hook
Public surfacespf.init/dispose/navigate/load/process/prefetch, spf.cache.remove/clear, slim spf.script.* / spf.style.*, spf.config, spf.event
Package@imsus/modern-spf on npm (scoped — npm requires new unscoped names to match the username)
DocsVitepress site + hand-maintained API reference (TypeDoc is incompatible with the TS7 native compiler); protocol spec lives in the site (protocol.md)
TestingVitest units + Playwright E2E
ReleaseSemantic-release + GitHub Actions
DemoLaravel (PHP) app in demo/laravel/ showing classic + streaming responses

Build order

  1. Scaffold (package, TS 7, Vite+ toolchain, Vitest, vitepress, semantic-release) — done
  2. Vertical slice: spf.initspf-link click → Navigation API intercept → ?spf=navigate fetch → single JSON apply → spfdonedone
  3. Deepen: cache (hybrid + policy) → NDJSON streaming (part events) → resources (import map, adoptedStyleSheets, unmanaged classics) → prefetch (speculation rules + viewport) → view transitions (default-on) → failure semantics — done (back/forward restore from cache, durable Cache API tier, speculation-rules injection, resource priming, transitions around apply only)
  4. Ship: IIFE build, API reference in the docs site, protocol spec, CI/release, publish checks — done (Vitepress site, hand-maintained API reference, publint + attw checks, semantic-release + GitHub Actions)
  5. Laravel demo: ?spf= detection, JSON + NDJSON responses, spf-link views — done (Laravel app in demo/laravel/: SpfPageController with single-JSON and 120ms-part NDJSON stream responses, spf-link/spf-nolink/spf-no-transition views, feature tests for the wire protocol)
  6. Test coverage: vitest (protocol, cache, registry) + Playwright E2E (navigation, streaming, back/forward, transitions, reduced-motion) — done (42 unit tests; 8 Playwright E2E specs against a zero-dependency harness server; Laravel demo verified in-browser)