frontage 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ test-results/
10
+ /site/
11
+ /pyscript_examples/
12
+ .make/
13
+ .idea/
14
+ .vscode/
15
+ .DS_Store
16
+ /www/
17
+ /tools/pyscript/
18
+ .wrangler/
@@ -0,0 +1,460 @@
1
+ # Frontage design
2
+
3
+ **Status: draft 4, 2026-09-05.** Draft 1 kept PuePy's shape. Draft 2 was written from Leptos.
4
+ Draft 3 added [Solid](https://github.com/solidjs/solid) (1.x, `dom-expressions`, the store,
5
+ `solid-router`, the 2.0 release candidate), the JavaScript origin of the model and the size
6
+ reference. This draft adds the three Python-first frameworks, [Streamlit](https://github.com/streamlit/streamlit),
7
+ [Shiny for Python](https://github.com/posit-dev/py-shiny) and [Reflex](https://github.com/reflex-dev/reflex),
8
+ with one question asked of each: what happens when it has to run in WebAssembly. Two of them
9
+ already do (stlite, Shinylive), and that evidence shapes section 2b. Decisions marked *open*
10
+ are for David.
11
+
12
+ ## 1. The decision
13
+
14
+ Frontage is a **clean-room implementation**, Optersoft's own code, of a fine-grained
15
+ reactive web framework for Python in the browser. The PuePy fork on `main` moves to the
16
+ branch `puepy-reference` and serves as an acceptance test until the rewrite passes its
17
+ examples. Reasons (ownership of copyright, license and story) are in draft 1 and hold.
18
+
19
+ What carries over untouched: the name, the PyPI plan, `pyproject.toml`, the uv/ruff/ty
20
+ toolchain, `ci.yml` with the OIDC publisher, the Cloudflare Pages site, `Makefile.py`.
21
+
22
+ ## 2. What the three references teach
23
+
24
+ | | PuePy | Leptos | Solid 1.x | Solid 2.0 RC |
25
+ |---|---|---|---|---|
26
+ | Update model | rebuild page, morph DOM | fine-grained signals | fine-grained signals | fine-grained, async in the graph |
27
+ | Size | 2.5k lines Python | ~80k lines Rust | ~2.4k reactive + ~0.7k DOM + ~1.1k store | 16k lines core alone |
28
+ | Lists | morph by id | keyed diff | keyed diff, DOM moves | same, one `For` with keying modes |
29
+ | Async | none | Resource, Suspense, Action | Resource, Suspense, transitions | async memos, Loading/Errored, actions, optimistic |
30
+ | Templates | Python builder | `view!` macro to typed tree | JSX compiled to `<template>` clones | same |
31
+ | Events | per-element listeners | delegation behind a flag | delegation by default | same |
32
+
33
+ **Solid 1.x is the size proof.** A complete fine-grained framework, with stores, routing
34
+ primitives and server rendering, is about five thousand lines of JavaScript. Frontage's core
35
+ budget of ~4,000 Python lines is realistic because Solid did it.
36
+
37
+ **Solid 2.0 is the direction, not the target.** Its core is seven times larger than 1.x to
38
+ put async, transitions and optimistic writes inside the graph. Frontage takes its *names and
39
+ shapes* (two-phase effects, `Loading` / `Errored`, one `For` with keying modes, draft-based
40
+ store writes, a `Renderer` interface) and leaves the machinery for a later major version.
41
+
42
+ **What transfers from all three** is in the sections below. What does not: Rust's typed
43
+ view tree and arena handles, JSX and the Babel compiler (Python 3.14 template strings do
44
+ that job at runtime), Solid's proxy-based store internals (Python has no `Proxy`; it has
45
+ dunder methods, which are enough), transitions and time slicing.
46
+
47
+ ## 2b. The Python-first frameworks, and the WebAssembly test
48
+
49
+ | | Streamlit | Shiny for Python | Reflex |
50
+ |---|---|---|---|
51
+ | Model | the script re-runs top to bottom on every interaction; widgets return values | reactive `Value` / `calc` / `effect` graph on the server; UI functions return HTML; outputs bound by id | `State` classes with typed vars and handler methods on the server; the view compiles to React |
52
+ | Where Python runs | server (Tornado) | server (Starlette, asyncio) | server (FastAPI + websocket); the browser runs generated JavaScript |
53
+ | Size | runtime 23k + elements 46k lines | reactive 2.7k, render 6k, ui 17k, express 4k | 27k lines, plus Node and a React toolchain |
54
+ | Nesting syntax | `with st.sidebar:` | Express: `with ui.card():` | function calls |
55
+ | Partial updates | `@st.fragment` re-runs a subtree | per-output invalidation | state deltas over the websocket |
56
+ | In the browser | **stlite**: the whole runtime in Pyodide in a Web Worker, packages on demand from a 200 MB+ distribution | **Shinylive**: Pyodide plus a service worker, ~13 MB before app code, static export, code-in-URL sharing | none, and none possible: the architecture is Python-on-the-server by construction |
57
+
58
+ **What the WebAssembly test says.** Streamlit and Shiny run in the browser only by carrying
59
+ their *server* into it: a session, a message protocol, an emulated transport, and the full
60
+ Pyodide. They work, they are used, and they start in tens of seconds. Reflex cannot be
61
+ ported at all, and everything un-Pythonic in Reflex (`rx.cond` and `rx.foreach` instead of
62
+ `if` and `for`, `Var` objects that are expressions rather than values, `.to(dict)` casts) is
63
+ the price of Python *not* being present in the browser at runtime. That is the clearest
64
+ argument for Frontage's premise there is: a browser-first framework has no session, no
65
+ transport and no DSL, because the Python is right there.
66
+
67
+ **What transfers, and it is a lot:**
68
+
69
+ 1. **`with` blocks for nesting.** Streamlit, Shiny Express and PuePy converged on the same
70
+ syntax independently. The builder gets it (`with h.div(cls="card"): h.p(...)`), and it is
71
+ the natural way to generate UI in a Python loop. The template string stays the primary
72
+ syntax for static structure.
73
+ 2. **Decorators are the Pythonic spelling of reactivity.** Shiny's `@reactive.calc` /
74
+ `@reactive.effect` / `@render.text` on named functions read better than lambdas. Frontage's
75
+ `Memo`, `Effect` and `RenderEffect` accept a function, so `@Memo` and `@Effect` work as
76
+ decorators with no extra API, and a decorated `Memo` is an accessor a template hole can
77
+ name. The tutorial teaches this form.
78
+ 3. **"Not ready" as control flow.** Shiny's `req()` raises a silent exception that cancels
79
+ the computation quietly; Solid 2.0's `NotReadyError` is the same idea from the other side.
80
+ Frontage: `raise NotReady` (or `require(x)`) inside a compute ends it without error and
81
+ registers with the nearest `Loading` boundary. This is how a hole reads a `Resource`
82
+ without a `None` check.
83
+ 4. **Timers as signals.** Shiny's `invalidate_later` and `reactive.poll`, Streamlit's
84
+ `run_every`. Frontage: `interval(seconds)` returns an accessor that ticks; `poll(fn,
85
+ seconds)` a `Resource` on a timer. Small, and every dashboard wants them.
86
+ 5. **A widget catalogue.** Both data frameworks ship a fixed set of inputs (text, number,
87
+ slider, select, checkbox, radio, date, file, button) as the beginner's vocabulary. Frontage
88
+ 0.2 ships `frontage.widgets`: plain HTML form controls bound to a signal (`text_input(sig,
89
+ label=…)`), unstyled beyond a class hook, the thing the first tutorial chapter can use
90
+ before templates are taught.
91
+ 6. **Class-based state as sugar.** Reflex's `State` with typed fields and handler methods is
92
+ a shape many Python developers reach for. Frontage 0.2 offers `State` as sugar over
93
+ signals: `count = field(0)` descriptors (explicit, because MicroPython does not populate
94
+ `__annotations__`), methods as handlers, `@computed` as memos. Optional; the primitives
95
+ stay the foundation.
96
+ 7. **Static export and a playground.** Shinylive's `export` command and code-in-URL sharing
97
+ are how its docs and courses work. Frontage: `mk export` (or `frontage export`) writes a
98
+ directory of app, wheel and `pyscript.json`; the site gets a playground page that runs the
99
+ code in the URL fragment. For a company that teaches, the playground is the classroom.
100
+ 8. **Fragments are a confession.** Streamlit added `@st.fragment` because re-running the
101
+ whole script does not scale; Solid's `For` and Frontage's holes are the fine-grained
102
+ answer to the same problem, taken from the start.
103
+
104
+ **What does not transfer.** The script-rerun model itself (its simplicity is real, and
105
+ `with` nesting plus module-level `mount()` give a first chapter that reads like a script
106
+ without paying its cost); server sessions and message protocols; Reflex's React component
107
+ wrapping (Frontage's component library is web components used as HTML, section 8.2, with
108
+ Shoelace as the documented example); anything that needs a thread or `time.sleep`, which
109
+ stlite lists as broken in the browser and Frontage's async model never wanted.
110
+
111
+ **Two consequences for the site.** Shinylive and stlite both note that several apps on one
112
+ page means several interpreters; the docs embed a dozen examples per chapter. The examples
113
+ site should load one interpreter per page and mount examples into it, or lazy-load each on
114
+ scroll. And the size story is Frontage's to tell: Shinylive is ~13 MB before the app; a
115
+ MicroPython Frontage app is under a megabyte. That number belongs on the landing page once
116
+ it is measured.
117
+
118
+ ## 3. Goals
119
+
120
+ 1. **Python only, in the browser.** Reactive UI, no JavaScript toolchain, deploy by serving files.
121
+ 2. **Fine-grained.** A state change touches the DOM nodes that read it and nothing else.
122
+ 3. **Few bridge calls.** Every DOM operation from Python crosses the Python-to-JavaScript
123
+ bridge, which is the dominant cost in PyScript. The design counts crossings the way a
124
+ database design counts round trips (section 8).
125
+ 4. **Two runtimes.** Pyodide and MicroPython, one codebase, both in CI.
126
+ 5. **Small and teachable.** Core under ~4,000 lines; a reader can hold it in a day.
127
+ 6. **Renderer-agnostic views**: DOM in the browser, HTML string on CPython; server rendering
128
+ stays possible without a rewrite (section 8).
129
+ 8. **Python is the control flow.** `if`, `for`, comprehensions and function calls work
130
+ inside a view because the Python is in the browser. `Show` and `For` exist for
131
+ fine-grained efficiency, never as a required DSL. No `cond`, no `foreach`, no `Var`.
132
+ 7. **Honest testing**: the reactive core and the view layer test on CPython with no browser;
133
+ the examples run in real browsers under both runtimes.
134
+
135
+ ## 4. Non-goals for 0.x
136
+
137
+ Server rendering and hydration as shipped features; transitions and optimistic updates; a
138
+ component library; PuePy import compatibility; PyScript releases older than the pinned one.
139
+
140
+ ## 5. Clean-room rules
141
+
142
+ - **Consult:** the documentation and examples of PuePy, Leptos and Solid; the reactive-graph
143
+ algorithms as described in their books and READMEs (Reactively's article, Solid's
144
+ "fine-grained reactivity" guide); PuePy's browser tests as an acceptance specification.
145
+ - **Do not copy:** source, tests, docstrings or prose from any of them. Code is written from
146
+ `SPEC.md` (section 13) with no reference repository open.
147
+ - **Old tree:** branch `puepy-reference`, never merged, deleted after 0.1.0.
148
+ - **Credit** in README: "Frontage's reactive model follows Solid and Leptos; the project began
149
+ as a fork of PuePy." True, courteous, not required.
150
+ - **Every PR states** it was written from the spec without reference source open.
151
+
152
+ ## 6. Platform
153
+
154
+ | | |
155
+ |---|---|
156
+ | PyScript | ≥ 2026.7.3; the examples pin one exact release, served locally in CI |
157
+ | Pyodide | 3.14; Python 3.14 semantics incl. template strings (PEP 750) |
158
+ | MicroPython | the build PyScript ships (template strings, `weakref`, `asyncio.Future` since 2026.3.1) |
159
+ | Browsers | evergreen Chromium, Firefox, WebKit |
160
+ | Server | CPython ≥ 3.12 for tests, tooling, the string renderer |
161
+
162
+ The package is written in the MicroPython subset (string annotations, no runtime `typing`,
163
+ no dataclasses). The bridge is `pyscript.document` / `window` / `ffi` / `js_modules` /
164
+ `asyncio`, present on both interpreters; `pyscript.web` is not used. *Open:* MicroPython
165
+ first-class or best-effort. Proposal: first-class.
166
+
167
+ ## 7. The reactive core (`frontage.reactive`)
168
+
169
+ The model is Solid's, which Leptos confirmed: signals (sources), memos (derived, cached,
170
+ lazy), effects (subscribers that touch the outside world), all with automatic, dynamic
171
+ dependency tracking under an owner tree.
172
+
173
+ ```python
174
+ from frontage import Signal, Memo, Effect, batch, untrack
175
+
176
+ count = Signal(0)
177
+ double = Memo(lambda: count() * 2) # accessors are callables
178
+ Effect(lambda: double(), lambda v, prev: print("double is", v))
179
+ count.set(2)
180
+ count.update(lambda n: n + 1)
181
+ ```
182
+
183
+ **Accessors are callables.** A `Signal`, a `Memo`, a `Resource` and a plain `lambda` are
184
+ all read by calling them. This is Solid's rule and it is what makes the view layer simple:
185
+ *any callable in a child or attribute position is reactive*; anything else is static.
186
+
187
+ **Propagation** is Solid 1.x's, chosen over Solid 2.0's microtask batching because reads
188
+ after a write return the new value, which is what a beginner expects and what tests can
189
+ assert without an event loop. Three node states, Clean / Check / Dirty. A write marks
190
+ dependents; pure nodes (memos) recompute in topological order within the same batch;
191
+ effects run once at the end of the outermost `batch()`, every write being an implicit
192
+ batch. Memos compare with `==` by default and take `equal=`. `untrack()` reads without
193
+ subscribing. `on(deps, fn)` for explicit dependencies.
194
+
195
+ **Two-phase effects** (Solid 2.0). `Effect(compute, effect)`: `compute` runs tracked and
196
+ returns a value; `effect(value, prev)` runs untracked after the batch and may return a
197
+ cleanup. This makes the two classic mistakes impossible by construction: side effects
198
+ subscribing to signals they happened to read, and writes inside tracked code. A one-argument
199
+ `Effect(fn)` shorthand tracks `fn` and is documented as the rough tool. `RenderEffect` is the
200
+ same with the effect phase run before user effects; the view layer uses it.
201
+
202
+ **Ownership.** `Owner` tree; every effect and memo is created under the current owner; a
203
+ component body and every `For` row run under their own owner (Solid creates a root per row,
204
+ which is what lets one row dispose without touching the others). `owner.dispose()` runs
205
+ `on_cleanup` callbacks, cancels effects, disposes children, **destroys every JavaScript
206
+ proxy registered under it**, and releases delegated-event registrations. `Context`:
207
+ `provide(ctx, value)` / `use(ctx)` walk the owner tree.
208
+
209
+ **Utilities carried from Solid:** `selector(source)` for O(1) "is this row selected"
210
+ checks in a list; `on_mount(fn)`; `get_owner()` / `run_with_owner()`; `children(fn)` to
211
+ resolve a child accessor once. **From Shiny:** `NotReady` (section 2b), `interval(seconds)`
212
+ and `poll(fn, seconds)`. **Decorator form** for `Memo`, `Effect` and `RenderEffect`, which
213
+ falls out of them taking a function and is the form the tutorial teaches.
214
+
215
+ **Stores (`frontage.store`), in 0.1.** Python application state is dicts and lists, so
216
+ nested reactivity is not an optimisation, it is the default case. Solid's design transfers
217
+ without `Proxy`: a `Store` wraps a dict or list in an object whose `__getitem__` /
218
+ `__getattr__` / `__iter__` / `__len__` lazily create one signal node per key on a *tracked*
219
+ read (so untracked keys cost nothing), and re-wrap nested containers on the way out. Writes
220
+ go through a draft: `store.set(lambda s: s["todos"].append(...))`, applied inside a batch
221
+ with the wrapper's `__setitem__` notifying exactly the keys touched (Solid 2.0 made this the
222
+ only write form; 1.x's path syntax `set("user", "name", v)` is offered as `store.set_path`).
223
+ `reconcile(data, key="id")` merges fresh server data into an existing store preserving row
224
+ identity, so a `For` over it moves rows instead of recreating them. `snapshot(store)`
225
+ returns plain data.
226
+
227
+ The whole module is testable on CPython with no browser, and that suite is where the
228
+ framework's correctness lives.
229
+
230
+ ## 8. Views (`frontage.view`)
231
+
232
+ ### 8.1 Templates first, because of the bridge
233
+
234
+ Solid compiles JSX to a static HTML string per template, creates a `<template>` element
235
+ once, and instantiates it with a single `cloneNode(true)`; only the dynamic holes are then
236
+ touched. Leptos does the same. For PyScript this is not an optimisation to add later; it is
237
+ the difference between one bridge crossing per instance and one per element and attribute.
238
+
239
+ So Frontage's unit of rendering is the **`Template`**: a static HTML skeleton plus a list of
240
+ holes (a text position, an attribute, an event, a child slot), each with a path to its
241
+ node. Instantiation is: clone the skeleton in one call, locate the hole nodes, bind each
242
+ hole with a `RenderEffect` (or a static value). Both front ends below compile to it.
243
+
244
+ **The template string** (Python 3.14, both interpreters), parsed once per call site and
245
+ cached, is the primary syntax and what the tutorial teaches:
246
+
247
+ ```python
248
+ def counter(initial=0):
249
+ count = Signal(initial)
250
+ return html(t"""
251
+ <div class="counter">
252
+ <button on:click={lambda e: count.update(lambda n: n - 1)}>-</button>
253
+ <span>Value: {count}!</span>
254
+ <button on:click={lambda e: count.update(lambda n: n + 1)}>+</button>
255
+ </div>
256
+ """)
257
+ ```
258
+
259
+ **The builder** is the fallback and the escape hatch, and it produces the same `Template`
260
+ when its structure is static. It has a call form and a `with` form; the second is what
261
+ Streamlit, Shiny Express and PuePy all arrived at, and the natural one inside a loop:
262
+
263
+ ```python
264
+ h.div(h.button("-", on_click=dec), h.span("Value: ", count, "!"), h.button("+", on_click=inc), cls="counter")
265
+
266
+ with h.ul(cls="menu"):
267
+ for item in items:
268
+ h.li(item.label, on_click=lambda e, i=item: select(i))
269
+ ```
270
+
271
+ *Open:* if template strings prove immature on either interpreter, the builder ships alone
272
+ in 0.1 and the template follows. Proposal: builder in M1, template in M2, tutorial on the
273
+ template.
274
+
275
+ ### 8.2 The insert rules
276
+
277
+ A child slot follows Solid's `insert` semantics exactly, because they are complete and
278
+ small: `str`/`int`/`float` → a text node updated in place; `None`/`bool` → nothing; a
279
+ callable → a `RenderEffect` around the same rules; a list → flattened, callables resolved,
280
+ then reconciled against the current nodes by identity (prefix, suffix, swap, then a map;
281
+ existing nodes are moved, not recreated); a view → mounted. A component's output is a view
282
+ or a list of nodes, never a wrapper element.
283
+
284
+ Attributes: a keyword or template attribute whose value is a callable is bound. Prefixes
285
+ name the DOM's real distinctions: `attr:` (default), `prop:` (a DOM property, the one form
286
+ inputs need for `value`), `class:name={bool}` and `class={dict}`, `style:prop`, `on:event`,
287
+ `bind:value` / `bind:checked` / `bind:group` (two-way), `ref={NodeRef()}`. Boolean
288
+ attributes are set and removed, never written as `"false"`.
289
+
290
+ ### 8.3 Components and control flow
291
+
292
+ A component is a function of keyword props that runs once under its own owner and returns a
293
+ view. Reactive props are accessors; static props are values; `children` is a callable. No
294
+ base class. Control flow is components:
295
+
296
+ | | |
297
+ |---|---|
298
+ | `Show(when, fallback, children)` | one branch mounted; `children` may be a function of the narrowed value (`keyed=True` re-creates on value change, Solid's rule) |
299
+ | `For(each, children, key=…)` | keyed by identity by default; `key=fn` for an extracted key; `key=False` for index mode where the item is an accessor and rows are reused positionally (Solid 2.0 folded `Index` into `For` this way) |
300
+ | `Switch` / `Match` | first matching branch |
301
+ | `Loading(fallback, children)` | Solid 2.0's name for Suspense: fallback while any `Resource` read beneath is loading; nested boundaries |
302
+ | `Errored(fallback, children)` | Solid 2.0's name for ErrorBoundary: `fallback(error, reset)` |
303
+ | `Dynamic(component, **props)` | component chosen at runtime |
304
+ | `Portal(mount, children)` | render elsewhere in the DOM |
305
+
306
+ ### 8.4 The renderer seam
307
+
308
+ The view layer talks to a `Renderer` with Solid's universal-renderer surface, ten methods:
309
+ `create_element`, `create_text`, `replace_text`, `set_property`, `insert_node`,
310
+ `remove_node`, `is_text`, `parent`, `first_child`, `next_sibling`, plus `clone_template`
311
+ and the event hooks. Solid's `universal` package is the proof that a whole framework can
312
+ sit on this seam. Two implementations: `DomRenderer` in the browser and `HtmlRenderer` on
313
+ CPython, which is how views are unit-tested without a browser and the seam server rendering
314
+ would plug into later. Hydration markers are designed now and shipped never, in 0.x.
315
+
316
+ ### 8.5 Events
317
+
318
+ Delegated, as in Solid: one listener per event type on the document for the bubbling
319
+ events (click, input, keydown, pointer and touch events, focusin/out, …); the handler walks
320
+ from the target up to the mount root and dispatches to the Python handler registered for
321
+ that element, simulating `currentTarget`, honouring `disabled` and `stopPropagation`.
322
+ Non-bubbling events attach directly. `on:` on the template forces a direct listener;
323
+ `oncapture:` for the capture phase. Handlers may be `async def`. In PyScript this turns one
324
+ `create_proxy` per handler into one per event type, and a disposed owner drops its
325
+ registrations from a dict.
326
+
327
+ ### 8.6 A small JavaScript shim, measured
328
+
329
+ *Open, and the one place this design proposes shipping JavaScript.* Two hot paths cross the
330
+ bridge many times per operation when written in Python: locating a clone's hole nodes
331
+ (one call per `firstChild` / `nextSibling` step) and the delegated event walk (one call per
332
+ ancestor). A ~100-line `frontage.js`, loaded through `js_modules` like morphdom was, can do
333
+ each in a single call: `holes(root)` returns the hole nodes of a clone, `dispatch(event)`
334
+ walks to the registered ancestor and calls Python once. The user never sees it. Proposal:
335
+ build pure Python first, measure with the js-framework-benchmark rows example on both
336
+ runtimes, and add the shim only where the numbers say so.
337
+
338
+ ## 9. Async (`frontage.reactive`, continued)
339
+
340
+ `Resource(fetcher, source=None)`: `fetcher` is an `async def`; it re-runs when `source`
341
+ changes; the resource is an accessor returning the latest value (or `None`) with
342
+ `.loading`, `.error`, `.state` as accessors and `.refetch()` / `.mutate()` for imperative
343
+ control. Reading it under a `Loading` boundary registers with that boundary's counter
344
+ (Solid's increment/decrement design). `Action(fn)`: `.dispatch(input)`, with `.pending`,
345
+ `.value`, `.input` accessors; what a form submits to. Tasks are spawned with
346
+ `asyncio.create_task` on both interpreters and owned, so a disposed owner cancels them.
347
+
348
+ One rule, documented and enforced with a dev-mode warning because Python makes it easy to
349
+ get wrong: **read every reactive input before the first `await`.** After an `await` the
350
+ tracking context is gone. Solid 2.0 states the same rule for its async memos.
351
+
352
+ Solid 2.0's async-in-the-graph (memos returning awaitables, `is_pending`, `latest`,
353
+ `refresh`, generator-based transactional actions, optimistic values) is the model Frontage
354
+ 1.0 should grow toward once 0.x has users. It is written down here so 0.x does not paint
355
+ over it: `Resource` is designed to be replaceable by an async `Memo` without changing the
356
+ `Loading` / `Errored` boundaries.
357
+
358
+ ## 10. Router (`frontage.router`)
359
+
360
+ Solid-router's shape, which is Leptos's with data loading co-located:
361
+
362
+ ```python
363
+ router = Router(
364
+ Route("/", Home),
365
+ Route(
366
+ "/users",
367
+ Users,
368
+ children=[
369
+ Route("/", UserList),
370
+ Route("/:id", User, preload=preload_user),
371
+ ],
372
+ ),
373
+ Route("/*any", NotFound),
374
+ mode="history", # or "hash" for the no-server tutorial case, or "memory" for tests
375
+ )
376
+ mount("#app", router)
377
+ ```
378
+
379
+ - **Nested routes**; a parent route component receives the matched child as `children`.
380
+ - **Params, query, location** as accessors: `use_params()["id"]` is reactive.
381
+ - **`preload`** per route: called when a route is about to render and, eagerly, when a link
382
+ is hovered; returns nothing, warms `query` caches. Solid's render-as-you-fetch.
383
+ - **`query(fn)`**: a keyed async cache with de-duplication and `revalidate()`; a `Resource`
384
+ reading it gets the cached value first.
385
+ - **`action(fn)`** and **`use_submission()`** for mutations bound to forms, with
386
+ `redirect()` raised from inside.
387
+ - **Plain `<a>` works** (document-level interception); `A(href)` adds relative resolution
388
+ and the active class. `Navigate(to)`, `navigate()`, `use_before_leave()`, scroll
389
+ restoration. **Memory mode** is what makes the router unit-testable.
390
+
391
+ ## 11. Errors and development mode
392
+
393
+ Exceptions in a component body or effect route to the nearest `Errored`; with none, to the
394
+ mount root, which in debug mode renders the traceback with the component named and in
395
+ production renders a configured fallback and logs. Dev-mode warnings for the three mistakes
396
+ the design cannot prevent: a signal read after `await`, a write inside a tracked compute,
397
+ and a `For` over unstable keys.
398
+
399
+ ## 12. The three questions the measurements answer
400
+
401
+ Before M2 the rows example from js-framework-benchmark runs under both runtimes with a
402
+ harness that counts bridge crossings and wall time for create 1,000 rows, update every
403
+ tenth row, swap two rows, remove a row, clear. It decides: whether the JavaScript shim of
404
+ 8.6 is needed and where; whether MicroPython is first-class or best-effort; and whether
405
+ the builder without templates is acceptable in 0.1. Numbers, not opinions.
406
+
407
+ ## 13. Testing
408
+
409
+ - `SPEC.md` first: behaviours, one line each, grouped by chapter, in our words.
410
+ - **Reactive core**: exhaustive on CPython: the diamond and branching graphs, dynamic
411
+ dependency changes, cleanup order, batch semantics, store node laziness.
412
+ - **Views** through `HtmlRenderer` (structure) and a recording fake renderer (which
413
+ operations a change caused, and how few).
414
+ - **List reconciliation**: property-based against a brute-force reference: random
415
+ sequences of keys, the DOM order equals the target, operation counts bounded.
416
+ - **Browser suite**: the examples under Playwright, `py` and `mpy`, Chromium every run,
417
+ Firefox and WebKit nightly, PyScript served locally, `autouse` server fixture.
418
+ - **The rows benchmark** of section 12, run in CI for regressions.
419
+
420
+ ## 14. Documentation and distribution
421
+
422
+ Docs on `academy.optersoft.com/tool/frontage`, one chapter per concept in the order of this
423
+ document, each with its live example on `frontage.optersoft.com/examples/…`. Distribution:
424
+ the wheel on PyPI and mirrored on the site; a `pyscript.json` of a few lines is the whole
425
+ install. Reference generated from docstrings.
426
+
427
+ ## 15. License and ownership
428
+
429
+ **Apache License 2.0**, copyright Optersoft, S.L. Section 3 grants patents both ways,
430
+ section 5 makes contributions arrive under the same terms with no CLA, section 6 reserves the
431
+ Frontage trademark. Not the MIT OR Apache pair: a user choosing MIT would take none of those
432
+ obligations. PyScript is Apache 2.0; Solid and Leptos are MIT, which is why their ideas may
433
+ be studied freely and their code is not copied regardless. The fork on `puepy-reference`
434
+ keeps its own notice; the rewrite carries Optersoft's from the first commit.
435
+
436
+ ## 16. Milestones
437
+
438
+ | | Deliverable | Done when |
439
+ |---|---|---|
440
+ | M0 | `SPEC.md`; `puepy-reference` branch; skeleton; `Renderer` protocol with `HtmlRenderer` and a recording fake; local PyScript fixture | `mk check` green; hello-world renders to a string |
441
+ | M1 | reactive core (signals, memos, two-phase effects, owner, context, batch, `on`, `selector`); `Store` with draft writes; builder → `Template`; `DomRenderer`; delegated events; insert rules; `Show`, `For` (all keying modes); `bind:`; counter, todo and rows examples | unit suite green; browser suite green for those examples on both runtimes; the rows benchmark runs |
442
+ | M2 | `t"…"` templates; `Resource`, `Action`, `Loading`, `Errored`; `NodeRef`; `Dynamic`, `Portal`; fetch and forms examples; the shim decision of 8.6 | same; section 12 decided |
443
+ | M3 | router (nested, params, `preload`, `query`, `action`, `A`, three modes); contacts example; debug error page | full example suite green on Chromium, both runtimes |
444
+ | M4 | docs on academy; landing page with the measured size; wheel on the site; `mk export` | **0.1.0** on PyPI |
445
+ | M5 | `frontage.widgets`; `State` sugar; `interval` / `poll`; `reconcile`; the playground page; Firefox + WebKit in CI; performance pass | **0.2.0** |
446
+ | later | server rendering through `HtmlRenderer`; hydration; async memos, `is_pending`, transactions and optimistic writes (Solid 2.0's model) | 1.0 |
447
+
448
+ ## 17. Open decisions
449
+
450
+ 1. MicroPython first-class or best-effort. Proposal: first-class, confirmed by section 12.
451
+ 2. Template strings in M2 with the builder as fallback. Proposal: yes.
452
+ 3. The JavaScript shim of 8.6. Proposal: pure Python first, shim only where measured.
453
+ 4. Solid 1.x synchronous propagation versus Solid 2.0 microtask batching. Proposal: 1.x.
454
+ 5. Whether `Store` is in 0.1 (this draft says yes, because Python state is dicts) or 0.2.
455
+ 6. Accessor spelling: `count()` (Solid) or `count.get()` (Leptos). Proposal: `count()`, so
456
+ the rule "a callable is reactive" has no exceptions; `.value` as a read-only property
457
+ alias for people who find bare calls odd.
458
+ 7. Whether `frontage.widgets` (the Streamlit/Shiny input catalogue) belongs in the core
459
+ package or in a second package. Proposal: a subpackage of the core, so `pip download
460
+ frontage` is still the whole install.