Server module now declares `const api = { ... }` for apis {} blocks in
mode "any", dispatching in-process via requireRequestContext + the
existing __wrnexusCallApi transport helper. The try wraps only the
transport call; the response body runs after it, outside the try, so
a bug in the author's response code surfaces rather than being
mistaken for a request failure. A block with no error {} section
rethrows instead of resolving undefined.
Also closes the pageCtx.__wrnexusCallApi wiring gap in
dev-server/runtime.ts: it now forwards input through to
callApiFromContext instead of dropping it.
The old assertions only searched REACTIVE_RUNTIME for substrings; they never
touched buildApiRequest and were not anchored to the content-type line they
claimed to guard, so they could not detect drift on either side. Replace
with a fixture-driven test that runs both implementations on the same
(path, method, input) cases and compares the actual request they produce.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
server.fn() posts to POST /__wrnexus/rpc. Dev intercepts that path before
handlers.fetch and routes it to a createRpcHandler instance built from
loadWrnServerModule; createProductionServer/createProductionHandlers had no
such route, so the request fell through to the internal-caller-gated
inter-app service RPC and 404'd.
Add resolveProdServerFunctions(), a synchronous equivalent of dev's resolve
that searches the already-statically-imported ProdManifest components/pages/
layouts for __wrnexusServerFunctions + __wrnexusRpcManifest, and wire it into
createProductionHandlers with the same validateCsrf + withServerFnRequestContext
wrapping dev uses. Move those two helpers into a new rpc-shared.ts so prod.ts
can use them without a circular import through index.ts.
Add packages/dev-server/test/prod-server-fn-rpc.test.ts covering a successful
call, CSRF rejection, and clean 404s for an unknown component/function.
Wraps the three additional entry points where user server code runs
outside fetchHandler's own context wrap:
- the server-function RPC path (/__wrnexus/rpc) intercepted before
handlers.fetch in the dev server (index.ts) - what server.fn() travels
- the service RPC path (isRpcPath) inside fetchHandler, which runs
implement()/implementStream() service code before ctx existed
- the HMR-sync handler, which runs real load blocks/actions via dispatch()
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bare bodies inside apis {} silently discarded their text with no error,
producing a do-nothing block. They now throw a ParseError naming the entry
and pointing at the response {} section. Duplicate-name detection for
dataApis moved from an incremental, order-dependent check (only saw prior
entries in the array) to a single post-parse pass over the whole ast.dataApis,
so it catches cross-mode duplicates (apis {} vs ssr { api }) regardless of
declaration order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- generate-complete-framework-report.mjs no longer claims legacyEmit/
legacyEventProps/legacyComponentDiscovery/stringLayouts/
functions.legacyDefaultRuntime are usable compatibility flags; they were
removed before the first public release and a config setting them is now
rejected. Regenerated docs/WRNEXUS-COMPLETE-FEATURE-REPORT.md.
- Restored the update.test.ts coverage lost in the sub-0.8.0 migration
cleanup: a 0.8.x fixture now asserts refreshFrameworkFiles' still-live
behaviour (public/llms.txt and CLAUDE.md creation) and that
pkg.wrnexus.version stays at its old value after an unverified update.
The .gitignore refresh and build/start/production script backfill were
themselves removed as part of dropping the sub-0.8.0 migrations that
implemented them, so there is nothing left to cover for those two.
- updateApp now logs a clear warning when the detected project version is
below 0.8.0, naming the version and stating that automated migration from
below 0.8.0 is no longer supported, without failing the command (a
marker-less app, like examples/basic-app, is benign and must still
upgrade cleanly).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- regenerate docs/public-api-0.8.json (removals only: CompatibilityPolicy,
CompatibilityReport, CURRENT_COMPATIBILITY_DATE, CURRENT_FRAMEWORK_BEHAVIOUR,
isCompatibilityDate, resolveCompatibility from @wrnexus/styles)
- regenerate docs/WRNEXUS-COMPLETE-FEATURE-REPORT.md
- remove the deleted 'wrnexus compatibility' command row and its config-pinning
sentence from packages/cli/README.md
- drop compatibilityDate/frameworkBehaviour example fields from docs/GUIDE.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The single-branch 'shared'-only action fixture failed to infer through
defineStore's actions generic, typing store.increment as never and
failing bun run typecheck (TS2349) even though bun test passed.
Explicit type arguments fix the inference without weakening the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix round 2 for task 1: store-codegen.ts stopped emitting runtime:
"legacy" actions in round 1, making the StoreRuntime variant and its
resolution fallback dead. Narrows StoreRuntime to three variants and
adds a regression test for the remaining options.runtime -> shared
fallback chain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix round 1 for task 1: packages/typecheck also branched on the
removed legacy runtime (componentContract's exclusion filter and the
runtime-namespace loop). Removes both, adds a regression test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typed, callable api blocks for .wrn files: sectioned request/response/error,
callable from client code as api.name(input), type-checked by tsc against the
route contracts.
Also two fixes found along the way: defineEndpoint never received its input
through the real router, so every typed endpoint was validating undefined; and
v.boolean() silently coerced any unrecognised string to false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
checkField in packages/validation/src/index.ts (and its browser mirror in
runtime.ts) treated any string other than "true"/"on" as false with no
error, so typos like "treu" or values like "yes"/"1"/"TRUE" silently
passed as false.
Now:
- true/false booleans pass through unchanged
- recognised true strings (case-insensitive, trimmed): true, on, 1, yes
- recognised false strings: false, off, 0, no
- numeric 1/0 coerce (JSON payloads)
- undefined/null/"" still coerce to false (unchecked-checkbox semantics)
- anything else is now a type error (desc.typeMessage or "Must be true or
false") instead of a silent false
Locked-in behaviours preserved: a required boolean given false still
errors, and parseEnv DEBUG: "true" coercion still works.
Added coverage for recognised strings, numeric 1/0, the type-error
regression guard, absent/empty handling, the required+false case, and a
client/server parity test driving both checkField and the browser runtime
through the same inputs.
Blast radius: searched packages/, examples/, services/ for v.boolean()
usage; all existing call sites (auth consent/rememberDevice, db 'active'
default, example consent checkboxes) feed true/false/'on'/absent values,
none of which change behavior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Client codegen chained .then().catch(), so a .catch() after .then()
caught exceptions thrown by the response body too. Switched to the
two-argument then(onFulfilled, onRejected) form, whose rejection
handler cannot see errors from the fulfilment handler.
SSR codegen wrapped both the transport call and the response-body eval
in the same try; only __wrnexusCallApi is now inside the try, and
__wrnexusEvalData runs after it, outside.
Also verifies (and locks in with a regression test) that GET query
numbers already coerce correctly through defineEndpoint + checkField,
and documents that in the typed-api-block spec.
B1: qualify each generated __wrn_api_check_* assertion name with a short
hash of the page's path (relative to app/, for reproducibility across
checkouts) so two pages declaring a same-named block no longer collide
with an identical type alias (TS2300).
B2: skip assertion emission for any block that is not client-mode, or
that has zero declared request fields. ssr sectioned blocks can never
declare a request and always fell back to Record<string, never>, whose
keyof is `string` -- making the key-exactness arm of AssertAssignable
evaluate to false unconditionally (TS2344) on every ssr sectioned block
regardless of correctness. Chose to skip both non-client blocks and
zero-field client blocks, since neither has anything meaningful to
assert type-safety about.
B3: only resolve the endpoint's input (query params / ctx.req.json())
when the endpoint declares an input schema. Previously the router-set
fix accidentally read the request body unconditionally, so a handler
with no input schema that parses the request itself hit
ERR_BODY_ALREADY_USED.
B4: run response/error bodies in client-mode api blocks through
eraseFunctionTypes, matching every other browser-bound body in
client-codegen.ts, so a TypeScript-only construct inside one (e.g. an
annotated locally-declared function) doesn't reach the .mjs artifact.
B5: only exclude "api" from state/prop destructuring in the generated
browser module when the page actually has client-mode api blocks (i.e.
there is a real `api` binding to shadow). Previously "api" was always
excluded, so a page with `state api` and no api blocks got an
undeclared `api` reference (ReferenceError) in client code.
B6: prefix each emitted assertion with `export`, so it isn't flagged as
an unused local under a downstream project's noUnusedLocals (TS6196).
B7: wrnexusCallApi now resolves with undefined for an ok 204/205
response, or an ok response with an empty/unparseable body, instead of
rejecting with "Response was not valid JSON" -- matching the spec's
failure table (error path only for non-2xx, network failure, or an
actually unparseable body on a non-empty response).
Regenerated examples/basic-app's generated types and editor bundles to
match. Confirmed the example's type gate still fails when an
unaccepted field is added to a request body, and passes cleanly
otherwise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every existing test in endpoint-schema.test.ts passed rawInput
explicitly, so the branch added to endpoint.ts's fix (GET/HEAD query
parsing, JSON body parsing, malformed/absent body fallback) was
exercised by nothing but a manual curl. Add coverage that calls the
endpoint with only a context, matching the real router's calling
convention:
- GET with query parameters populates input from ctx.url.searchParams.
- POST with a JSON body populates input from the parsed body.
- POST with a malformed or absent body does not throw; the schema's
own validation decides the outcome (asserted on the real response).
- An explicit rawInput argument still wins and the request is never
read (the body is drained first, so a second .json() call would
reject if the endpoint tried to read it again) -- the regression
guard for the branch intentionally left untouched.
Confirmed the GET and POST-body tests fail against the pre-fix
endpoint.ts (input resolves as undefined/null instead of the sent
value); the malformed/absent-body test does not distinguish pre- and
post-fix, because in that specific edge case both normalize to an
effectively empty input -- noted in the report rather than forced.
Added a CHANGELOG entry documenting the behavior change for
downstream apps: a request that previously passed vacuous validation
on a defineEndpoint route can now legitimately fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
directory.ts previously exported a plain (ctx: Context) => ... handler.
With that shape ApiInput<> resolved to unknown, so the generated
__wrn_api_check assertion for the demo page passed trivially even with
a field the endpoint does not accept -- the worked example did not
demonstrate the type safety it exists to demonstrate.
Rewrite directory.ts to use defineEndpoint with a schema (matching
typed-user.ts), which gives the generated assertion a real input type
to check against. Confirmed: adding an unaccepted field to the block's
request body now fails typecheck naming
__wrn_api_check_searchDirectory; removing it passes with zero net
diff.
Fix a real bug this surfaced: packages/core/src/endpoint.ts only read
its input from a second 'rawInput' argument, but the actual HTTP
router (packages/dev-server/src/runtime.ts handleApi) invokes route
handlers as handler(ctx) with no second argument. Every
defineEndpoint-based route -- including the pre-existing typed-user.ts
example -- silently received an empty/undefined input through the
real router (confirmed via curl: valid typed-user payloads were
rejected as 'Required'; directory's name filter matched every record
regardless of query). Fixed by having the endpoint wrapper parse the
request itself (query params for GET/HEAD, JSON body otherwise) when
no rawInput is explicitly supplied, while still honoring an explicit
rawInput for direct/unit-test callers.
Also update api-block-demo.wrn's response section: defineEndpoint
wraps handler output as { data: ... }, so the block's raw response
body is now { data: { users: [...] } } -- response reads
data.data.users instead of data.users.
Re-verified in a real browser after the endpoint rewrite and the
router fix: search returns exactly "Ajay, Asha", exactly one
POST /api/directory carrying x-csrf-token, and the error section
still runs cleanly (no exception, empty result) on a missing route.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Add examples/basic-app/app/api/directory.ts and app/pages/api-block-demo.wrn
as the end-to-end worked example for typed api blocks (Task 6).
- Add **/*.generated.api-checks.ts to .prettierignore: this generated file
must match the CLI's raw output byte-for-byte for check:generated-types,
and prettier was reformatting it.
- Fix packages/csr/test/api-call.test.ts: no-unsafe-function-type lint error
from the raw Function type, uncovered while running the full gate.
- Rebuild editors/vscode bundles (packages/compiler and packages/syntax
changed in Tasks 1-5).
- Regenerate docs/public-api-0.8.json (additive: isIdentPart, isIdentStart,
skipLiteralOrComment newly exported from packages/syntax).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AssertAssignable was one-directional ([Actual] extends [Expected]), so a
block declaring a field the contract doesn't accept passed silently
(TypeScript's excess-property check only applies to fresh object
literals, not conditional-type extends). Add a key-exactness check
(Exclude<keyof Actual, keyof Expected> extends never) alongside the
assignability check. Guard it with 'unknown extends Expected' so
untyped (no defineEndpoint contract) routes still only warn, per the
existing behaviour, instead of being forced to fail on every declared
field.
Add packages/cli/test/api-block-types.test.ts cases that regenerate a
fixture and run the real TypeScript compiler (via bunx tsc) over the
generated output, asserting on its diagnostics rather than on emitted
text: matching fields compile clean; a wrong-typed field, an extra
field (the Finding-A regression guard), and a missing required field
all fail, each pointing at the offending block's __wrn_api_check_*
line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
skipLibCheck exempts .d.ts contents from being checked, so assertions
written inside wrnexus.generated.d.ts were never evaluated by tsc.
Emit them into wrnexus.generated.api-checks.ts instead, referencing
the WRNexusGenerated namespace's helper types (which stay in the
.d.ts). Track the new generated file in check:generated-types.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reuse tokenizer.readBalancedBraces string/comment skipping (extracted as
skipLiteralOrComment) for both section detection and slicing, instead of a
second hand-rolled brace counter. Fixes truncation on braces inside strings
and false-positive sectioned detection from keywords inside comments/strings.
Also switch api-sections.ts errors from plain Error to LexError so parser.ts
upgrades them to ParseError with an offset.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Strips TypeScript from client function bodies when emitting browser
modules, so `wrnexus build` no longer fails on an annotated local.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`wrnexus build` failed on any client function whose body used TypeScript:
const requestBody: Record<string, unknown> = {}
error: Expected ";" but found ":"
Codegen copies a client function's body into the browser module verbatim.
It removes the types from the function's *signature*, which is what made
this easy to miss -- the emitted module looked transpiled, and only bodies
carried types through. The artifact is written as .mjs and read back as
plain JavaScript, so the failure surfaced as a syntax error in generated
code rather than at the .wrn line responsible.
Browser modules are now transpiled before they are written, at all three
sites that emit one (the production build and both dev-server paths).
Reproduced end to end: a page with an annotated body failed the build with
the reported errors, and after the fix builds, ships valid minified JS, and
runs -- the handler sets its state correctly in a browser.
Note: the same body is also embedded as a string for the CSP-safe fallback
interpreter, which still receives it untranspiled. The compiled module
shadows the fallback, so this is only reachable in the window before that
module loads. Left alone here because stripping it lives in codegen, which
also runs under Node in the editor bundle where the Bun transpiler is
unavailable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Client control blocks and loops, the dev-server rebuild recycle, the
editor's tag and completion handling, and the island mount/HMR fixes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two faults found by driving the island demo in a real browser. Both were
silent: the markup, every asset, and all 48 island tests were correct
either way.
An island renders nothing until it mounts, so its placeholder is
zero-height, and IntersectionObserver does not treat a zero-area target
consistently -- client:visible islands mounted on one load and not the
next. Visibility for those is now decided from the element's own rect,
driven by scroll and resize; a placeholder with real size still uses the
observer. The strategy had no test at all, which is why this shipped.
After an island source edit the browser kept running the old code. The
rebuild worked and the file was refetched, but the loader imports a URL
that does not change, and the browser caches modules by URL. Remounts now
carry a generation the dev loader folds into the request.
Verified in the browser: mounts with start={3} as a number, clicks reach
React (3 -> 5), and an edit to Counter.tsx now shows the new text and
stays interactive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dev server got slower the longer it ran. Measured on the example app:
30 .wrn edits grew RSS from 117 MB to 137 MB and never gave it back, while
30 CSS edits cost nothing -- so the leak is exactly one retained module
identity per rebuild, not caches or file handles.
That is inherent to reloading a module in-process. Bun caches modules by
path, so a rebuild has to be given a new identity to be picked up at all,
and Bun has no API to unload the old one. At roughly 0.66 MB a rebuild, a
long editing session is several hundred megabytes of garbage that cannot
be collected.
The process now recycles itself past a rebuild threshold, exiting with the
RESTART_EXIT_CODE the CLI supervisor already respawns on; browsers
reconnect because the HMR client already retries. It waits for a quiet
period first so a live request is never cut off, and the threshold (300
rebuilds, about 200 MB) sits well above a normal session. Set
WRNEXUS_DEV_RECYCLE_AFTER to tune it, or 0 to switch it off.
Also bounds browserArtifactPaths and islandArtifactPaths, which are keyed
by content hash and so gained an entry per rebuild that was never read
again. Small next to the module leak, but unbounded is unbounded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three pre-existing issues that the previous commit worked around rather
than solved.
Test global pollution. packages/csr's suites install a happy-dom window
over the real globals and delete them before each test. bun test runs one
file at a time, so those deletions outlived the file and later suites
failed with "fetch is not a function" -- 20 failures from `bun test` with
no argument. They now restore what they captured. The editor's Node tests
shim the vscode host by patching Module._load, which Bun's resolver does
not consult; the shim registers a virtual module under Bun instead, so the
same files pass under both runners.
Multi-cursor tag auto-close. The handler now closes the tag at every
caret. Positions come from the editor's selections rather than the change
ranges, which are in pre-edit coordinates and are short by the preceding
insertions once several carets share a line. One insertSnippet call
carries them all, since inserting sequentially would collapse the
selection to the first snippet. Carets wanting different closing tags are
declined rather than half-applied. Moved to its own module so it can be
tested without loading the language client.
Runtime size. Trimmed 2,414 bytes: the global lookup tables became one
prototype-safe scheme (a name like "toString" was previously a hit on
Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the
repeated chains, and dead code went. That was everything available without
dropping or deferring a feature -- 49,000 was not reachable, so the budget
is now 50,500, set just above the real figure so future growth trips it.
Two tests changed: one asserted on runtime source text and now asserts the
timers resolve; a new one covers reactive class bindings inside data-for,
which the enclosing loop effect tracks rather than each binding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The client runtime had no loop support, so any shared function using one
returned early -- Pagination and ButtonGroup were broken client-side, not
just in tests.
Adding loops exposed two further faults:
- A var reaching writeScope creates a signal and triggers a render sweep.
A declaration inside a function called during a render therefore looped
forever. Declarations now bind into the handler locals instead.
- A control block removed from the DOM keeps its effect in the renderers
list. Running it against a detached node threw, aborting the sweep and
leaving every later effect stale.
Also raises the reactive runtime budget to 53,000: the runtime had already
grown past 49,000 before this change, and 52,570 minified is 16,803 gzipped.
Two deferred minors: html-service leaves absent documentation undefined
rather than an empty string, and the extension declines tag auto-close on
multi-cursor edits rather than closing only the first cursor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
{#if}, {#each} and their {:else}/{:else if}/{:empty} branches worked on
the server and after hydration, but a block nested inside another block
stayed empty once the outer block rerendered. Adding a row to a list
produced the row's markup with its inner block markers in place and
nothing between them, for the life of the page.
Two causes, both on the client-created path only:
reactive() registers an effect; effects run when renderAll sweeps the
list. A state change runs just the affected effects rather than sweeping,
so an effect registered during that rerender was queued and never
invoked. setupControlBlock now returns its runner and the creating block
invokes it immediately.
The first reactive pass is skipped so hydration does not discard
server-rendered DOM. A block created by a rerender has no server DOM, so
skipping its only pass left it permanently empty. firstRun is now keyed
off outerLocals, which is set only on the client-created path.
Verified in a browser as well as in tests: adding a group to a list now
renders the new row's nested {:else}, and the existing rows' nested loops
survive the rerender.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>