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,