fix layout SSR translations and remediation follow-ups

This commit is contained in:
2026-08-09 18:49:16 +05:30
parent 51286d0fa3
commit 7d87200e31
10 changed files with 160 additions and 89 deletions
+86 -68
View File
@@ -167,7 +167,7 @@ This section is the real work. These are capability gaps, not defects, and they
are what currently limits what anyone can build. **§2.1 is the highest-value
item in this entire document.**
### 2.1 The framework fails silently, everywhere
### 2.1 Development diagnostics — FIXED
**Issue.** Nothing in §1 produced a single diagnostic at any layer. That is not
five unlucky bugs; it is a systemic property. The failure mode is always
@@ -207,10 +207,10 @@ bun test packages/csr/test/diagnostics.test.ts
bun run build && grep -c "WRN-DEV-" examples/basic-app/dist/reactive.js # expect 0
```
Add a runtime size assertion so the production bundle does not grow: it is
69,947 bytes minified today.
The production size ratchet now caps the split core runtime at 49,000 bytes; it
currently measures 48,124 minified bytes.
### 2.2 Deferred state writes are silently dropped
### 2.2 Deferred state writes — FIXED
**Issue.** State written after a client function returns — in a `setTimeout`, a
promise callback, an observer, a `pointermove` — is discarded. The peer-function
@@ -231,9 +231,9 @@ model limitation being paid for in bundle size.
interacts with re-render batching.
**How to test.** A test where a client function writes state inside a
`setTimeout` and the DOM reflects it after the timer. It must fail today.
`setTimeout` and the DOM reflects it after the timer. This failed before the fix.
### 2.3 Props and slot content render once
### 2.3 Reactive props and slot content — FIXED
**Issue.** Component props and slot content do not track page state. The
controlled-component pattern does not work at all: passing `value={pageState}`
@@ -255,9 +255,9 @@ quietly doesn't.
**How to test.** A page with `state n`, a component bound to `value={n}`, and a
button incrementing `n`; the component's rendered output must follow. Must fail
today.
before the fix.
### 2.4 A component tag inside another component's slot is dropped
### 2.4 Nested component slots — FIXED
**Issue.** Nesting a component tag in another component's slot drops the tag and
leaves its children orphaned.
@@ -267,9 +267,9 @@ it is fixed, emit the §2.1 diagnostic — a dropped component must never be
silent.
**How to test.** Render `<Card><Badge label="x" /></Card>` and assert the Badge
root element exists in the output. Must fail today.
root element exists in the output. This failed before the fix.
### 2.5 Object props must be hand-hoisted
### 2.5 Structured object props — FIXED
**Issue.** An object or array prop written inline in an attribute is read as
interpolation. Three encodings were tried this session and all failed: bare
@@ -286,17 +286,19 @@ outcomes for a plain authoring mistake.
**How to test.** A compiler test asserting the specific diagnostic for
`items={{ a: 1 }}`, and one asserting the hoisted form still compiles.
### 2.6 i18n text is not server-rendered
### 2.6 i18n text is server-rendered — FIXED
**Issue.** Translated text ships as empty spans and is filled after hydration.
Navigation is blank until the runtime loads.
**Original issue.** Translated text shipped as empty spans and was filled after
hydration. Navigation was blank until the runtime loaded.
**Why it matters.** This is an SSR-first framework failing to server-render
text. It is an SEO problem, a layout-shift problem, and it undercuts the
framework's central claim.
**Change.** Resolve the active locale during SSR and emit the translated text
into the HTML, keeping the client path for locale switches.
**Change.** Done. The active locale is resolved during SSR and translated text
is emitted into the HTML, keeping the client path for locale switches. A
follow-up found that document layouts retained the pre-translation template;
the runtime now synchronizes the translated body back to `documentTemplate`.
**How to test.**
@@ -305,13 +307,13 @@ curl -s http://localhost:3520/ | grep -c "Navigation" # expect > 0
```
Assert against the raw server response with JavaScript disabled — not the
hydrated DOM, which already looks correct today.
hydrated DOM.
---
## 3. Smaller but real
### 3.1 Twenty-two outputs still have no emitter
### 3.1 Dead outputs — FIXED (22 → 0)
Everything needed to do this in one pass is in this section: what was already
done, what is left, and which disposition each remaining component takes.
@@ -343,9 +345,9 @@ all ten rewired components. **`Map` was not verified** — its three outputs wer
converted by the same mechanical change and the build passes, but no Map was on
the probe page. Put one on a page and confirm before treating it as done.
#### 3.1.2 What is left — 22 outputs, 9 components
#### 3.1.2 Original remaining work — resolved
**Issue.** These are not miswired. All nine are **pure scaffolds**: roughly 25
**Original issue.** These were not miswired. All nine were **pure scaffolds**: roughly 25
lines each, zero state, zero functions, no style block, no event handlers. They
are markup shells that declare outputs. "Add an emitter" is not the work — there
is nothing to emit from.
@@ -405,9 +407,7 @@ self-contained. `TreeView` is medium — recursive rendering plus expand state.
**How to test.**
- The ratchet in `packages/ui/test/ui.test.ts` is pinned at 22 and only ever
moves down. Lower the ceiling **in the same commit** that fixes or removes a
component — never in a separate one, or the ratchet stops meaning anything.
- The ratchet in `packages/ui/test/ui.test.ts` is now pinned at **0**.
- For a removal: `bun run check:public-api` flags the dropped export, and the
migration entry is required before `release:prepare` will pass.
- For a build: the component renders, its behaviour works in a browser, its
@@ -415,16 +415,16 @@ self-contained. `TreeView` is medium — recursive rendering plus expand state.
output from a page and confirm it arrives — §1.1 and §1.2 are both cases where
reading the source said it worked and the browser said otherwise.
### 3.2 Twenty-eight components are still scaffolds
### 3.2 Component scaffolds — FIXED (28 → 0)
**Issue.** No style block, no functions and no event handlers — markup shells,
**Original issue.** No style block, no functions and no event handlers — markup shells,
the same shape as the Table scaffold that was removed and the LayoutSplitter
scaffold that was rebuilt. A scaffold in a published library is a promise the
library does not keep.
**Evidence.** Counted as components with no `style {}`, no `function` and no
`@handler`. An earlier figure of 23 in the audit was measured with a looser rule;
28 is the number:
28 was the number:
> AdvancedDatePicker, AdvancedRangeSlider, AuthSplitLayout, avatar, Blockquote,
> button, Chart, Clipboard, Confetti, CopyMarkup, DataMap, DragAndDrop,
@@ -437,8 +437,9 @@ AdvancedRangeSlider, Chart, Confetti, CopyMarkup, FileUpload, Toast,
ToastNotifications, TreeView. Do §3.1 first and this list drops to 19 without
any extra work. Do not plan the two items separately.
Separately, **58 components still carry the `wire-next` scaffold class** in
their markup, including many that are otherwise finished. That class is a
Separately, 58 components carried the `wire-next` scaffold class in the
original audit; **45 retain it now**. This is a cosmetic naming artefact and was
not part of the scaffold completion criterion. That class is a
generation artefact rather than a design, and it is what ties them to `ui.css`
instead of their own styles (§4.1).
@@ -458,12 +459,15 @@ Track the count down the same way as §4.1:
bun run build && gzip -c examples/basic-app/dist/ui.css | wc -c
```
### 3.3 Seven components still use Tailwind utilities
### 3.3 Tailwind component migrations — FIXED (7 → 0)
**Issue.** `List` (23 utility classes), `InputNumber` (22), `Marquee` (15),
**Original issue.** `List` (23 utility classes), `InputNumber` (22), `Marquee` (15),
`TextLink` (14), `Map` (12), `SearchBox` (8), `Timeline` (8) still style
themselves with utility classes in markup.
All seven now use local `wire-*` BEM styles. `InputNumber` styles are emitted as
component-scoped CSS and therefore do not appear in the global `ui.css` file.
**Change.** Migrate to `wire-*` BEM classes in the component's own `style {}`
block, per §0.1. Note this changes the rendered class list, so application CSS
selecting on those utilities stops matching — it needs a migration note, exactly
@@ -478,7 +482,7 @@ bun run generate:ui-visual # only after reviewing
Compare screenshots before and after; the visual result should be unchanged.
### 3.4 `List` only fires `select` for items with an href
### 3.4 `List` selection without an href — FIXED
**Issue.** The `@click` handler sits on the `item.href` branch only, so an item
without a link is not selectable. Pre-existing, and surprising.
@@ -493,28 +497,29 @@ whichever behaviour was chosen.
## 4. Delivery and the dev loop
### 4.1 `ui.css` ships whole to every page
### 4.1 Global `ui.css` migration — FIXED
**Issue.** `ui.css` is **175,848 bytes, 26,350 gzipped**, served on every page.
Only **42 of 108** components have a local `style {}` block, so 66 still depend
on it.
**Original issue.** `ui.css` was **175,848 bytes, 26,350 gzipped**, served on
every page. Only **42 of 108** components had a local `style {}` block.
**Change.** Migrate the remaining 66 under §0.1. This is mechanical, high-value,
and shrinks `ui.css` toward the global-only baseline it should be. Do it in
groups, one commit per group, regenerating the visual contract each time.
**Result.** The maintained library now has **102 of 102** components with local
styles. Built `ui.css` is **77,410 bytes / 12,764 gzipped**.
**Change.** Done in groups with the visual contract regenerated after each
migration.
**How to test.** Track the number down, every time:
```bash
ls packages/ui/components/*.wrn | wc -l # 108
grep -l '^ style {' packages/ui/components/*.wrn | wc -l # 42 today
bun run build && gzip -c examples/basic-app/dist/ui.css | wc -c # 26350 today
ls packages/ui/components/*.wrn | wc -l # 102
grep -l '^ style {' packages/ui/components/*.wrn | wc -l # 102
bun run build && gzip -c examples/basic-app/dist/ui.css | wc -c # 12764
```
Add a ratchet test asserting the gzipped size only ever decreases, so this
cannot silently regress.
### 4.2 Dev server: `packages/ui` edits do not take effect
### 4.2 Dev-server package UI hot reload — FIXED
**Issue.** Editing a component under `packages/ui` does not hot-reload. Every UI
change this session needed a full server restart, which is the single biggest
@@ -541,9 +546,9 @@ confirming it.
`packages/ui/components/*.wrn`, reload the page **without restarting**, and see
the change. That is the whole acceptance criterion.
### 4.3 Running tests wipes the cache of a live dev server
### 4.3 Per-process development caches — FIXED
**Issue.** Running `bun test` or `check:production` while a dev server is up
**Original issue.** Running `bun test` or `check:production` while a dev server was up
empties `.wrnexus`, and every client module then 404s until a restart. This
produced several confusing "the Sidebar is broken" symptoms this session that
were not component bugs at all.
@@ -553,7 +558,11 @@ were not component bugs at all.
`WRNEXUS_PRESERVE_CACHE=1`. Any dev server a test spawns against the repo root
therefore deletes the running server's cache.
**Change.** Two options, both cheap:
**Change.** The first option was implemented: each process uses
`.wrnexus-<pid>`, normal server shutdown removes its directory, and
`.wrnexus-*/` is gitignored so an interrupted process cannot pollute a commit.
The considered options were:
- Give each dev-server instance its own cache directory (keyed by port or pid),
so instances cannot collide; or
@@ -565,23 +574,22 @@ Prefer the first — it fixes the class rather than one caller. Either way,
owns.
**How to test.** Start the dev server, load a page, run `bun test packages` to
completion, reload the page without restarting. It must still work. This fails
reliably today.
completion, reload the page without restarting. It must still work.
### 4.4 Generated artefacts have an order dependency
### 4.4 Generated artefact ordering — FIXED
**Issue.** The component reference must be regenerated **before** the showcase,
**Original issue.** The component reference had to be regenerated **before** the showcase,
or newly added props and outputs are invisible to the showcase generator. This
is undocumented and cost real time.
**Change.** Make the showcase generator depend on the reference explicitly, or
fail loudly when the reference is older than the component sources.
**Change.** Done. `generate:pages` regenerates the reference first, and both
generators emit idempotent Prettier-compatible artifacts.
**How to test.** Add a prop to a component, run the showcase generator without
regenerating the reference, and assert it errors rather than silently emitting a
stale page.
### 4.5 Two authoring traps worth compiler errors
### 4.5 Compiler diagnostics for authoring traps — FIXED
**Issue.** Block comments are not allowed inside `props {}` — line comments
only — and `state page` collides with the `page` keyword. Both currently fail in
@@ -605,9 +613,9 @@ the real number.
"about 18%" of the runtime and concluded that splitting saves "3-4 kB gzipped".
Both figures were wrong, and the conclusion that followed from them was wrong.
#### The runtime today
#### Runtime before remediation
`reactive.js` is **70,101 bytes minified, 21,736 gzipped**. Removing each
`reactive.js` was **70,101 bytes minified, 21,736 gzipped**. Removing each
subsystem and re-minifying gives its true cost:
| Subsystem | Minified | Share |
@@ -631,7 +639,11 @@ fetch, which are framework features) total **23,722 minified / 6,660 gzipped —
15,076 gzipped: the expression engine, the scope and reactivity core, and loop
diffing.
#### How much of it a page actually uses
After the split, the core runtime is **48,124 minified bytes** and the
on-demand controller asset is **24,027 minified bytes**. `/` requests no
controller asset.
#### How much of it a page actually used before the split
Measured against the example app by checking which controller markers appear in
the served HTML:
@@ -662,9 +674,9 @@ bun run scripts/lib/measure-runtime-size.ts # core must stay under budget
Plus a browser check on `/`: zero controller chunks requested.
#### The bigger problem: generated client modules
#### Generated client modules — FIXED
The runtime is not where the weight is. On `/navigation`:
Before remediation, `/navigation` measured:
- **490,212 bytes decoded** across 11 client modules, **21,026 transferred**
a 23:1 compression ratio.
@@ -673,6 +685,9 @@ The runtime is not where the weight is. On `/navigation`:
- One line appears **162 times**: `brand = context.state.brand; topLinks =
context.state.topLinks; ...` — the full state-restore prologue.
After hoisting and closure deduplication, the page is **101,253 decoded bytes**;
the largest module is **51,946 bytes** with **2.4% duplicated lines**.
Gzip hides this on the wire, but **parse and compile cost scales with decoded
bytes, not transferred bytes**. Half a megabyte of JavaScript is parsed to run
one page.
@@ -732,16 +747,18 @@ bun run build
Add the ratio itself as a signal: any module compressing better than about 10:1
is duplicating itself and should fail the check. Existing behaviour is covered
by the current suite, so correctness is the 1,427 tests; this is purely a size
by the original 1,427-test suite; this is purely a size
assertion on top.
### 4.7 `generate-ui-complete-catalog.mjs` is broken and destructive
### 4.7 Destructive generator — REMOVED
**Issue.** The script fails partway through with
**Original issue.** The script failed partway through with
`TypeError: factories[entry.category] is not a function`
(`scripts/generate-ui-complete-catalog.mjs:156`) — but not before it has already
started writing. It **overwrites real components with bare scaffolds and deletes
others**, then crashes, leaving the library in a wrecked state.
started writing. It **overwrote real components with bare scaffolds and deleted
others**, then crashed, leaving the library in a wrecked state. The script has
now been removed; remaining repository-mutating tools require an explicit write
or install flag.
**Evidence.** Running it on 2026-08-09 rewrote Accordion, alert, Badge,
AvatarGroup, ToggleCount and LayoutSplitter down to ~15-line stubs, deleted 24
@@ -766,10 +783,11 @@ a case-sensitive filesystem the same script produces duplicate files instead.
Whichever is chosen, **no script that rewrites `packages/ui/components/` should
write in place.** Generate to a staging directory, validate, then move.
**Related, and worth doing regardless:** several other ungated scripts mutate
**Original related finding:** several other ungated scripts mutated
the repository or start servers when run — `install-captcha.mjs`, the
`validate-*.mjs` and `verify-*.mjs` families, and `benchmark-framework.mjs`. All
of them fail today. They should either be repaired and gated, or removed. A
`validate-*.mjs` and `verify-*.mjs` families, and `benchmark-framework.mjs`.
Obsolete validators were removed, current validators were repaired, and useful
mutating scripts now require explicit flags. A
`scripts/` directory where running a file at random can scaffold apps, start
dev servers on ports 3000-3002 and rewrite the component library is a hazard to
anyone exploring the repo, human or otherwise.
@@ -784,7 +802,7 @@ Add a check that runs each generator in `--check` mode and fails if it would
modify tracked files, the way `check:workspace` and `check:public-api` already
do.
### 4.8 Filename casing is not consistent with the git index
### 4.8 Filename casing — FIXED
**Issue.** Four components were tracked in git under lowercase names
(`card.wrn`, `container.wrn`, `divider.wrn`, `grid.wrn`) while existing on disk
@@ -809,10 +827,10 @@ must return the capitalised names. Better, set `git config core.ignorecase
false` locally so a future rename cannot hide again, and consider a check that
compares `git ls-files` against the on-disk listing byte for byte.
## 5. Order of work
## 5. Historical order of work
Ranked by return, not by size. The first item changes the cost of every item
below it, which is why it is first.
This is the execution order used during remediation, retained as implementation
history. Every row is complete.
| # | Item | Why now |
| --- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |