From 7d87200e312fdb792ddd88dc57f50a5af9935eb7 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sun, 9 Aug 2026 18:49:16 +0530 Subject: [PATCH] fix layout SSR translations and remediation follow-ups --- .gitignore | 1 + docs/framework-remediation-plan.md | 154 ++++++++++-------- examples/basic-app/app/example.test.ts | 6 +- .../apps/web/test/inter-app.test.ts | 25 +-- package.json | 2 +- packages/dev-server/src/index.ts | 3 +- packages/dev-server/src/runtime.ts | 9 +- .../dev-server/test/authz-startserver.test.ts | 7 +- .../component-composition-runtime.test.ts | 39 +++++ packages/ui/test/ui.test.ts | 3 + 10 files changed, 160 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index 26bd7488..32d049b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ .wrnexus/ +.wrnexus-*/ *.log *.db *.db-shm diff --git a/docs/framework-remediation-plan.md b/docs/framework-remediation-plan.md index 3682c1d8..a0710a30 100644 --- a/docs/framework-remediation-plan.md +++ b/docs/framework-remediation-plan.md @@ -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 `` 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-`, 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 | | --- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/examples/basic-app/app/example.test.ts b/examples/basic-app/app/example.test.ts index 4949da6f..72bbcbc7 100644 --- a/examples/basic-app/app/example.test.ts +++ b/examples/basic-app/app/example.test.ts @@ -60,10 +60,14 @@ describe("full app", () => { }); afterAll(() => app?.close()); - test("home page responds with HTML", async () => { + test("home page server-renders translated page and layout text", async () => { const res = await app.fetch("/"); expect(res.status).toBe(200); expect(res.headers.get("content-type")).toContain("text/html"); + const html = await res.text(); + expect(html).toContain('Home'); + expect(html).toContain('Hello from WrNexus'); + expect(html).not.toContain(''); }); test("GET /api/hello returns a translated greeting", async () => { diff --git a/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts b/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts index 202af8de..84aab15b 100644 --- a/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts +++ b/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts @@ -1,7 +1,6 @@ import { afterEach, expect, test } from "bun:test"; -import { implement } from "../../../../../packages/rpc/src/index.ts"; import { handleRpcRequest } from "../../../../../packages/dev-server/src/rpc-dispatch.ts"; -import { catalogService } from "../../../packages/shared/src/index.ts"; +import catalogServiceImplementation from "../../admin/app/services/catalog.ts"; import { GET } from "../app/api/product.ts"; const secret = process.env.WRNEXUS_RPC_SECRET; @@ -16,23 +15,17 @@ afterEach(() => { else process.env.WRNEXUS_INTERNAL_ORIGINS = origins; }); -function startAdmin(allowed: boolean) { - const service = implement( - catalogService, - { getProduct: ({ sku }) => ({ sku, name: "WRNexus Starter", priceCents: 4900 }) }, - { - selfApp: "admin", - checkPermission: (permission, subject) => - allowed && permission === "catalog:read" && subject?.subjectId === "demo-user", - }, - ); +function startAdmin() { return Bun.serve({ port: 0, hostname: "127.0.0.1", async fetch(request) { return ( - (await handleRpcRequest(request, new URL(request.url), new Map([["catalog", service]]))) ?? - new Response("Not found", { status: 404 }) + (await handleRpcRequest( + request, + new URL(request.url), + new Map([["catalog", catalogServiceImplementation]]), + )) ?? new Response("Not found", { status: 404 }) ); }, }); @@ -41,7 +34,7 @@ function startAdmin(allowed: boolean) { test("web calls admin through private RPC when permitted", async () => { process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long"; process.env.WRNEXUS_APP_NAME = "web"; - const server = startAdmin(true); + const server = startAdmin(); process.env.WRNEXUS_INTERNAL_ORIGINS = JSON.stringify({ admin: `http://127.0.0.1:${server.port}`, }); @@ -62,7 +55,7 @@ test("web calls admin through private RPC when permitted", async () => { test("web returns 403 when admin denies catalog:read", async () => { process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long"; process.env.WRNEXUS_APP_NAME = "web"; - const server = startAdmin(false); + const server = startAdmin(); process.env.WRNEXUS_INTERNAL_ORIGINS = JSON.stringify({ admin: `http://127.0.0.1:${server.port}`, }); diff --git a/package.json b/package.json index e2244d64..4970616a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write", "sbom": "node scripts/generate-sbom.mjs", "benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write", - "check:production": "bun run check:workspace && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check", + "check:production": "bun run check:workspace && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples", "validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs", "stage:packages": "bun run scripts/publish-packages.ts", "test:staged-consumers": "node scripts/test-staged-consumers.mjs", diff --git a/packages/dev-server/src/index.ts b/packages/dev-server/src/index.ts index 5a98f0fd..5dbbeeb4 100644 --- a/packages/dev-server/src/index.ts +++ b/packages/dev-server/src/index.ts @@ -6,7 +6,7 @@ * the running process while the HMR socket morphs fresh HTML into the browser. */ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { resolve, dirname, isAbsolute, join } from "node:path"; import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core"; import { buildRouter, type Router } from "@wrnexus/router"; @@ -744,6 +744,7 @@ export async function startServer(opts: ServeOptions): Promise { unsubscribeDevToolbar?.(); server.stop(); void pluginRunner.hook("shutdown"); + rmSync(cacheDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); }, }; } diff --git a/packages/dev-server/src/runtime.ts b/packages/dev-server/src/runtime.ts index 0710eb7d..bbae98a3 100644 --- a/packages/dev-server/src/runtime.ts +++ b/packages/dev-server/src/runtime.ts @@ -1815,7 +1815,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers { if (isMobileRequest) body = revealMobileOnlyHtml(body); // i18n: resolve `{t:key}` / `t:attr` markers against the request language. - if (deps.i18n) body = translateHtml(body, ctx.t); + if (deps.i18n) { + body = translateHtml(body, ctx.t); + // renderDocument prefers the complete document template when one was + // rendered. Keep it in sync with the translated body; otherwise markers + // owned by document/page layouts are replaced by the stale pre-translation + // template even though page-only responses translate correctly. + if (documentTemplate) documentTemplate = body; + } // Point 3: only ship the JS this page actually uses. const scripts = collectScripts(body, deps.clientRuntimes, deps.navigation).map((script) => diff --git a/packages/dev-server/test/authz-startserver.test.ts b/packages/dev-server/test/authz-startserver.test.ts index a4ca0251..b34b5ea5 100644 --- a/packages/dev-server/test/authz-startserver.test.ts +++ b/packages/dev-server/test/authz-startserver.test.ts @@ -1,5 +1,5 @@ import { afterAll, describe, expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { getAuthzCatalog, hasAuthzCatalog } from "@wrnexus/authz"; import { startServer } from "../src/index.ts"; @@ -63,9 +63,14 @@ export default defineAuthz({ permissions: { "post:read": { title: "View posts" } try { expect(hasAuthzCatalog()).toBe(true); expect(getAuthzCatalog().permissions.has("post:read")).toBe(true); + const cacheDir = join(appDir, "..", `.wrnexus-${process.pid}`); + mkdirSync(cacheDir, { recursive: true }); + writeFileSync(join(cacheDir, "generated.js"), "generated", "utf8"); + expect(existsSync(cacheDir)).toBe(true); } finally { server.stop(); } + expect(existsSync(join(appDir, "..", `.wrnexus-${process.pid}`))).toBe(false); }); test("an app with no app/authz declarations boots without throwing", async () => { diff --git a/packages/dev-server/test/component-composition-runtime.test.ts b/packages/dev-server/test/component-composition-runtime.test.ts index ea185c10..0110800f 100644 --- a/packages/dev-server/test/component-composition-runtime.test.ts +++ b/packages/dev-server/test/component-composition-runtime.test.ts @@ -73,3 +73,42 @@ test("translated text is present in the raw server response", async () => { const html = await response!.text(); expect(html).toContain('Navigation'); }); + +test("translated layout text survives document-template rendering", async () => { + const root = mkdtempSync(join(tmpdir(), "wrnexus-layout-ssr-i18n-")); + roots.push(root); + const app = join(root, "app"); + mkdirSync(join(app, "pages"), { recursive: true }); + mkdirSync(join(app, "layouts"), { recursive: true }); + writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n"); + writeFileSync(join(app, "layouts/public.wrn"), "layout Public { view { } }\n"); + writeFileSync( + join(app, "layouts/document.wrn"), + "layout Document { view { } }\n", + ); + + const handlers = createHandlers({ + mode: "development", + hmr: false, + router: buildRouter(app), + i18n: resolveI18n({ en: { nav: { home: "Home" } } }, { default: "en" }), + loadModule: async (file) => { + if (basename(file) === "public.wrn") { + return { render: () => '' }; + } + if (basename(file) === "document.wrn") { + return { render: () => "" }; + } + return { default: () => "
Page
", layout: "public" }; + }, + getMiddleware: async () => [], + assets: { serve: async () => null }, + } satisfies RuntimeDeps); + + const response = await handlers.fetch(new Request("https://example.test/"), { + upgrade: () => false, + }); + const html = await response!.text(); + expect(html).toContain('Home'); + expect(html).not.toContain(''); +}); diff --git a/packages/ui/test/ui.test.ts b/packages/ui/test/ui.test.ts index 9a27d045..26b15608 100644 --- a/packages/ui/test/ui.test.ts +++ b/packages/ui/test/ui.test.ts @@ -1671,6 +1671,9 @@ test("carousel autoplay wraps to the first slide", async () => { test("input number increments, decrements, applies steps, and respects limits", async () => { const source = readFileSync(uiComponentPath("InputNumber"), "utf8"); + const compiled = compileWireFile(source, uiComponentPath("InputNumber")); + expect(compiled).toContain("export const __wrnexusStyles"); + expect(compiled).toContain(".wire-input-number__control"); const html = await renderComponent(source, { name: "quantity", value: 1,